GrAutocomplete
Берут, когда вариантов слишком много для списка.
Обзор
Когда брать
- вариантов слишком много для списка — пользователи, города, теги, репозитории: их ищут вводом, а не просматривают;
- список приходит с сервера —
sourceиdebounceвместе с состояниями «идёт поиск» и «ничего не найдено»; - нужен минимум символов —
minQueryLengthне даёт уйти на сервер за первой же буквой; - значений несколько —
multipleпоказывает выбранное чипами, каждый снимается с клавиатуры; - значения может не быть в справочнике —
allowCustomValueразрешает своё.
Когда взять другое
| Нужно | Берите |
|---|---|
| Вариантов 5–50 и их просматривают, а не ищут | GrSelect |
| Варианты — дерево с уровнями | GrTreeSelect |
| Ищут команду приложения, а не значение поля | GrCommandPalette |
| Справочника нет вовсе, вводят свои строки | GrInputTag |
| Вариантов 2–5 и они видны сразу | GrSegmented |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrAutocomplete
:options="options"
v-model="value"
placeholder="Find a customer"
/>Примеры 5 у каждого — живое демо и его код
Находится по тому, что помнишь
Опции приходят группами — сначала недавние клиенты — в той же форме, что у
GrSelect. Свойfilterищет по имени, почте или номеру НДС, а слот#optionпоказывает то, что отличает двух похожих клиентов.#prefixдержит значок поиска,clearableочищает поле.New invoice Name, email or VAT number
Northwind TradersHamburg · DE 812 345 678 · [email protected]Спрашиваем сервер, но не на каждой клавише
fetch-optionsберёт обмен с сервером на себя: ждётdebounceмс после последней клавиши, не спрашивает раньшеmin-query-lengthбукв и отменяет устаревший запрос черезAbortSignal. Слоты#loadingи#emptyдают ожиданию и пустому ответу свои слова, и диктор их слышит.Request a reviewQ3 VAT return · due 10 OctoberНесколько меток и одна, которой ещё нет
multipleпоказывает выбор чипами —tag-toneиtag-radiusпридают им вид меток, — а выключенныйclose-on-selectдержит панель открытой, пока выбирают.allow-custom-valueдобавляет «Add …» для новой метки. Сnameскрытые поля отправляют значения, а не набранный текст.Пять тысяч товаров, в DOM — окно
Каталог, который никто не листает и который не должен целиком жить в DOM:
virtualдержит только окно строк вокруг вьюпорта, аdropdown-max-heightзадаёт это окно.filter, берущий каждое слово отдельно, находит «walnut desk», а слот#optionставит цену справа в каждой строке.Add an order line5,000 products in the catalogue
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
modelValueобязательныйv-model | GrAutocompleteModelValue<TValue> | — | Выбранное значение (single — строка, multiple — массив строк). |
options | GrAutocompleteOptionOrGroup<TValue>[] | undefined | undefined | Доступные опции. Для локального режима — полный список (фильтруется на клиенте). Для remote-режима (`filterable=false`) — список, который родитель обновляет в ответ на событие `search`. |
multiple | boolean | undefined | false | Выбор нескольких значений: модель — массив, выбранное стоит в поле чипами. По умолчанию `false`. |
tagTone |
| "neutral" | Вид чипов выбранных значений в режиме `multiple`. Рисует их `GrChip`, но шкала пропа осталась бейджевой: `tagSize` публичен, и его значения обязаны означать тот же кегль, что и раньше. Перевод ступеней — в `chipSizeForBadgeScale`. |
tagDark | boolean | undefined | false | Чипы выбранного залиты тоном, а не стоят на мягкой подложке — для пёстрого фона. |
tagSize | "xs" | "sm" | "md" | "lg" | undefined | "sm" | Кегль чипов по бейджевой шкале. По умолчанию `sm`: чип живёт внутри поля, а не в тексте. |
tagRadius | GrBadgeRadius | undefined | "round" | Форма чипов: `round` — таблетка, `semi` — ярлык, `square` — штамп. По умолчанию `round`. |
disabled | boolean | undefined | false | Поле недоступно: панель не открывается, опции не выбираются, чипы не снимаются. Наследуется от `GrFormField`. |
readonly | boolean | undefined | false | Только для чтения: значение видно и уходит в форму, но не редактируется. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Высота и кегль поля по шкале контролов. По умолчанию `md`; берётся из `GrConfigProvider`. |
placeholder | string | undefined | undefined | Подсказка в пустом поле. В `multiple` с выбранными значениями прячется: место занимают чипы. |
ariaLabel | string | undefined | undefined | Доступное имя поля, когда видимой подписи нет. Внутри `GrFormField` имя даёт его `label`. |
clearable | boolean | undefined | undefined | Кнопка очистки выбранного значения/запроса. |
loading | boolean | undefined | false | Внешне управляемое состояние загрузки (для async-сценариев). |
filterable | boolean | undefined | true | Локальная фильтрация опций по введённому запросу. Отключите (`false`) для чисто удалённого поиска — тогда `options` показываются как есть, а фильтрацию выполняет сервер по событию `search`. |
filter | ((option: GrAutocompleteOption<TValue>, query: string) => boolean) | undefined | undefined | Кастомный матчер локальной фильтрации. По умолчанию — подстрока в `label`/`value`. |
fetchOptions |
| undefined | Удалённая загрузка опций под управлением компонента: дебаунс, отмена устаревшего запроса и `loading` берёт на себя он. Ответ на отменённый запрос игнорируется — при быстром вводе в списке всегда результат последнего запроса, а не того, который вернулся позже. `signal` пробрасывается в `fetch`. Локальная фильтрация в этом режиме выключена: список фильтрует сервер. Альтернатива — событие `search`, если запрос ведёт само приложение. |
minQueryLength | number | undefined | 0 | Минимальная длина запроса до эмита `search` (для дебаунса remote-загрузки). |
debounce | number | undefined | 250 | Задержка дебаунса события `search`, мс. |
allowCustomValue | boolean | undefined | false | Разрешить ввод/коммит значения, которого нет в `options`. |
closeOnSelect | boolean | undefined | true | Закрывать панель после выбора (single всегда закрывает). |
dropdownMaxHeight | number | undefined | 280 | Максимальная высота панели, px. |
virtual | boolean | undefined | false | Виртуализация панели: в DOM живёт только окно вокруг вьюпорта. Высоту окна задаёт `dropdownMaxHeight`. Включается осознанно: на сотне опций выигрыша нет, а в разметке остаётся только окно — вместе с ним меняется и то, что находит `querySelector` потребителя. Профильный сценарий — удалённый поиск по справочнику на тысячи позиций. |
loadingText | string | undefined | undefined | i18n-тексты состояний панели / aria. |
noResultsText | string | undefined | undefined | Текст панели, когда по запросу ничего не нашлось. i18n: fallback `gr.autocomplete.noResults`. |
clearLabel | string | undefined | undefined | Имя кнопки очистки (`aria-label`). i18n: fallback `gr.common.clear`. |
openv-model | boolean | undefined | undefined | Контролируемое состояние панели (`v-model:open`). Без пропа панель ведёт себя сама (uncontrolled), с ним — слушайте `update:open` и меняйте проп. |
name | string | undefined | undefined | Имя для нативной формы: hidden input по значению модели (не по тексту запроса). |
prefixMinWidth | string | undefined | undefined | Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета (`docs/form-controls.md`). |
prefixMaxWidth | string | undefined | undefined | Верхняя граница ширины аддона `prefix`: содержимое шире обрезается. |
suffixMinWidth | string | undefined | undefined | Нижняя граница ширины аддона `suffix`. По умолчанию — ширина аддона для ступени размера. |
suffixMaxWidth | string | undefined | undefined | Верхняя граница ширины аддона `suffix`: содержимое шире обрезается. |
prefixFixed | boolean | undefined | false | Жёсткая ширина аддона `prefix` (`prefixMaxWidth` → `prefixMinWidth` → дефолт) — когда аддоны соседних полей обязаны совпасть. |
suffixFixed | boolean | undefined | false | Жёсткая ширина аддона `suffix` (`suffixMaxWidth` → `suffixMinWidth` → дефолт) — когда аддоны соседних полей обязаны совпасть. |
Слоты
| Slot | Type | Описание |
|---|---|---|
prefix | any | Аддон слева от поля ввода. |
suffix | any | Аддон справа, перед спиннером и крестиком. |
option | { option: GrAutocompleteOption<TValue>; selected: boolean; } | Строка списка вместо подписи опции. |
loading | any | Содержимое панели, пока едут опции. |
empty | any | Содержимое панели, когда подходящих опций нет. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (e: "update:modelValue", value: GrAutocompleteModelValue<TValue>): void | Новое значение (`v-model`): строка или, в `multiple`, массив. |
search | (e: "search", query: string): void | Дебаунснутый поисковый запрос — точка входа для удалённой загрузки опций. |
searchError | (e: "searchError", error: unknown): void | Запрос `fetchOptions` завершился ошибкой (отмена устаревшего сюда не приходит). |
change | (e: "change", value: GrAutocompleteModelValue<TValue>): void | Значение зафиксировано выбором или снятием опции. |
update:open | (e: "update:open", value: boolean): void | Панель открылась/закрылась (`v-model:open`). |
clear | (e: "clear"): void | Значение снято кнопкой очистки; только при `clearable`. |
focus | (e: "focus", event: FocusEvent): void | Фокус вошёл в контрол. Переход между полем и его панелью событием не считается. |
blur | (e: "blur", event: FocusEvent): void | Фокус ушёл из контрола — из поля и из панели вместе. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrAutocompleteConfigurableProps, GrAutocompleteSize, GrAutocompleteOption } from '@feugene/granularity/components/GrAutocomplete'Файлdist/types/components/GrAutocomplete/config.d.ts
const grAutocompleteConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrAutocomplete">;Файлdist/types/components/GrAutocomplete/defaults.d.ts
Пропы GrAutocomplete, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrAutocomplete: { … } }">.
Только оформление и поведение по умолчанию — см. GrButton/defaults.ts.
interface GrAutocompleteConfigurableProps {
size: GrAutocompleteSize;
clearable: boolean;
}Файлdist/types/components/GrAutocomplete/grAutocompleteStyles.d.ts
type GrAutocompleteSize = GrComponentSize;Опция автокомплита. Метка — строка, значение параметризуется.
type GrAutocompleteOption<TValue extends GrAutocompleteValue = string> = {
value: TValue;
label: string;
disabled?: boolean;
};Группа опций: заголовок label + вложенные опции options.
Форма та же, что у GrSelect: два соседних контрола, у которых списки
различаются только способом ввода, обязаны принимать данные одинаково.
type GrAutocompleteOptionGroup<TValue extends GrAutocompleteValue = string> = {
label: string;
options: GrAutocompleteOption<TValue>[];
};Элемент списка: либо одиночная опция, либо группа.
type GrAutocompleteOptionOrGroup<TValue extends GrAutocompleteValue = string> = GrAutocompleteOption<TValue> | GrAutocompleteOptionGroup<TValue>;type GrAutocompleteModelValue<TValue extends GrAutocompleteValue = string> = TValue | TValue[];Файлdist/types/components/GrAutocomplete/GrAutocomplete.vue.d.ts
Публичный GR-примитив «Autocomplete / Combobox» (WAI-ARIA editable combobox).
В отличие от GrSelect (select-only combobox с кнопкой-триггером), здесь
combobox-ом является сам текстовый <input>: набранный текст — это поисковый
запрос, а выбор опции заполняет поле. Поддерживает локальную фильтрацию,
удалённую (async) загрузку через событие search + внешний проп loading,
произвольные значения (allowCustomValue) и multiple с удаляемыми chips.
interface GrAutocompleteProps<TValue extends GrAutocompleteValue = string> {
/** Выбранное значение (single — строка, multiple — массив строк). */
modelValue: GrAutocompleteModelValue<TValue>;
/**
* Доступные опции. Для локального режима — полный список (фильтруется на клиенте).
* Для remote-режима (`filterable=false`) — список, который родитель обновляет
* в ответ на событие `search`.
*/
options?: GrAutocompleteOptionOrGroup<TValue>[];
/** Выбор нескольких значений: модель — массив, выбранное стоит в поле чипами. По умолчанию `false`. */
multiple?: boolean;
/**
* Вид чипов выбранных значений в режиме `multiple`. Рисует их `GrChip`, но
* шкала пропа осталась бейджевой: `tagSize` публичен, и его значения обязаны
* означать тот же кегль, что и раньше. Перевод ступеней — в
* `chipSizeForBadgeScale`.
*/
tagTone?: GrBadgeTone;
/** Чипы выбранного залиты тоном, а не стоят на мягкой подложке — для пёстрого фона. */
tagDark?: boolean;
/** Кегль чипов по бейджевой шкале. По умолчанию `sm`: чип живёт внутри поля, а не в тексте. */
tagSize?: GrBadgeSize;
/** Форма чипов: `round` — таблетка, `semi` — ярлык, `square` — штамп. По умолчанию `round`. */
tagRadius?: GrBadgeRadius;
/**
* Поле недоступно: панель не открывается, опции не выбираются, чипы не
* снимаются. Наследуется от `GrFormField`.
*/
disabled?: boolean;
/** Только для чтения: значение видно и уходит в форму, но не редактируется. */
readonly?: boolean;
/** Визуальное и ARIA-состояние ошибки. */
invalid?: boolean;
/** Обязательное поле (`aria-required`). */
required?: boolean;
/** Высота и кегль поля по шкале контролов. По умолчанию `md`; берётся из `GrConfigProvider`. */
size?: GrAutocompleteSize;
/** Подсказка в пустом поле. В `multiple` с выбранными значениями прячется: место занимают чипы. */
placeholder?: string;
/** Доступное имя поля, когда видимой подписи нет. Внутри `GrFormField` имя даёт его `label`. */
ariaLabel?: string;
/** Кнопка очистки выбранного значения/запроса. */
clearable?: boolean;
/** Внешне управляемое состояние загрузки (для async-сценариев). */
loading?: boolean;
/**
* Локальная фильтрация опций по введённому запросу. Отключите (`false`) для
* чисто удалённого поиска — тогда `options` показываются как есть, а фильтрацию
* выполняет сервер по событию `search`.
*/
filterable?: boolean;
/** Кастомный матчер локальной фильтрации. По умолчанию — подстрока в `label`/`value`. */
filter?: (option: GrAutocompleteOption<TValue>, query: string) => boolean;interface GrAutocompleteEmits<TValue extends GrAutocompleteValue = string> {
/** Новое значение (`v-model`): строка или, в `multiple`, массив. */
(e: 'update:modelValue', value: GrAutocompleteModelValue<TValue>): void;
/** Дебаунснутый поисковый запрос — точка входа для удалённой загрузки опций. */
(e: 'search', query: string): void;
/** Запрос `fetchOptions` завершился ошибкой (отмена устаревшего сюда не приходит). */
(e: 'searchError', error: unknown): void;
/** Значение зафиксировано выбором или снятием опции. */
(e: 'change', value: GrAutocompleteModelValue<TValue>): void;
/** Панель открылась/закрылась (`v-model:open`). */
(e: 'update:open', value: boolean): void;
/** Значение снято кнопкой очистки; только при `clearable`. */
(e: 'clear'): void;
/** Фокус вошёл в контрол. Переход между полем и его панелью событием не считается. */
(e: 'focus', event: FocusEvent): void;
/** Фокус ушёл из контрола — из поля и из панели вместе. */
(e: 'blur', event: FocusEvent): void;
}Файлdist/types/components/GrAutocomplete/safelist.d.ts
const grAutocompleteSafelist: string[];Файлdist/types/components/GrAutocomplete/index.d.ts
type GrAutocompleteInstance = ComponentExposed<typeof GrAutocompleteComponent>;Доступность
Паттерн APG
combobox (editable)
Клавиши
←Delete / BackspaceEsc
| Клавиша | Действие |
|---|---|
| — | то же (при allowCustomValue вариант «Add …» — такая же остановка стрелок, как опция), плюс в multiple: Backspace в пустом поле — удалить последний чип |
| ← | уйти в чипы, там ←/→/Home/End — между ними |
| Delete / Backspace | удалить |
| Esc | вернуться в поле |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Группы опций
<GrAutocomplete
v-model="city"
:options="[
{ label: 'Европа', options: [{ value: 'lis', label: 'Лиссабон' }] },
{ label: 'Азия', options: [{ value: 'tyo', label: 'Токио' }] },
{ value: 'remote', label: 'Удалённо' },
]"
/>Форма данных та же, что у GrSelect: два соседних контрола, у
которых списки различаются только способом ввода, обязаны принимать данные
одинаково. Группы и одиночные опции смешиваются в одном массиве.
Фильтр прячет группу целиком, если в ней ничего не совпало. Заголовок над пустотой читается как сбой, а не как «здесь ничего нет».
Клавиатура ходит только по опциям. Заголовок группы стрелками не выбирается
и в набор для диктора не входит: прямыми потомками role="listbox" обязаны быть
опции, поэтому заголовок лежит внутри своей группы и даёт ей имя через
aria-labelledby. При виртуализации окно может начаться ниже заголовка — тогда
имя группы уходит в aria-label, и группа всё равно остаётся названной.
Одинаковое значение в разных группах — не ошибка: ключ строки собирается с индексом группы, поэтому две такие опции не схлопываются в один узел, а наведение мышью подсвечивает ту, на которую навели.
Группы приходят и от сервера: fetchOptions вправе вернуть тот же массив с
группами, и подпись состава считается по их содержимому — смена опции внутри
группы не проходит незамеченной.
Удалённый поиск
Два пути, взаимоисключающих по смыслу.
fetchOptions — запрос ведёт компонент:
<GrAutocomplete
v-model="user"
:fetch-options="fetchPeople"
:min-query-length="1"
@search-error="reportError"
/>async function fetchPeople(query: string, signal: AbortSignal) {
const response = await fetch(`/api/people?q=${query}`, { signal })
return response.json()
}Дебаунс, отмена предыдущего запроса и loading — на компоненте. Ответ на
отменённый запрос игнорируется: при быстром вводе в списке всегда результат
последнего запроса, а не того, который вернулся позже. Локальная фильтрация в
этом режиме выключена — список фильтрует сервер. Ошибка (кроме отмены) уходит
событием searchError, список при этом становится пустым.
search — запрос ведёт приложение:
<GrAutocomplete
v-model="user"
:options="options"
:loading="pending"
:filterable="false"
@search="load"
/>Путь для тех, у кого запрос живёт в сторе, кэше или своей отмене. Гонку ответов в этом случае разруливает приложение: компонент отдаёт только дебаунснутый запрос и не знает, какой ответ ему принесли.
Произвольные значения
allowCustomValue добавляет в список опцию «Add …». Она такая же опция
списка, как остальные, и участвует в навигации стрелками: без этого закоммитить
своё значение с клавиатуры было нельзя — при непустом списке Enter всегда
уходил в активную опцию.
Значение набирается текстом, поэтому оно строковое по природе: при числовом
TValue ветка неприменима.
Чипы (multiple)
Крестики чипов намеренно не табируемы: у combobox фокус живёт на <input>,
и двадцать выбранных значений не должны давать двадцать остановок Tab.
Навигация — стрелками: ← из пустого запроса переводит на последний чип,
←/→ ходят между ними, Delete/Backspace удаляет, Esc, → с последнего
и любой печатный символ возвращают в поле.
Backspace в пустом поле по-прежнему сносит последний чип без захода в
навигацию.
Состав панели
Прямые потомки role="listbox" — только опции: роль объявляет остальных
недопустимыми детьми. Загрузка, «введите ещё N символов» и «ничего не найдено»
живут ниже списка в одном живом регионе role="status" aria-live="polite" —
иначе асинхронные состояния менялись бы молча.
Опции не забирают фокус (mousedown подавлен), поэтому выбор мышью оставляет
каретку в поле — панель не переоткрывается и таб-порядок не сбивается.
Высота панели
dropdownMaxHeight — потолок списка, а не панели. Саму панель ограничивает
место, оставшееся до края вьюпорта: слой сообщает замер в
--gr-floating-available-height, и на коротком экране сжимается именно список —
поле ввода и строка статуса остаются на виду, а полоса прокрутки одна. Механизм общий на списочные
панели пакета и описан в ../overlays.md.
Виртуализация
virtual оставляет в DOM только окно вокруг вьюпорта — его высоту задаёт
dropdownMaxHeight или доступное место, если его меньше. Профильный сценарий один: удалённый поиск по справочнику,
где совпадений тысячи.
<GrAutocomplete v-model="city" :options="cities" virtual />Три следствия, о которых стоит знать заранее.
Размер набора объявляется явно. В обычном режиме диктор выводит его из DOM,
но при неполном наборе получил бы «1 из 12» на списке в десять тысяч. Поэтому
при virtual опции несут aria-setsize и aria-posinset — от всего
отфильтрованного списка, а не от окна. В обычном режиме этих атрибутов нет: там
они были бы шумом.
Строка «Add …» — элемент набора. При allowCustomValue она стоит первой и
прокручивается вместе со списком, а не прилипает к верху панели: клавиатура и
так ходит по ней как по обычной опции.
Активная опция всегда смонтирована. Стрелки прокручивают список до неё
прежде, чем перевести aria-activedescendant, — иначе атрибут указывал бы на
узел, которого в DOM нет. Устройство и ограничения примитива —
virtual-list.md.
Программное управление
const box = useTemplateRef('box')
box.value?.focus()
box.value?.open()
box.value?.close()Состояния
disabled и readonly приходят и пропом, и из GrFormField.
Оба запирают контрол одинаково: панель не открывается, опции не выбираются,
чипы не удаляются, кнопки очистки нет. Разница только в семантике — readonly
отдаёт значение в форму и объявляется aria-readonly.
Слоты
| Слот | Что заменяет |
|---|---|
prefix / suffix | аддон в оболочке поля |
option | содержимое опции (option, selected) |
loading | строку загрузки |
empty | текст «ничего не найдено» |
Аддоны prefix / suffix
Слоты кладут в оболочку иконку, единицу или метку; ширина ограничивается шестью
пропами (prefixMinWidth/prefixMaxWidth/prefixFixed и то же для суффикса).
Общий контракт контролов — form-controls.md.
Управление панелью и нативная форма
Панель управляема через v-model:open (общий контракт панельных оверлеев, как
у GrPopover): без пропа open — прежнее uncontrolled-поведение, с ним
состоянием владеет родитель, а update:open сопровождает каждое изменение.
Проп name включает участие в нативной форме: hidden-инпуты сериализуют
значение модели, а не текст запроса — по одному на значение при multiple,
ничего при пустом выборе.
minQueryLength и стартовый список в remote-режиме
Пока запрос короче minQueryLength, панель показывает подсказку «введите ещё
N символов», а не результаты прошлого запроса — устаревший список под
подсказкой дезинформировал бы. При allowCustomValue подсказка стоит рядом со
строкой «Add …»: та объясняет, что делать с набранным, но не то, куда делись
опции.
В remote-режиме (fetchOptions) проп options — стартовый список до первого
ответа сервера. Его замена родителем снова делает его источником до следующего
ответа и отменяет летящий запрос: тот относится к прежнему набору данных.
Смена отслеживается по составу, а не по идентичности массива, поэтому
инлайн-литерал :options="[...]" безопасен — пересозданный при ререндере
список с теми же опциями remote-результаты не трогает.
Пропы этого примера