From 8396a9cbdc7724039c1dd040cfac13457696ccbc Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Thu, 21 Aug 2025 11:35:06 +0200 Subject: [PATCH] feat: new select bottom sheet --- components/common/SelectBottomSheet.tsx | 190 ++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 components/common/SelectBottomSheet.tsx diff --git a/components/common/SelectBottomSheet.tsx b/components/common/SelectBottomSheet.tsx new file mode 100644 index 00000000..9d968a0d --- /dev/null +++ b/components/common/SelectBottomSheet.tsx @@ -0,0 +1,190 @@ +import { Ionicons } from "@expo/vector-icons"; +import { + BottomSheetBackdrop, + type BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetScrollView, +} from "@gorhom/bottom-sheet"; +import React, { useCallback, useImperativeHandle, useRef } from "react"; +import { Platform, TouchableOpacity, View } from "react-native"; +import { Text } from "./Text"; + +export interface SelectOption { + id: string; + label: string; + value?: string | number; + selected?: boolean; + onSelect?: () => void; +} + +export interface SelectOptionGroup { + id: string; + title: string; + options: SelectOption[]; +} + +export interface SelectBottomSheetRef { + present: () => void; + dismiss: () => void; +} + +interface SelectBottomSheetProps { + title?: string; + subtitle?: string; + groups: SelectOptionGroup[]; + triggerIcon?: keyof typeof Ionicons.glyphMap; + triggerSize?: number; + triggerColor?: string; + customTrigger?: React.ReactNode; +} + +const SelectBottomSheet = React.forwardRef< + SelectBottomSheetRef, + SelectBottomSheetProps +>( + ( + { + title, + subtitle, + groups, + triggerIcon = "ellipsis-horizontal", + triggerSize = 24, + triggerColor = "white", + customTrigger, + }, + ref, + ) => { + const bottomSheetModalRef = useRef(null); + + const present = useCallback(() => { + bottomSheetModalRef.current?.present(); + }, []); + + const dismiss = useCallback(() => { + bottomSheetModalRef.current?.dismiss(); + }, []); + + useImperativeHandle( + ref, + () => ({ + present, + dismiss, + }), + [present, dismiss], + ); + + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => ( + + ), + [], + ); + + const handleOptionSelect = useCallback( + (option: SelectOption) => { + option.onSelect?.(); + dismiss(); + }, + [dismiss], + ); + + if (Platform.isTV) { + // On TV, fall back to a different UI pattern or return null + return null; + } + + return ( + <> + {customTrigger ? ( + {customTrigger} + ) : ( + + + + )} + + + + + {title && ( + + + {title} + + {subtitle && ( + {subtitle} + )} + + )} + + + {groups.map((group) => ( + + + {group.title} + + + {group.options.map((option) => ( + handleOptionSelect(option)} + > + + {option.label} + + {option.selected && ( + + )} + + ))} + + + ))} + + + + + + ); + }, +); + +SelectBottomSheet.displayName = "SelectBottomSheet"; + +export default SelectBottomSheet;