Files
uopi/web/src/SearchableSelect.tsx
T

59 lines
1.9 KiB
TypeScript

import { h } from 'preact';
import { useState, useMemo } from 'preact/hooks';
// A dropdown with an inline search box. Extracted from SyntheticWizard so the
// same searchable picker can be reused (e.g. the logic editor's signal field).
export default function SearchableSelect({
value,
options,
onSelect,
placeholder = 'Select…',
}: {
value: string;
options: string[];
onSelect: (val: string) => void;
placeholder?: string;
}) {
const [open, setOpen] = useState(false);
const [filter, setFilter] = useState('');
const filtered = useMemo(() => {
const f = filter.toLowerCase();
return options.filter(o => o.toLowerCase().includes(f));
}, [options, filter]);
return (
<div class="search-select">
<div class="search-select-trigger" onClick={() => setOpen(!open)}>
{value || <span class="search-select-placeholder">{placeholder}</span>}
<span class="search-select-arrow">{open ? '▴' : '▾'}</span>
</div>
{open && (
<div class="search-select-dropdown">
<input
class="prop-input"
autoFocus
placeholder="Search…"
value={filter}
onInput={(e) => setFilter((e.target as HTMLInputElement).value)}
onClick={(e) => e.stopPropagation()}
/>
<div class="search-select-list">
{filtered.length === 0 && <div class="search-select-item hint">No matches</div>}
{filtered.map(opt => (
<div
key={opt}
class={`search-select-item${opt === value ? ' search-select-item-selected' : ''}`}
onClick={() => { onSelect(opt); setOpen(false); setFilter(''); }}
>
{opt}
</div>
))}
</div>
</div>
)}
{open && <div class="search-select-backdrop" onClick={() => setOpen(false)} />}
</div>
);
}