import React, { useState, useCallback, useRef, useEffect } from 'react'; import { Text, View, TouchableOpacity, ScrollView, TextInput } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { BottomDialog } from './dialog'; import MyText from './text'; import { useTheme } from '../../hooks/theme-context'; import tw from '../lib/tailwind'; import { colors } from '../lib/theme-colors'; import type { DropdownOption as BaseDropdownOption } from '@packages/shared'; import type { DropdownBaseProps } from './dropdown'; // Bottom/multi dropdown option — shared base + per-option disabled flag export interface DropdownOption extends BaseDropdownOption { disabled?: boolean; } interface BottomDropdownProps extends DropdownBaseProps { topLabel?: string; value: string | number | string[] | number[]; onValueChange: (value: string | number | string[] | number[]) => void; multiple?: boolean; triggerComponent?: React.ComponentType<{ onPress: () => void; disabled?: boolean; displayText: string; }>; onSearch?: (query: string) => void; testID?: string; } const BottomDropdown: React.FC = ({ label, topLabel, value, options, onValueChange, multiple = false, error, style, placeholder, disabled, className, triggerComponent, onSearch, testID, }) => { const { theme } = useTheme(); const [isOpen, setIsOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const debounceTimeoutRef = useRef(null); // Debounced search function const debouncedOnSearch = useCallback((query: string) => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } debounceTimeoutRef.current = setTimeout(() => { onSearch?.(query); }, 2000); }, [onSearch]); // Cleanup on unmount useEffect(() => { return () => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } }; }, []); // Filter options based on search query const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchQuery.toLowerCase()) ); const getDisplayText = () => { if (multiple) { const selectedValues = value as string[]; if (selectedValues.length === 0) return placeholder ?? label; const selectedLabels = options .filter(option => selectedValues.includes(option.value as string)) .map(option => option.label); return selectedLabels.length > 0 ? selectedLabels.join(', ') : (placeholder ?? label); } else { const selectedOption = options.find(option => option.value === value); return selectedOption ? selectedOption.label : (placeholder ?? label); } }; const handleSelect = (optionValue: string | number) => { if (multiple) { const currentValues = value as string[]; const newValues = currentValues.includes(optionValue as string) ? currentValues.filter(v => v !== optionValue) : [...currentValues, optionValue as string]; onValueChange(newValues); } else { onValueChange(optionValue); setIsOpen(false); } }; const handleDone = () => { setIsOpen(false); setSearchQuery(''); // Clear search when done if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); debounceTimeoutRef.current = null; } }; const isSelected = (optionValue: string | number) => { if (multiple) { return (value as string[]).includes(optionValue as string); } else { return value === optionValue; } }; return ( {topLabel && ( {topLabel} )} {triggerComponent ? ( React.createElement(triggerComponent, { onPress: () => !disabled && setIsOpen(true), disabled, displayText: getDisplayText(), }) ) : ( !disabled && setIsOpen(true)} disabled={disabled} > 0 : options.some(opt => opt.value === value)) ? tw`text-gray-900 font-medium` : tw`text-gray-500`, disabled && tw`text-gray-400`, ]} numberOfLines={1} > {getDisplayText()} )} { setIsOpen(false); setSearchQuery(''); // Clear search when closed if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); debounceTimeoutRef.current = null; } }}> {label} {/* Search Input - Fixed at top */} {onSearch && ( { setSearchQuery(text); debouncedOnSearch(text); }} autoFocus={true} clearButtonMode="while-editing" /> )} {filteredOptions.length === 0 && searchQuery ? ( No options found for "{searchQuery}" ) : ( filteredOptions.map((option) => { const selected = isSelected(option.value); const isDisabled = option.disabled || disabled; return ( !isDisabled && handleSelect(option.value)} disabled={isDisabled} > {(() => { const labelStr = option.label as string; const [code, rest] = labelStr.split(' - ', 2); return ( {code} {rest ? ` - ${rest}` : ''} ); })()} {selected && ( )} ); }) )} {multiple && ( Done )} ); }; export default BottomDropdown;