import { useEffect, useMemo, useRef, useState } from 'react'; export type SearchSelectOption = { value: string; label: string; /** Searchable but not displayed — e.g. aliases, domains. */ keywords?: string; /** Whether this option is a static/special entry (e.g. "none", "don't redirect"). */ fixed?: boolean; }; type Props = { options: SearchSelectOption[]; value: string; onChange: (value: string) => void; placeholder?: string; id?: string; }; export default function SearchSelect({ options, value, onChange, placeholder = 'Select…', id, }: Props) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const containerRef = useRef(null); const inputRef = useRef(null); const selected = options.find(o => o.value === value); useEffect(() => { if (!open) return; function onDocClick(e: MouseEvent) { if (!containerRef.current?.contains(e.target as Node)) setOpen(false); } function onKey(e: KeyboardEvent) { if (e.key === 'Escape') setOpen(false); } document.addEventListener('mousedown', onDocClick); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDocClick); document.removeEventListener('keydown', onKey); }; }, [open]); useEffect(() => { if (open) { setQuery(''); requestAnimationFrame(() => inputRef.current?.focus()); } }, [open]); const q = query.trim().toLowerCase(); const { fixed, filterable } = useMemo(() => { const f: SearchSelectOption[] = []; const r: SearchSelectOption[] = []; for (const o of options) { if (o.fixed) f.push(o); else r.push(o); } return { fixed: f, filterable: r }; }, [options]); const filtered = useMemo( () => q ? filterable.filter( o => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q) || o.keywords?.toLowerCase().includes(q) ) : filterable, [filterable, q] ); function pick(val: string) { onChange(val); setOpen(false); } return (
{open && (
setQuery(e.target.value)} />
{fixed.map(o => ( ))} {fixed.length > 0 && filtered.length > 0 && (
)} {filtered.length === 0 ? (
No matches.
) : ( filtered.map(o => ( )) )}
)}
); } function ChevronDown() { return ( ); }