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; defaultValue?: string | number; options: SelectOption[]; onChange?: (value: string | number | undefined) => void; placeholder?: string; disabled?: boolean; loading?: boolean; allowClear?: boolean; showSearch?: boolean; mode?: "single" | "multiple"; // multiple not implemented yet style?: StyleProp; size?: "small" | "middle" | "large"; listStyle?: StyleProp; } /** * Select * A Select component inspired by Ant Design, adapted for React Native. * Supports single 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 [selectedValue, setSelectedValue] = useState< string | number | undefined >(value ?? defaultValue); const [isOpen, setIsOpen] = useState(false); const [searchText, setSearchText] = useState(""); const [containerHeight, setContainerHeight] = useState(0); useEffect(() => { setSelectedValue(value); }, [value]); const filteredOptions = showSearch ? options.filter((opt) => opt.label.toLowerCase().includes(searchText.toLowerCase()) ) : options; const selectedOption = options.find((opt) => opt.value === selectedValue); const handleSelect = (val: string | number) => { setSelectedValue(val); onChange?.(val); setIsOpen(false); setSearchText(""); }; const handleClear = () => { setSelectedValue(undefined); 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]; return ( !disabled && !loading && setIsOpen(!isOpen)} disabled={disabled || loading} activeOpacity={0.8} onLayout={(e) => setContainerHeight(e.nativeEvent.layout.height)} > {loading ? ( ) : ( {selectedOption?.label || placeholder} )} {allowClear && selectedValue && !loading ? ( ) : null} {isOpen && ( {showSearch && ( )} {filteredOptions.map((item) => ( !item.disabled && handleSelect(item.value)} disabled={item.disabled} > {item.label} {selectedValue === item.value && ( )} ))} )} ); }; const styles = StyleSheet.create({ wrapper: { position: "relative", }, container: { borderWidth: 1, borderColor: "#e6e6e6", borderRadius: 8, backgroundColor: "#fff", flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, content: { flex: 1, }, text: { color: "#111", }, suffix: { flexDirection: "row", alignItems: "center", }, icon: { marginRight: 8, }, arrow: { marginLeft: 4, }, dropdown: { position: "absolute", left: 0, right: 0, backgroundColor: "#fff", borderWidth: 1, borderColor: "#e6e6e6", 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, borderColor: "#e6e6e6", borderRadius: 4, padding: 8, margin: 8, }, list: { maxHeight: 200, }, option: { padding: 12, borderBottomWidth: 1, borderBottomColor: "#f0f0f0", flexDirection: "row", justifyContent: "space-between", alignItems: "center", }, optionDisabled: { opacity: 0.5, }, optionSelected: { backgroundColor: "#f6ffed", }, optionText: { fontSize: 16, color: "#111", }, optionTextDisabled: { color: "#999", }, optionTextSelected: { color: "#4ecdc4", fontWeight: "600", }, }); export default Select;