import React, { useState, useRef, useEffect, useLayoutEffect, useId, useCallback, type ReactNode, type CSSProperties, type KeyboardEvent, } from 'react'; import './select.css'; export interface SelectOption { value: T; label: string; lead?: ReactNode; code?: string; meta?: ReactNode; group?: string; disabled?: boolean | string; } export interface SelectProps { id?: string; name?: string; className?: string; style?: CSSProperties; options: SelectOption[]; value?: T | T[]; defaultValue?: T | T[]; multiple?: boolean; onChange?: (value: any) => void; placeholder?: string; prefix?: string; /** Trigger text when a multiple select has every option chosen, e.g. "All regions". */ allLabel?: string; disabled?: boolean; size?: 'sm' | 'md'; required?: boolean; 'aria-label'?: string; 'aria-labelledby'?: string; 'aria-describedby'?: string; lead?: ReactNode; autoFocus?: boolean; } export function Select({ id, name, className = '', style, options, value, defaultValue, multiple = false, onChange, placeholder, prefix, allLabel, disabled = false, size = 'md', required = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, lead: triggerLeadOverride, autoFocus = false, }: SelectProps) { const generatedId = useId(); const selectId = id || generatedId; const listboxId = `${selectId}-listbox`; const [internalValue, setInternalValue] = useState(() => { if (value !== undefined) return value; if (defaultValue !== undefined) return defaultValue; return multiple ? [] : ('' as T); }); const currentValue = value !== undefined ? value : internalValue; const [isOpen, setIsOpen] = useState(false); const [isFlipped, setIsFlipped] = useState(false); const [menuMaxHeight, setMenuMaxHeight] = useState(340); const [searchQuery, setSearchQuery] = useState(''); const [highlightedIndex, setHighlightedIndex] = useState(-1); // True while focus sits on the trigger because of a pointer; hides the keyboard ring. const [pointerFocus, setPointerFocus] = useState(false); // Ids of external