import { useEffect, useRef, useState } from 'react'; import { Search } from 'lucide-react'; import './SearchSelect.css'; export type SearchSelectOption = { value: string; label: string; }; /** `search` leads with a magnifier; `plain` is a bare field for a picker that also shows what is chosen. */ export type SearchSelectVariant = 'search' | 'plain'; type SearchSelectProps = { value: string; options: SearchSelectOption[]; placeholder: string; ariaLabel: string; noResultsText: string; onValueChange: (value: string) => void; onSelect: (option: SearchSelectOption) => void; variant?: SearchSelectVariant; /** The current choice, shown in the empty field as a value rather than as a hint. */ selectedLabel?: string; /** List every option before anything is typed, for a picker whose options are the whole choice. */ listOnFocus?: boolean; }; const SearchSelect = ({ value, options, placeholder, ariaLabel, noResultsText, onValueChange, onSelect, variant = 'search', selectedLabel, listOnFocus = false, }: SearchSelectProps) => { const [isOpen, setIsOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const listboxId = useRef(`ahr-search-select-${Math.random().toString(36).slice(2)}`); const hasQuery = value.trim().length > 0; const mayList = hasQuery || listOnFocus; const showResults = isOpen && mayList; const hasSelection = Boolean(selectedLabel) && !hasQuery; const shownPlaceholder = selectedLabel || placeholder; const className = [ 'ahr-search-select', variant === 'plain' ? 'ahr-search-select--plain' : '', hasSelection ? 'ahr-search-select--has-selection' : '', ].filter(Boolean).join(' '); useEffect(() => { setActiveIndex(-1); }, [value, options.length]); const selectOption = (option: SearchSelectOption) => { onSelect(option); setIsOpen(false); setActiveIndex(-1); }; return (
{ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { setIsOpen(false); setActiveIndex(-1); } }} > {variant === 'search' && (
); }; export default SearchSelect;