/** * Select - Dropdown select component with optional search/filter. * * Renders a native {placeholder && ( )} {options.map((opt) => ( ))} {hasError && ( )} {helperText && !hasError && (

{helperText}

)} ); }); // --------------------------------------------------------------------------- // Searchable Select // --------------------------------------------------------------------------- function SearchableSelect({ id, name, label, options, value, onChange, placeholder = "Search...", disabled, required, error, className, }) { const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(""); const containerRef = useRef(null); const inputRef = useRef(null); const selectId = id || name; const hasError = Boolean(error); const selectedOption = options.find((o) => o.value === value); const filtered = options.filter((opt) => opt.label.toLowerCase().includes(query.toLowerCase()), ); const handleSelect = useCallback( (optionValue) => { if (onChange) { onChange({ target: { name, value: optionValue } }); } setIsOpen(false); setQuery(""); }, [onChange, name], ); // Close on outside click useEffect(() => { function handleClickOutside(event) { if ( containerRef.current && !containerRef.current.contains(event.target) ) { setIsOpen(false); setQuery(""); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Close on Escape const handleKeyDown = useCallback( (event) => { if (event.key === "Escape") { setIsOpen(false); setQuery(""); } }, [], ); const ringColor = hasError ? "border-danger-500 focus-within:ring-danger-500" : "border-neutral-300 focus-within:ring-primary-500"; return (
{label && ( )}
{isOpen && (
setQuery(e.target.value)} placeholder="Type to filter..." className="w-full rounded border border-neutral-200 px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-primary-500" aria-label="Filter options" />
    {filtered.length === 0 && (
  • No results found
  • )} {filtered.map((opt) => (
  • { if (!opt.disabled) { handleSelect(opt.value); } }} > {opt.label}
  • ))}
)}
{hasError && (

{error}

)}
); } // --------------------------------------------------------------------------- // Composite Select // --------------------------------------------------------------------------- const OPTION_SHAPE = PropTypes.shape({ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, label: PropTypes.string.isRequired, disabled: PropTypes.bool, }); const Select = forwardRef(function Select(props, ref) { if (props.searchable) { return ; } return ; }); Select.propTypes = { id: PropTypes.string, name: PropTypes.string, label: PropTypes.string, options: PropTypes.arrayOf(OPTION_SHAPE).isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onChange: PropTypes.func, placeholder: PropTypes.string, disabled: PropTypes.bool, required: PropTypes.bool, searchable: PropTypes.bool, error: PropTypes.string, helperText: PropTypes.string, className: PropTypes.string, }; Select.defaultProps = { searchable: false, className: "", }; export default Select;