From b87d7c6fad2b4b378cfe808b55b569e9416ec617 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 26 Jun 2026 23:03:20 +0700 Subject: [PATCH] =?UTF-8?q?fix(web):=20AutoCompleteInput=20=E2=80=94=20can?= =?UTF-8?q?onical=20shadcn=20Combobox,=20=D0=B1=D0=B5=D0=B7=20=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=83=D0=B7=D0=B5=D1=80=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D1=81=D0=BA=D0=B0=D0=B7=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - переделан по документации shadcn: Button trigger + Popover + Command с CommandInput внутри (вместо нативного Input в триггере) - CommandInput с autoComplete="off" / autoCorrect="off" / spellCheck=false — браузерные подсказки больше не появляются - поддержан произвольный ввод: пункт «Использовать: <запрос>» при отсутствии совпадений Co-authored-by: Cursor --- .../src/components/auto-complete-input.tsx | 120 +++++++++++------- 1 file changed, 71 insertions(+), 49 deletions(-) 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}