import { useState, useRef, forwardRef } from 'react'; import { View, Text, Pressable, Modal, ScrollView, TextInput, ActionSheetIOS, Platform, Animated, } from 'react-native'; import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons'; import { SelectProps, SelectOption } from './types'; import { selectStyles } from './Select.styles'; import { BoundedModalContent } from '../internal/BoundedModalContent.native'; import { useSmartPosition } from '../hooks/useSmartPosition.native'; import useMergeRefs from '../hooks/useMergeRefs'; import type { IdealystElement } from '../utils/refTypes'; const Select = forwardRef(({ options, value, onChange, placeholder = 'Select an option', disabled = false, error, helperText, label, type = 'outlined', intent = 'neutral', size = 'md', searchable = false, filterOption, presentationMode = 'dropdown', maxHeight = 240, // Spacing variants from FormInputStyleProps margin, marginVertical, marginHorizontal, style, testID, accessibilityLabel, id, }, ref) => { // Derive hasError boolean from error prop const hasError = Boolean(error); // Get error message if error is a string const errorMessage = typeof error === 'string' ? error : undefined; const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const scaleAnim = useRef(new Animated.Value(0)).current; const { position: dropdownPosition, size: dropdownSize, isPositioned, anchorRef: triggerRef, measureAndPosition, handleLayout: handleDropdownLayout, reset: resetPosition, } = useSmartPosition({ placement: 'bottom-start', offset: 4, maxHeight, matchWidth: true, }); const mergedTriggerRef = useMergeRefs(ref, triggerRef); const selectedOption = options.find(option => option.value === value); // Filter options based on search term const filteredOptions = searchable && searchTerm ? options.filter(option => { if (filterOption) { return filterOption(option, searchTerm); } return option.label.toLowerCase().includes(searchTerm.toLowerCase()); }) : options; // Apply styles with variants selectStyles.useVariants({ type, size, disabled, hasError, focused: isOpen, margin, marginVertical, marginHorizontal, }); // Get dynamic styles - call as functions for theme reactivity const containerStyle = (selectStyles.container as any)({}); const labelStyle = (selectStyles.label as any)({}); const triggerStyle = (selectStyles.trigger as any)({ type, intent, disabled, hasError, focused: isOpen }); const triggerContentStyle = (selectStyles.triggerContent as any)({}); const triggerTextStyle = (selectStyles.triggerText as any)({}); const placeholderStyle = (selectStyles.placeholder as any)({}); const iconStyle = (selectStyles.icon as any)({}); const chevronStyle = (selectStyles.chevron as any)({}); const dropdownStyle = (selectStyles.dropdown as any)({}); const searchContainerStyle = (selectStyles.searchContainer as any)({}); const searchInputStyle = (selectStyles.searchInput as any)({}); const optionsListStyle = (selectStyles.optionsList as any)({}); const optionStyle = (selectStyles.option as any)({}); const optionContentStyle = (selectStyles.optionContent as any)({}); const optionIconStyle = (selectStyles.optionIcon as any)({}); const optionTextStyle = (selectStyles.optionText as any)({}); const optionTextDisabledStyle = (selectStyles.optionTextDisabled as any)({}); const helperTextStyle = (selectStyles.helperText as any)({ hasError }); const overlayStyle = (selectStyles.overlay as any)({}); const handleTriggerPress = () => { if (disabled) return; if (Platform.OS === 'ios' && presentationMode === 'actionSheet') { showIOSActionSheet(); } else { // Measure and position dropdown measureAndPosition(); // Open the modal (it will be invisible with opacity 0 until positioned) setIsOpen(true); setSearchTerm(''); // Animate dropdown appearance Animated.spring(scaleAnim, { toValue: 1, useNativeDriver: true, tension: 300, friction: 20, }).start(); } }; const showIOSActionSheet = () => { const actionOptions = options.map(option => option.label); const cancelButtonIndex = actionOptions.length; actionOptions.push('Cancel'); ActionSheetIOS.showActionSheetWithOptions( { options: actionOptions, cancelButtonIndex, title: label || 'Select an option', message: helperText, }, (buttonIndex: number) => { if (buttonIndex !== cancelButtonIndex && buttonIndex < options.length) { const selectedOption = options[buttonIndex]; if (!selectedOption.disabled) { onChange?.(selectedOption.value); } } } ); }; const handleOptionSelect = (option: SelectOption) => { if (!option.disabled) { onChange?.(option.value); closeDropdown(); } }; const closeDropdown = () => { Animated.spring(scaleAnim, { toValue: 0, useNativeDriver: true, tension: 300, friction: 20, }).start(() => { setIsOpen(false); resetPosition(); setSearchTerm(''); }); }; const handleSearchChange = (text: string) => { setSearchTerm(text); }; const renderChevron = () => { return ( ); }; const renderTriggerIcon = (icon: any) => { if (!icon) return null; // If it's a string, render as MaterialCommunityIcons if (typeof icon === 'string') { return ( ); } // Otherwise render the React element directly return {icon}; }; const renderOptionIcon = (icon: any) => { if (!icon) return null; // If it's a string, render as MaterialCommunityIcons if (typeof icon === 'string') { return ( ); } // Otherwise render the React element directly return {icon}; }; const renderDropdown = () => { if (!isOpen) return null; // Show dropdown only after it has been measured AND positioned const isMeasured = dropdownSize.height > 0; const shouldShow = isMeasured && isPositioned; return ( {searchable && ( )} {filteredOptions.map((option) => ( handleOptionSelect(option)} disabled={option.disabled} android_ripple={{ color: 'rgba(0, 0, 0, 0.1)' }} > {renderOptionIcon(option.icon)} {option.label} ))} {filteredOptions.length === 0 && ( No options found )} ); }; return ( {label && ( {label} )} {renderTriggerIcon(selectedOption?.icon)} {selectedOption ? selectedOption.label : placeholder} {renderChevron()} {/* Only render dropdown modal if not using iOS ActionSheet */} {!(Platform.OS === 'ios' && presentationMode === 'actionSheet') && renderDropdown()} {(errorMessage || helperText) && ( {errorMessage || helperText} )} ); }); Select.displayName = 'Select'; export default Select;