import { useEffect, useId, useRef, useState } from "react"; import "./ComboBox.css"; interface ComboBoxOption { label: string; value: string; } interface ComboBoxProps { options?: ComboBoxOption[]; value?: string; onChange?: (value: string) => void; onSelect?: (option: ComboBoxOption) => void; placeholder?: string; disabled?: boolean; } const ComboBox = ({ options = [], value, onChange, onSelect, placeholder = "Search...", disabled = false, }: ComboBoxProps) => { const [inputValue, setInputValue] = useState(value ?? ""); const [isOpen, setIsOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const containerRef = useRef(null); const listboxId = useId(); const filtered = options.filter((opt) => opt.label.toLowerCase().includes(inputValue.toLowerCase()), ); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { setIsOpen(false); setActiveIndex(-1); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const handleInputChange = (e: React.ChangeEvent) => { const val = e.target.value; setInputValue(val); setIsOpen(true); setActiveIndex(-1); onChange?.(val); }; const handleSelect = (option: ComboBoxOption) => { setInputValue(option.label); setIsOpen(false); setActiveIndex(-1); onSelect?.(option); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (!isOpen) { if (e.key === "ArrowDown" || e.key === "ArrowUp") { setIsOpen(true); setActiveIndex(0); e.preventDefault(); } return; } if (e.key === "ArrowDown") { setActiveIndex((i) => Math.min(i + 1, filtered.length - 1)); e.preventDefault(); } else if (e.key === "ArrowUp") { setActiveIndex((i) => Math.max(i - 1, 0)); e.preventDefault(); } else if (e.key === "Enter" && activeIndex >= 0) { handleSelect(filtered[activeIndex]); e.preventDefault(); } else if (e.key === "Escape") { setIsOpen(false); setActiveIndex(-1); } }; return (
setIsOpen(true)} onKeyDown={handleKeyDown} placeholder={placeholder} disabled={disabled} role="combobox" aria-expanded={isOpen} aria-autocomplete="list" aria-controls={listboxId} aria-activedescendant={ activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined } /> {isOpen && (
{filtered.length > 0 ? ( filtered.map((option, index) => (
handleSelect(option)} > {option.label}
)) ) : (
No results found
)}
)}
); }; export default ComboBox;