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