import * as React from 'react'; import styles from './SearchableSelect.module.scss'; import { Icon } from './Icon'; export interface ISearchableOption { value: string; label: string; /** Второстепенная строка под названием — например, компания у отдела. */ sublabel?: string; } export interface ISearchableSelectProps { options: ISearchableOption[]; value: string; onChange: (value: string) => void; placeholder?: string; searchPlaceholder?: string; disabled?: boolean; loading?: boolean; /** Показывать пункт «очистить» — для необязательных полей. */ allowClear?: boolean; clearLabel?: string; } export const SearchableSelect: React.FC = ({ options, value, onChange, placeholder = 'Выберите значение', searchPlaceholder = 'Поиск...', disabled, loading, allowClear, clearLabel = 'Не выбрано' }) => { const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(''); const rootRef = React.useRef(null); const searchRef = React.useRef(null); const selected = options.filter(o => o.value === value)[0]; // Поиск на клиенте: справочник целиком уже загружен, отделов ~700 — // фильтровать в памяти дешевле, чем ходить на сервер на каждый символ. const filtered = React.useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return options; return options.filter(o => o.label.toLowerCase().indexOf(q) !== -1 || (o.sublabel ? o.sublabel.toLowerCase().indexOf(q) !== -1 : false) ); }, [options, query]); React.useEffect(() => { if (!open) return; const onDocumentMouseDown = (e: MouseEvent): void => { if (rootRef.current && !rootRef.current.contains(e.target as Node)) { setOpen(false); } }; const onKeyDown = (e: KeyboardEvent): void => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', onDocumentMouseDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('mousedown', onDocumentMouseDown); document.removeEventListener('keydown', onKeyDown); }; }, [open]); React.useEffect(() => { if (open && searchRef.current) searchRef.current.focus(); }, [open]); const toggle = (): void => { if (disabled) return; setQuery(''); setOpen(!open); }; const pick = (next: string): void => { onChange(next); setOpen(false); }; return (
{open && (
setQuery(e.target.value)} />
{filtered.length === 0 &&
Ничего не найдено
} {filtered.map(opt => ( ))}
{allowClear && value && (
)}
)}
); };