import { useEffect, useId, useRef, useState } from 'react'; const SEARCH_DEBOUNCE_MS = 320; export function AsyncCombobox({ allowClear = true, ariaLabel = 'Поисковый список', emptyLabel = 'Ничего не найдено', load, onSelect, optionDescription = () => '', optionLabel = item => item.name, optionValue = item => item.id, placeholder = 'Начните вводить название', selectedLabel = '', selectedValue = '' }) { const rootRef = useRef(null); const inputRef = useRef(null); const debounceTimer = useRef(null); const requestNumber = useRef(0); const restoringFocus = useRef(false); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const listId = useId(); useEffect(() => () => { window.clearTimeout(debounceTimer.current); requestNumber.current += 1; }, []); useEffect(() => { if (!open) return undefined; const handlePointerDown = event => { if (rootRef.current && !rootRef.current.contains(event.target)) { close(); } }; document.addEventListener('pointerdown', handlePointerDown); return () => document.removeEventListener('pointerdown', handlePointerDown); }); async function search(nextQuery) { const currentRequest = requestNumber.current + 1; requestNumber.current = currentRequest; setOpen(true); setLoading(true); setError(''); try { const result = await load(String(nextQuery || '').trim(), selectedValue || ''); if (requestNumber.current !== currentRequest) return; setItems(Array.isArray(result) ? result : []); setLoading(false); } catch { if (requestNumber.current !== currentRequest) return; setError('Не удалось выполнить поиск'); setLoading(false); } } function close() { window.clearTimeout(debounceTimer.current); requestNumber.current += 1; setOpen(false); setQuery(''); setItems([]); setError(''); setLoading(false); } function choose(item) { onSelect(item); close(); restoreInputFocus(); } function restoreInputFocus() { restoringFocus.current = true; inputRef.current?.focus(); restoringFocus.current = false; } function clear() { onSelect(null); setQuery(''); restoreInputFocus(); void search(''); } function handleFocus() { if (restoringFocus.current || open) return; setQuery(selectedLabel); void search(selectedLabel); } function handleInput(event) { const value = event.target.value; setQuery(value); // Старый ответ не должен стать доступен во время задержки нового поиска. requestNumber.current += 1; setOpen(true); setItems([]); setLoading(true); setError(''); window.clearTimeout(debounceTimer.current); debounceTimer.current = window.setTimeout(() => { void search(value); }, SEARCH_DEBOUNCE_MS); } function handleInputKeydown(event) { if (event.key === 'Escape') { event.preventDefault(); close(); } else if (event.key === 'ArrowDown') { event.preventDefault(); rootRef.current?.querySelector('[role="option"]')?.focus(); } } function handleMenuKeydown(event) { const options = Array.from(rootRef.current?.querySelectorAll('[role="option"]') || []); const current = options.indexOf(document.activeElement); if (event.key === 'Escape') { event.preventDefault(); close(); restoreInputFocus(); } else if (event.key === 'ArrowDown' && current >= 0) { event.preventDefault(); options[Math.min(current + 1, options.length - 1)]?.focus(); } else if (event.key === 'ArrowUp' && current >= 0) { event.preventDefault(); (current === 0 ? inputRef.current : options[current - 1])?.focus(); } } return (