import { useThemeContext } from "@/hooks/use-theme-context"; import { AntDesign } from "@expo/vector-icons"; import React, { useEffect, useState } from "react"; import { ActivityIndicator, ScrollView, StyleProp, StyleSheet, Text, TextInput, TouchableOpacity, View, ViewStyle, } from "react-native"; export interface SelectOption { label: string; value: string | number; disabled?: boolean; } export interface SelectProps { value?: string | number | (string | number)[]; defaultValue?: string | number | (string | number)[]; options: SelectOption[]; onChange?: (value: string | number | (string | number)[] | undefined) => void; placeholder?: string; disabled?: boolean; loading?: boolean; allowClear?: boolean; showSearch?: boolean; mode?: "single" | "multiple"; style?: StyleProp; size?: "small" | "middle" | "large"; listStyle?: StyleProp; } /** * Select * A Select component inspired by Ant Design, adapted for React Native. * Supports single and multiple selection, search, clear, loading, disabled states. */ const Select: React.FC = ({ value, defaultValue, options, onChange, placeholder = "Select an option", disabled = false, loading = false, allowClear = false, showSearch = false, mode = "single", style, listStyle, size = "middle", }) => { const initialValue = value ?? defaultValue; const [selectedValues, setSelectedValues] = useState<(string | number)[]>( Array.isArray(initialValue) ? initialValue : initialValue ? [initialValue] : [] ); const [isOpen, setIsOpen] = useState(false); const [searchText, setSearchText] = useState(""); const [containerHeight, setContainerHeight] = useState(0); const [textHeight, setTextHeight] = useState(0); useEffect(() => { const newVal = value ?? defaultValue; setSelectedValues(Array.isArray(newVal) ? newVal : newVal ? [newVal] : []); }, [value, defaultValue]); const filteredOptions = showSearch ? options.filter((opt) => opt.label.toLowerCase().includes(searchText.toLowerCase()) ) : options; const handleSelect = (val: string | number) => { let newSelected: (string | number)[]; if (mode === "single") { newSelected = [val]; } else { newSelected = selectedValues.includes(val) ? selectedValues.filter((v) => v !== val) : [...selectedValues, val]; } setSelectedValues(newSelected); onChange?.( mode === "single" ? newSelected.length > 0 ? newSelected[0] : undefined : newSelected ); if (mode === "single") { setIsOpen(false); setSearchText(""); } }; const handleClear = () => { setSelectedValues([]); onChange?.(undefined); }; const sizeMap = { small: { height: 32, fontSize: 14, paddingHorizontal: 10 }, middle: { height: 40, fontSize: 16, paddingHorizontal: 14 }, large: { height: 48, fontSize: 18, paddingHorizontal: 18 }, }; const sz = sizeMap[size]; // Theme colors from context (consistent with other components) const { colors } = useThemeContext(); const selectBackgroundColor = disabled ? colors.backgroundSecondary : colors.surface; let displayText = placeholder; if (selectedValues.length > 0) { if (mode === "single") { const opt = options.find((o) => o.value === selectedValues[0]); displayText = opt?.label || placeholder; } else { const labels = selectedValues .map((v) => options.find((o) => o.value === v)?.label) .filter(Boolean); displayText = labels.join(", "); } } return ( !disabled && !loading && setIsOpen(!isOpen)} disabled={disabled || loading} activeOpacity={0.8} onLayout={(e) => setContainerHeight(e.nativeEvent.layout.height)} > {loading ? ( ) : ( 0 ? colors.text : colors.textSecondary, }, ]} onLayout={(e) => setTextHeight(e.nativeEvent.layout.height)} > {displayText} )} {allowClear && selectedValues.length > 0 && !loading ? ( ) : null} {isOpen && ( {showSearch && ( )} {filteredOptions.map((item) => ( !item.disabled && handleSelect(item.value)} disabled={item.disabled} > {item.label} {selectedValues.includes(item.value) && ( )} ))} )} ); }; const styles = StyleSheet.create({ wrapper: { position: "relative", }, container: { borderWidth: 1, borderRadius: 8, flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, content: { flex: 1, }, text: { // Color is set dynamically via theme }, suffix: { flexDirection: "row", alignItems: "center", }, icon: { marginRight: 8, }, arrow: { marginLeft: 4, }, dropdown: { position: "absolute", left: 0, right: 0, borderWidth: 1, borderTopWidth: 0, borderRadius: 10, borderBottomLeftRadius: 8, borderBottomRightRadius: 8, shadowColor: "#000", shadowOpacity: 0.1, shadowRadius: 4, shadowOffset: { width: 0, height: 2 }, elevation: 5, zIndex: 1000, }, searchInput: { borderWidth: 1, borderRadius: 4, padding: 8, margin: 8, }, list: { maxHeight: 200, }, option: { padding: 12, borderBottomWidth: 1, flexDirection: "row", justifyContent: "space-between", alignItems: "center", }, optionDisabled: { opacity: 0.5, }, // optionSelected is handled dynamically via inline styles optionText: { fontSize: 16, }, // optionTextDisabled and optionTextSelected are handled dynamically via inline styles }); export default Select;