react-native-reusables
react-native-reusables copied to clipboard
I'm trying to use the combobox component but it's not working properly
Combobox.tsx
import * as React from "react";
import {Text, View, type ListRenderItemInfo} from "react-native";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import {cn} from "../../lib/utils";
import {Check, ChevronsUpDown, Search} from "../Icons";
import {
BottomSheet,
BottomSheetContent,
BottomSheetFlatList,
BottomSheetHeader,
BottomSheetOpenTrigger,
BottomSheetTextInput,
useBottomSheet,
} from "./bottom-sheet";
import {Button, buttonTextVariants, buttonVariants} from "./button";
const HEADER_HEIGHT = 130;
interface ComboboxOption {
label?: string;
value?: string;
}
const Combobox = React.forwardRef<
React.ElementRef<typeof Button>,
Omit<React.ComponentPropsWithoutRef<typeof Button>, "children"> & {
items: ComboboxOption[];
placeholder?: string;
inputProps?: React.ComponentPropsWithoutRef<typeof BottomSheetTextInput>;
emptyText?: string;
defaultSelectedItem?: ComboboxOption | null;
selectedItem?: ComboboxOption | null;
onSelectedItemChange?: (option: ComboboxOption | null) => void;
}
>(
(
{
className,
textClass,
variant = "outline",
size = "sm",
inputProps,
placeholder,
items,
emptyText = "Nothing found...",
defaultSelectedItem = null,
selectedItem: selectedItemProp,
onSelectedItemChange,
...props
},
ref,
) => {
const insets = useSafeAreaInsets();
const [search, setSearch] = React.useState("");
const [selectedItem, setSelectedItem] =
React.useState<ComboboxOption | null>(defaultSelectedItem);
const bottomSheet = useBottomSheet();
const inputRef =
React.useRef<React.ComponentRef<typeof BottomSheetTextInput>>(null);
const listItems = React.useMemo(() => {
return search
? items.filter((item) => {
return item.label
?.toLocaleLowerCase()
.includes(search.toLocaleLowerCase());
})
: items;
}, [items, search]);
function onItemChange(listItem: ComboboxOption) {
if (selectedItemProp?.value === listItem.value) {
return null;
}
setSearch("");
bottomSheet.close();
return listItem;
}
const renderItem = React.useCallback(
({ item }: ListRenderItemInfo<unknown>) => {
const listItem = item as ComboboxOption;
const isSelected = onSelectedItemChange
? selectedItemProp?.value === listItem.value
: selectedItem?.value === listItem.value;
return (
<Button
variant="ghost"
className="flex-1 flex-row items-center justify-between px-3 py-4"
style={{ minHeight: 70 }}
onPress={() => {
if (onSelectedItemChange) {
onSelectedItemChange(onItemChange(listItem));
return;
}
setSelectedItem(onItemChange(listItem));
}}
>
<View className="flex-1 flex-row">
<Text className={"text-foreground text-xl"}>
{listItem.label}
</Text>
</View>
{isSelected && (
<Check size={24} className={"mt-1.5 px-6 text-foreground"} />
)}
</Button>
);
},
[selectedItem, selectedItemProp],
);
function onSubmitEditing() {
const firstItem = listItems[0];
if (!firstItem) return;
if (onSelectedItemChange) {
onSelectedItemChange(firstItem);
} else {
setSelectedItem(firstItem);
}
bottomSheet.close();
}
function onSearchIconPress() {
if (!inputRef.current) return;
const input = inputRef.current;
if (input && "focus" in input && typeof input.focus === "function") {
input.focus();
}
}
const itemSelected = onSelectedItemChange ? selectedItemProp : selectedItem;
return (
<BottomSheet>
<BottomSheetOpenTrigger
ref={ref}
className={buttonVariants({
variant,
size,
className: cn("w-full flex-row", className),
})}
role="combobox"
{...props}
>
<View className="flex-1 flex-row justify-between ">
<Text
className={buttonTextVariants({
variant,
size,
className: cn(!itemSelected && "opacity-50", textClass),
})}
numberOfLines={1}
>
{itemSelected ? itemSelected.label : placeholder ?? ""}
</Text>
<ChevronsUpDown className="ml-2 text-foreground opacity-50" />
</View>
</BottomSheetOpenTrigger>
<BottomSheetContent
ref={bottomSheet.ref}
onDismiss={() => {
setSearch("");
}}
>
<BottomSheetHeader className="border-b-0">
<Text className="px-0.5 text-center font-bold text-foreground text-xl">
{placeholder}
</Text>
</BottomSheetHeader>
<View className="relative border-border border-b px-4 pb-4">
<BottomSheetTextInput
role="searchbox"
ref={inputRef}
className="pl-12"
value={search}
onChangeText={setSearch}
onSubmitEditing={onSubmitEditing}
returnKeyType="next"
clearButtonMode="while-editing"
placeholder="Search..."
{...inputProps}
/>
<Button
variant={"ghost"}
size="sm"
className="absolute top-2.5 left-4"
onPress={onSearchIconPress}
>
<Search size={18} className="text-foreground opacity-50" />
</Button>
</View>
<BottomSheetFlatList
data={listItems}
contentContainerStyle={{
paddingBottom: insets.bottom + HEADER_HEIGHT,
}}
renderItem={renderItem}
keyExtractor={(item, index) =>
(item as ComboboxOption)?.value ?? index.toString()
}
className={"px-4"}
keyboardShouldPersistTaps="handled"
ListEmptyComponent={() => {
return (
<View
className="flex-1 flex-row items-center justify-center px-3 py-5"
style={{ minHeight: 70 }}
>
<Text className={"text-center text-muted-foreground text-xl"}>
{emptyText}
</Text>
</View>
);
}}
/>
</BottomSheetContent>
</BottomSheet>
);
},
);
Combobox.displayName = "Combobox";
export {Combobox, type ComboboxOption};
When clicking on the input it opens up the bottom sheet, it consists of a input and a list of options. When I click on the input, the bottom sheet closes automatically.
BottomSheet.tsx:
import type { BottomSheetFooterProps as GBottomSheetFooterProps } from "@gorhom/bottom-sheet";
import {
BottomSheetFlatList as GBottomSheetFlatList,
BottomSheetFooter as GBottomSheetFooter,
BottomSheetTextInput as GBottomSheetTextInput,
BottomSheetView as GBottomSheetView,
useBottomSheetModal,
type BottomSheetBackdrop,
type BottomSheetModal,
} from "@gorhom/bottom-sheet";
import React, { useCallback } from "react";
import {
Keyboard,
Pressable,
View,
type GestureResponderEvent,
type ViewStyle,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { X } from "../../components/Icons";
import { cn } from "../../lib/utils";
import * as Slot from "../primitives/slot";
import { Button } from "./button";
// !IMPORTANT: This file is only for web.
type BottomSheetRef = React.ElementRef<typeof View>;
type BottomSheetProps = React.ComponentPropsWithoutRef<typeof View>;
interface BottomSheetContext {
sheetRef: React.RefObject<BottomSheetModal>;
}
const BottomSheetContext = React.createContext({} as BottomSheetContext);
const BottomSheet = React.forwardRef<BottomSheetRef, BottomSheetProps>(
({ ...props }, ref) => {
return <View ref={ref} {...props} />;
},
);
type BottomSheetContentRef = React.ElementRef<typeof BottomSheetModal>;
type BottomSheetContentProps = Omit<
React.ComponentPropsWithoutRef<typeof BottomSheetModal>,
"backdropComponent"
> & {
backdropProps?: Partial<
React.ComponentPropsWithoutRef<typeof BottomSheetBackdrop>
>;
};
const BottomSheetContent = React.forwardRef<
BottomSheetContentRef,
BottomSheetContentProps
>(() => {
return null;
});
const BottomSheetOpenTrigger = React.forwardRef<
React.ElementRef<typeof Pressable>,
React.ComponentPropsWithoutRef<typeof Pressable> & {
asChild?: boolean;
}
>(({ onPress, asChild = false, ...props }, ref) => {
function handleOnPress() {
window.alert(
"Not implemented for web yet. Check `bottom-sheet.tsx` for more info.",
);
}
const Trigger = asChild ? Slot.Pressable : Pressable;
return <Trigger ref={ref} onPress={handleOnPress} {...props} />;
});
const BottomSheetCloseTrigger = React.forwardRef<
React.ElementRef<typeof Pressable>,
React.ComponentPropsWithoutRef<typeof Pressable> & {
asChild?: boolean;
}
>(({ onPress, asChild = false, ...props }, ref) => {
const { dismiss } = useBottomSheetModal();
function handleOnPress(ev: GestureResponderEvent) {
dismiss();
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
onPress?.(ev);
}
const Trigger = asChild ? Slot.Pressable : Pressable;
return <Trigger ref={ref} onPress={handleOnPress} {...props} />;
});
const BOTTOM_SHEET_HEADER_HEIGHT = 60; // BottomSheetHeader height
type BottomSheetViewProps = Omit<
React.ComponentPropsWithoutRef<typeof GBottomSheetView>,
"style"
> & {
hadHeader?: boolean;
style?: ViewStyle;
};
function BottomSheetView({
className,
children,
hadHeader = true,
style,
...props
}: BottomSheetViewProps) {
const insets = useSafeAreaInsets();
return (
<GBottomSheetView
style={[
{
paddingBottom:
insets.bottom + (hadHeader ? BOTTOM_SHEET_HEADER_HEIGHT : 0),
},
style,
]}
className={cn(`px-4`, className)}
{...props}
>
{children}
</GBottomSheetView>
);
}
type BottomSheetTextInputRef = React.ElementRef<typeof GBottomSheetTextInput>;
type BottomSheetTextInputProps = React.ComponentPropsWithoutRef<
typeof GBottomSheetTextInput
>;
const BottomSheetTextInput = React.forwardRef<
BottomSheetTextInputRef,
BottomSheetTextInputProps
>(({ className, placeholderClassName, ...props }, ref) => {
return (
<GBottomSheetTextInput
ref={ref}
className={cn(
"h-14 items-center rounded-md border border-input bg-background px-3 text-foreground text-xl leading-[1.25] placeholder:text-muted-foreground disabled:opacity-50",
className,
)}
placeholderClassName={cn("text-muted-foreground", placeholderClassName)}
{...props}
/>
);
});
type BottomSheetFlatListRef = React.ElementRef<typeof GBottomSheetFlatList>;
type BottomSheetFlatListProps = React.ComponentPropsWithoutRef<
typeof GBottomSheetFlatList
>;
const BottomSheetFlatList = React.forwardRef<
BottomSheetFlatListRef,
BottomSheetFlatListProps
>(({ className, ...props }, ref) => {
const insets = useSafeAreaInsets();
return (
<GBottomSheetFlatList
ref={ref}
contentContainerStyle={[{ paddingBottom: insets.bottom }]}
className={cn("py-4", className)}
keyboardShouldPersistTaps="handled"
{...props}
/>
);
});
type BottomSheetHeaderRef = React.ElementRef<typeof View>;
type BottomSheetHeaderProps = React.ComponentPropsWithoutRef<typeof View>;
const BottomSheetHeader = React.forwardRef<
BottomSheetHeaderRef,
BottomSheetHeaderProps
>(({ className, children, ...props }, ref) => {
const { dismiss } = useBottomSheetModal();
function close() {
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
dismiss();
}
return (
<View
ref={ref}
className={cn(
"flex-row items-center justify-between border-border border-b pl-4",
className,
)}
{...props}
>
{children}
<Button onPress={close} variant="ghost" className="pr-4">
<X className="text-muted-foreground" size={24} />
</Button>
</View>
);
});
type BottomSheetFooterRef = React.ElementRef<typeof View>;
type BottomSheetFooterProps = Omit<
React.ComponentPropsWithoutRef<typeof View>,
"style"
> & {
bottomSheetFooterProps: GBottomSheetFooterProps;
children?: React.ReactNode;
style?: ViewStyle;
};
/**
* To be used in a useCallback function as a props to BottomSheetContent
*/
const BottomSheetFooter = React.forwardRef<
BottomSheetFooterRef,
BottomSheetFooterProps
>(({ bottomSheetFooterProps, children, className, style, ...props }, ref) => {
const insets = useSafeAreaInsets();
return (
<GBottomSheetFooter {...bottomSheetFooterProps}>
<View
ref={ref}
style={[{ paddingBottom: insets.bottom + 6 }, style]}
className={cn("px-4 pt-1.5", className)}
{...props}
>
{children}
</View>
</GBottomSheetFooter>
);
});
function useBottomSheet() {
const ref = React.useRef<BottomSheetContentRef>(null);
const open = useCallback(() => {
ref.current?.present();
}, []);
const close = useCallback(() => {
ref.current?.dismiss();
}, []);
return { ref, open, close };
}
export {
BottomSheet,
BottomSheetCloseTrigger,
BottomSheetContent,
BottomSheetFlatList,
BottomSheetFooter,
BottomSheetHeader,
BottomSheetOpenTrigger,
BottomSheetTextInput,
BottomSheetView,
useBottomSheet,
};
If someone could help me solve this I'm using this for address autocomplete
@briskteq-faiz What is the issue exactly? Is the following?
When clicking on the input it opens up the bottom sheet, it consists of a input and a list of options. When I click on the input, the bottom sheet closes automatically.
If you want to prevent that, you can remove bottomSheet.close(); from the onItemChange function.
Please use note that that is a deprecated component. I also noticed, that you are missing at least one class for android devices so you can copy the latest version on the component.
If that is not the issue, please provide more details and a minimal reproduction repo.
Closed due to issue being out of newly defined scope.