diff --git a/apps/web/src/components/auto-complete-input.tsx b/apps/web/src/components/auto-complete-input.tsx index 1655ffc..c8e7d97 100644 --- a/apps/web/src/components/auto-complete-input.tsx +++ b/apps/web/src/components/auto-complete-input.tsx @@ -1,11 +1,12 @@ import * as React from 'react' -import { CheckIcon, SearchIcon } from 'lucide-react' +import { CheckIcon, ChevronsUpDownIcon, SearchIcon } from 'lucide-react' -import { Input } from '@cfdm/ui/components/input' +import { Button } from '@cfdm/ui/components/button' import { Command, CommandEmpty, CommandGroup, + CommandInput, CommandItem, CommandList, } from '@cfdm/ui/components/command' @@ -32,8 +33,10 @@ interface AutoCompleteInputProps { searchPlaceholder?: string emptyText?: string className?: string - /** Показывать ли выбранный leading в самом Input (например флаг). */ + /** Показывать ли выбранный leading в триггере (например флаг). */ showLeadingInInput?: boolean + /** Разрешать ли произвольный ввод (не только из списка). По умолчанию true. */ + allowFreeText?: boolean } export function AutoCompleteInput({ @@ -41,80 +44,99 @@ export function AutoCompleteInput({ value, onChange, options, - placeholder, - searchPlaceholder: _searchPlaceholder = 'Поиск…', + placeholder = 'Выбрать…', + searchPlaceholder = 'Поиск…', emptyText = 'Ничего не найдено', className, showLeadingInInput = true, + allowFreeText = true, }: AutoCompleteInputProps) { const [open, setOpen] = React.useState(false) + const [query, setQuery] = React.useState('') const filtered = React.useMemo(() => { - const q = value.trim().toLowerCase() + const q = query.trim().toLowerCase() if (!q) return options - return options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q)) - }, [options, value]) + return options.filter( + (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q), + ) + }, [options, query]) - const selected = options.find((o) => o.value.toLowerCase() === value.trim().toLowerCase()) - const leading = selected?.leading + const selected = options.find( + (o) => o.value.toLowerCase() === value.trim().toLowerCase(), + ) + const displayLabel = selected?.label ?? value + const leading = showLeadingInInput ? selected?.leading : undefined + + const handleSelect = (next: string) => { + onChange(next) + setQuery('') + setOpen(false) + } return ( - + { + setOpen(o) + if (!o) setQuery('') + }}> - {showLeadingInInput && leading ? ( - - {leading} + } /> - + -
- - - {value.trim() || 'Введите для поиска'} - -
+ {emptyText} + {allowFreeText && query.trim() && !filtered.some( + (o) => o.label.toLowerCase() === query.trim().toLowerCase(), + ) ? ( + handleSelect(query.trim())} + className="gap-2" + > + + + Использовать: {query.trim()} + + + ) : null} {filtered.map((opt) => { const isSelected = opt.value.toLowerCase() === value.trim().toLowerCase() return ( { - onChange(opt.value) - setOpen(false) - }} + onSelect={() => handleSelect(opt.value)} className="gap-2" > - {opt.leading ? {opt.leading} : null} + {opt.leading ? ( + {opt.leading} + ) : null} {opt.label} {isSelected ? : null}