import { useState, useRef, useEffect } from 'react'; import { ChevronDown } from 'lucide-react'; import './Select.css'; export interface SelectOption { value: string; label: string; group?: string; } interface SelectProps { id?: string; value: string; options: SelectOption[]; onChange: (value: string) => void; placeholder?: string; 'aria-label'?: string; } const Select = ({ id, value, options, onChange, placeholder, 'aria-label': ariaLabel }: SelectProps) => { const [open, setOpen] = useState(false); const [focusIdx, setFocusIdx] = useState(-1); const containerRef = useRef(null); const listRef = useRef(null); const selected = options.find(o => o.value === value); const groups = options.reduce<{ group: string; items: SelectOption[] }[]>((acc, opt) => { const g = opt.group || ''; const existing = acc.find(a => a.group === g); if (existing) existing.items.push(opt); else acc.push({ group: g, items: [opt] }); return acc; }, []); useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); useEffect(() => { if (open && focusIdx >= 0 && listRef.current) { const item = listRef.current.children[focusIdx] as HTMLElement | undefined; item?.scrollIntoView({ block: 'nearest' }); } }, [focusIdx, open]); const flatOptions = options; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { setOpen(false); return; } if (!open && (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown')) { e.preventDefault(); setOpen(true); const currentIdx = flatOptions.findIndex(o => o.value === value); setFocusIdx(currentIdx >= 0 ? currentIdx : 0); return; } if (!open) return; if (e.key === 'ArrowDown') { e.preventDefault(); setFocusIdx(prev => Math.min(prev + 1, flatOptions.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setFocusIdx(prev => Math.max(prev - 1, 0)); } else if (e.key === 'Home') { e.preventDefault(); setFocusIdx(0); } else if (e.key === 'End') { e.preventDefault(); setFocusIdx(flatOptions.length - 1); } else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (focusIdx >= 0 && focusIdx < flatOptions.length) { onChange(flatOptions[focusIdx].value); setOpen(false); } } }; const handleToggle = () => { setOpen(prev => !prev); if (!open) { const currentIdx = flatOptions.findIndex(o => o.value === value); setFocusIdx(currentIdx >= 0 ? currentIdx : 0); } }; const handleSelect = (opt: SelectOption) => { onChange(opt.value); setOpen(false); }; let itemIndex = 0; return (
{open && (
    {groups.map((g) => { const elems = []; if (g.group) { elems.push(
  • {g.group}
  • ); } for (const opt of g.items) { const idx = itemIndex++; const isSelected = opt.value === value; const isFocused = idx === focusIdx; elems.push(
  • setFocusIdx(idx)} onMouseDown={(e) => { e.preventDefault(); handleSelect(opt); }} > {opt.label}
  • ); } return elems; })}
)}
); }; export default Select;