GrSelect
Берут, когда поле формы со списком значений.
Обзор
Когда брать
- поле формы со списком значений — 5–50 вариантов, которые просматривают, а не ищут: статус, категория, ответственный;
- мобильная форма —
optionsView="native"отдаёт выбор системному колесу, и на телефоне это лучше любой своей панели; - множественный выбор —
multipleсtags, когда выбранное должно быть видно целиком, иmaxTagCount, когда хвост пора сворачивать; - список на сотни строк —
virtualоставляет в DOM окно вокруг вьюпорта; - значение-объект —
valueKeyсравнивает по идентификатору, а не по ссылке: модель приходит снаружи отдельной копией и по===не совпала бы.
Когда взять другое
| Нужно | Берите |
|---|---|
| Пользователь ищет вводом, а не просматривает список | GrAutocomplete |
| Варианты — дерево с уровнями | GrTreeSelect |
| Выбирают команду приложения, а не значение поля | GrCommandPalette |
| Вариантов 2–5 и они должны быть видны сразу | GrSegmented / GrRadioGroup |
| Значений несколько, и все они видны списком | GrCheckboxGroup |
| Справочника нет, пользователь вводит свои строки | GrInputTag |
filterable фильтрует уже загруженный список — это удобство внутри выбора, а не
поиск. Как только запрос уходит на сервер и список строится по нему, задача
меняется: там роль combobox носит сам <input>, и это GrAutocomplete.
Варианты все тона и виды — настоящим компонентом
state \ variant | primary | default | muted | danger |
|---|---|---|---|---|
default | ||||
success | ||||
warning | ||||
danger |
Размеры
xssmmdlg
Состояния
обычнаяdisabledloadingreadonlyinvalid
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrSelect
:options="options"
v-model="value"
ariaLabel="Status"
/>Примеры 5 у каждого — живое демо и его код
Статус, исполнитель и метки задачи
Разметку принимает только панель, поэтому
options-view="panel"рисует точку статуса вprefixи в каждой строке, а статусы разложены по группам «Open» и «Closed». Исполнитель — объект: задача держит свою копию, и сопоставляет её со спискомvalue-key="id"—===не совпал бы. Метки приходят со своимtone, после третьей сворачиваются в «+1», аallow-custom-valueзаводит метку, которой у команды ещё нет.BILL-1839Seats added on the last day of the cycle are not proratedOpened by Mina Ortiz from ticket #4182 BugBillingCustomer report +1Фильтры, которые читаются фразой
view="link"превращает селект в слово фразы «Show all events by anyone in the last 30 days». Фильтр, который сужает журнал, переходит вvariant="primary"— фраза сама показывает, что включено;underlineотмечает слова, которые можно поменять. Список за каждым словом остаётся нативным.Security log Showbyin- Mina Ortiz signed in from Lisbon, Chrome on macOSToday
- Daniel Kim made Grace Wu an adminYesterday
- Mina Ortiz created the API key «Zapier export»2 days ago
- Grace Wu signed in from Dublin, Safari on iOS4 days ago
- Daniel Kim revoked the API key «Old CRM sync»9 days ago
- Grace Wu removed Jonas Weber from the workspace21 days ago
Оформление заказа с телефона
Короткие списки, которые просматривают, остаются нативными — на телефоне системное колесо лучше любой панели. Слот по умолчанию принимает свои опции: отключённую первую строку и страны в
<optgroup>по сроку доставки. Регион ждёт выключенным, пока неизвестна страна,state="warning"подкрашивает почти разобранный слот доставки, аnameотправляет каждое значение вместе с формой.Четыреста часовых поясов
Все зоны IANA, которые знает браузер, по регионам.
virtualдержит в DOM только строки у области просмотра, а группы переживают окно: строка из середины «America» по-прежнему слышна как её часть.filterableсужает список по городу, слотoptionдобавляет смещение от UTC, а#emptyобъясняет промах.Workspace time zoneReports, digests and the billing day are cut at midnight here Local time appears here
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
modelValueобязательныйv-model | GrSelectModelValue<TValue> | — | Выбранное значение (`v-model`); в `multiple` — массив. |
options | GrSelectOptionOrGroup<TValue>[] | undefined | undefined | Список опций. Поддерживает плоский массив опций и группы опций (`{ label, options }`). |
disabled | boolean | undefined | false | Выключить поле целиком. Складывается с `disabled` у `GrFormField`. |
readonly | boolean | undefined | false | Только для чтения: значение видно и уходит в форму, но не меняется. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. |
state | "default" | "success" | "warning" | "danger" | undefined | "default" | Визуальный оттенок рамки: `default | success | warning | danger`. `invalid` сильнее — ошибка перекрывает любую другую подсветку. В `view="link"` рамки нет, и состояние туда не применяется. |
valueKey | string | undefined | undefined | Имя поля-идентификатора, когда значения опций — объекты. Без него объекты сравнивались бы по ссылке, и пришедшая снаружи копия с тем же `id` не совпала бы ни с одной опцией. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
ariaLabel | string | undefined | undefined | Доступное имя вне `GrFormField`. |
shape | "box" | "pill" | undefined | undefined | Форма рамки. `box` — скругление шкалы контролов, как было всегда; `pill` — пилюля, как у `GrSegmented`. В `view="link"` не участвует: у ссылки рамки нет. |
view | GrSelectView | undefined | "default" | Вид триггера: `default` — поле с рамкой, `link` — строчная ссылка без рамки, как `GrLink`. По умолчанию `default`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Высота и кегль по шкале контролов. По умолчанию `md`; берётся из `GrConfigProvider`. |
placeholder | string | undefined | undefined | Placeholder (показывается, когда значение не выбрано). |
multiple | boolean | undefined | false | Multiple selection. |
tagTone |
| "neutral" | Вид чипов выбранных значений в режиме `multiple`. Чип — это `GrBadge`: своя плашка на светлой теме почти не отличалась от фона поля. |
tagDark | boolean | undefined | false | Плотная заливка чипов в режиме `tags`. Поле `dark` у опции сильнее. |
tagSize | "xs" | "sm" | "md" | "lg" | undefined | "sm" | Размер чипов по шкале `GrBadge`. По умолчанию `sm`. |
tagRadius | GrBadgeRadius | undefined | "round" | Скругление чипов: `square`, `semi` или `round`. По умолчанию `round`. |
optionsView | GrSelectOptionsView | undefined | "native" | Как отображать список опций: нативный `<select>` или кастомная панель. |
allowCustomValue | boolean | undefined | false | Разрешает ввод/выбор значения, которого нет в `options`. |
filterable | boolean | undefined | false | Поиск/фильтрация опций по вводу (независимо от `allowCustomValue`). Показывает поле поиска над списком и фильтрует опции. Работает только в `optionsView="panel"` (при `native` панель форсится автоматически). |
filterPlaceholder | string | undefined | undefined | Placeholder поля поиска (`filterable`). i18n: fallback `gr.select.searchPlaceholder`. |
searchv-model | string | undefined | undefined | Текст поиска как контролируемое значение (`v-model:search`). Без него `loading` был декоративным: набранное пользователем наружу не выходило, и сходить за опциями на сервер было не с чем. |
loading | boolean | undefined | false | Состояние загрузки: вместо списка опций панель показывает индикатор загрузки. Полезно для удалённой подгрузки опций. Форсит `optionsView="panel"`. |
loadingText | string | undefined | undefined | Текст индикатора загрузки. i18n: fallback `gr.select.loading`. |
noResultsText | string | undefined | undefined | Текст пустого результата фильтрации. i18n: fallback `gr.select.noResults`. |
tags | boolean | undefined | false | Режим тегов для `multiple`: выбранные значения показываются как удаляемые chips в триггере (вместо строки «a, b, c»). Форсит `optionsView="panel"`. |
customValuePlaceholder | string | undefined | undefined | Placeholder для инпута кастомного значения (только в `optionsView="panel"`). i18n-friendly: если не задан — берётся из адаптера перевода (`gr.select.customValuePlaceholder`), иначе — встроенный fallback. |
dropdownMaxHeight | number | undefined | 280 | Максимальная высота панели (только в `optionsView="panel"`). |
virtual | boolean | undefined | false | Виртуализация панели: в DOM живёт только окно вокруг вьюпорта. Высоту окна задаёт `dropdownMaxHeight`. Только для `optionsView="panel"`: у нативного `<select>` панели нет вовсе. Несовместим с `view="link"` — там ширина панели равна ширине отрисованной опции и прыгала бы при прокрутке. |
closeOnSelect | boolean | undefined | true | Закрывать панель после выбора (только в `optionsView="panel"`). |
maxTagCount | number | undefined | undefined | Сколько chips показать до сворачивания в «+N» (только `tags`). |
clearable | boolean | undefined | undefined | Разрешает очистку выбранного значения. |
clearLabel | string | undefined | undefined | i18n-label для кнопки очистки (`aria-label`). |
variant | GrSelectVariant | undefined | undefined | Цвет/вариант ссылки для `view="link"` (аналогично `GrLink`). В `view="default"` не используется. |
underline | GrSelectUnderline | undefined | undefined | Подчёркивание для `view="link"` (аналогично `GrLink`). В `view="default"` не используется. |
openv-model | boolean | undefined | undefined | Контролируемое состояние панели (`v-model:open`). Без пропа панель ведёт себя сама (uncontrolled), с ним — слушайте `update:open` и меняйте проп. Только для `optionsView="panel"`: у нативного `<select>` панель браузерная. |
name | string | undefined | undefined | Имя для нативной формы: в native-режиме уходит на сам `<select>`, в panel-режиме значения сериализуются hidden-инпутами (ключ — `keyOf`). |
prefixMinWidth | string | undefined | undefined | Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета (`docs/form-controls.md`). Аддоны живут в панельном триггере: внутрь нативного `<select>` разметку положить нельзя. |
prefixMaxWidth | string | undefined | undefined | Максимальная ширина префикса: содержимое шире обрезается. |
suffixMinWidth | string | undefined | undefined | Минимальная ширина суффикса. По умолчанию — по ступени размера. |
suffixMaxWidth | string | undefined | undefined | Максимальная ширина суффикса: содержимое шире обрезается. |
prefixFixed | boolean | undefined | false | Жёсткая ширина префикса: `prefixMaxWidth`, иначе `prefixMinWidth`, иначе по ступени размера. |
suffixFixed | boolean | undefined | false | Жёсткая ширина суффикса: `suffixMaxWidth`, иначе `suffixMinWidth`, иначе по ступени размера. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Собственные `<option>` (и `<optgroup>`) для нативного режима. Выбор у них ставит компонент по `v-model` — так же, как у опций из `options`: свой `:selected` писать не нужно, и сервер отдаёт уже выбранную опцию. |
prefix | any | Аддон слева в панельном триггере (в нативном режиме недоступен). |
suffix | any | Аддон справа в панельном триггере, перед крестиком и шевроном. |
value |
| Отображение значения в триггере вместо текста по умолчанию. |
option | { option: GrSelectOption<TValue>; selected: boolean; } | Строка списка вместо подписи опции. |
loading | any | Содержимое панели, пока едут опции. |
empty | any | Содержимое панели, когда подходящих опций нет. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (e: "update:modelValue", value: GrSelectModelValue<TValue>): void | Новое значение (`v-model`): выбор опции, снятие чипа или очистка. |
change | (e: "change", value: GrSelectModelValue<TValue>): void | Значение изменилось — тот же payload, что у `update:modelValue`. |
clear | (e: "clear"): void | Значение снято кнопкой очистки. |
update:open | (e: "update:open", value: boolean): void | Панель открылась/закрылась (`v-model:open`). |
update:search | (e: "update:search", value: string): void | Текст поиска как контролируемое значение (`v-model:search`). |
focus | (e: "focus", event: FocusEvent): void | Фокус вошёл в контрол. Переход между полем и его панелью событием не считается. |
blur | (e: "blur", event: FocusEvent): void | Фокус ушёл из контрола — из поля и из панели вместе. |
search | (e: "search", value: string): void | Пользователь набрал запрос — сигнал сходить за опциями. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrSelectConfigurableProps, GrSelectView, GrSelectSize } from '@feugene/granularity/components/GrSelect'Файлdist/types/components/GrSelect/config.d.ts
const grSelectConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSelect">;Файлdist/types/components/GrSelect/defaults.d.ts
Пропы GrSelect, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrSelect: { … } }">.
Только оформление и поведение по умолчанию — см. GrButton/defaults.ts.
interface GrSelectConfigurableProps {
size: GrSelectSize;
variant: GrSelectVariant;
underline: GrSelectUnderline;
clearable: boolean;
/** Форма рамки — см. `components/shared/controlShape.ts`. */
shape: GrControlShape;
}Файлdist/types/components/GrSelect/grSelectStyles.d.ts
type GrSelectView = 'default' | 'link';type GrSelectSize = GrComponentSize;type GrSelectVariant = 'primary' | 'default' | 'muted' | 'danger';type GrSelectUnderline = 'auto' | 'always' | 'none';type GrSelectOptionsView = 'native' | 'panel';type GrSelectState = GrControlState;type GrSelectOption<TValue extends GrSelectValue = string> = {
value: TValue;
label: string;
disabled?: boolean;
/**
* Тон чипа в режиме `tags` — перекрывает `tagTone` для этой опции.
*
* Цвет у каждого тега свой — обычная модель (метки, категории, статусы), и
* без неё потребитель уходил в слот `#value` рисовать выбранное сам, теряя
* ровно то, что в `tags` сделано правильно: чипы снаружи `role="combobox"`,
* снятие крестиком, сворачивание в «+N» и клавиатуру.
*/
tone?: GrChipTone;
/** Плотная заливка чипа этой опции — перекрывает `tagDark`. */
dark?: boolean;
};Группа опций: заголовок label + вложенные опции options.
type GrSelectOptionGroup<TValue extends GrSelectValue = string> = {
label: string;
options: GrSelectOption<TValue>[];
};Элемент списка опций: либо одиночная опция, либо группа опций.
type GrSelectOptionOrGroup<TValue extends GrSelectValue = string> = GrSelectOption<TValue> | GrSelectOptionGroup<TValue>;type GrSelectModelValue<TValue extends GrSelectValue = string> = TValue | TValue[];Файлdist/types/components/GrSelect/GrSelect.vue.d.ts
Пропсы публичного GR-примитива «Select».
interface GrSelectProps<TValue extends GrSelectValue = string> {
/** Выбранное значение (`v-model`); в `multiple` — массив. */
modelValue: GrSelectModelValue<TValue>;
/** Список опций. Поддерживает плоский массив опций и группы опций (`{ label, options }`). */
options?: GrSelectOptionOrGroup<TValue>[];
/** Выключить поле целиком. Складывается с `disabled` у `GrFormField`. */
disabled?: boolean;
/** Только для чтения: значение видно и уходит в форму, но не меняется. */
readonly?: boolean;
/** Визуальное и ARIA-состояние ошибки. */
invalid?: boolean;
/**
* Визуальный оттенок рамки: `default | success | warning | danger`. `invalid`
* сильнее — ошибка перекрывает любую другую подсветку. В `view="link"` рамки
* нет, и состояние туда не применяется.
*/
state?: GrSelectState;
/**
* Имя поля-идентификатора, когда значения опций — объекты. Без него объекты
* сравнивались бы по ссылке, и пришедшая снаружи копия с тем же `id` не
* совпала бы ни с одной опцией.
*/
valueKey?: string;
/** Обязательное поле (`aria-required`). */
required?: boolean;
/** Доступное имя вне `GrFormField`. */
ariaLabel?: string;
/**
* Форма рамки. `box` — скругление шкалы контролов, как было всегда; `pill` —
* пилюля, как у `GrSegmented`. В `view="link"` не участвует: у ссылки рамки нет.
*/
shape?: GrControlShape;
/**
* Вид триггера: `default` — поле с рамкой, `link` — строчная ссылка без рамки, как
* `GrLink`. По умолчанию `default`.
*/
view?: GrSelectView;
/** Высота и кегль по шкале контролов. По умолчанию `md`; берётся из `GrConfigProvider`. */
size?: GrSelectSize;
/** Placeholder (показывается, когда значение не выбрано). */
placeholder?: string;
/** Multiple selection. */
multiple?: boolean;
/**
* Вид чипов выбранных значений в режиме `multiple`. Чип — это `GrBadge`: своя
* плашка на светлой теме почти не отличалась от фона поля.
*/
tagTone?: GrBadgeTone;
/** Плотная заливка чипов в режиме `tags`. Поле `dark` у опции сильнее. */
tagDark?: boolean;
/** Размер чипов по шкале `GrBadge`. По умолчанию `sm`. */
tagSize?: GrBadgeSize;
/** Скругление чипов: `square`, `semi` или `round`. По умолчанию `round`. */
tagRadius?: GrBadgeRadius;
/** Как отображать список опций: нативный `<select>` или кастомная панель. */
optionsView?: GrSelectOptionsView;
/** Разрешает ввод/выбор значения, которого нет в `options`. */
allowCustomValue?: boolean;
/**
* Поиск/фильтрация опций по вводу (независимо от `allowCustomValue`). Показывает
* поле поиска над списком и фильтрует опции. Работает только в `optionsView="panel"`
* (при `native` панель форсится автоматически).
*/
filterable?: boolean;
/** Placeholder поля поиска (`filterable`). i18n: fallback `gr.select.searchPlaceholder`. */
filterPlaceholder?: string;
/**
* Текст поиска как контролируемое значение (`v-model:search`). Без него
* `loading` был декоративным: набранное пользователем наружу не выходило, и
* сходить за опциями на сервер было не с чем.
*/
search?: string;
/**
* Состояние загрузки: вместо списка опций панель показывает индикатор загрузки.
* Полезно для удалённой подгрузки опций. Форсит `optionsView="panel"`.
*/
loading?: boolean;
/** Текст индикатора загрузки. i18n: fallback `gr.select.loading`. */
loadingText?: string;
/** Текст пустого результата фильтрации. i18n: fallback `gr.select.noResults`. */
noResultsText?: string;
/**
* Режим тегов для `multiple`: выбранные значения показываются как удаляемые
* chips в триггере (вместо строки «a, b, c»). Форсит `optionsView="panel"`.
*/
tags?: boolean;
/** Placeholder для инпута кастомного значения (только в `optionsView="panel"`). i18n-friendly: если не задан — берётся из адаптера перевода (`gr.select.customValuePlaceholder`), иначе — встроенный fallback. */
customValuePlaceholder?: string;
/** Максимальная высота панели (только в `optionsView="panel"`). */
dropdownMaxHeight?: number;
/**
* Виртуализация панели: в DOM живёт только окно вокруг вьюпорта. Высоту окна
* задаёт `dropdownMaxHeight`.
*
* Только для `optionsView="panel"`: у нативного `<select>` панели нет вовсе.
* Несовместим с `view="link"` — там ширина панели равна ширине отрисованной
* опции и прыгала бы при прокрутке.
*/
virtual?: boolean;
/** Закрывать панель после выбора (только в `optionsView="panel"`). */
closeOnSelect?: boolean;
/** Сколько chips показать до сворачивания в «+N» (только `tags`). */
maxTagCount?: number;
/** Разрешает очистку выбранного значения. */
clearable?: boolean;
/** i18n-label для кнопки очистки (`aria-label`). */
clearLabel?: string;
/**
* Цвет/вариант ссылки для `view="link"` (аналогично `GrLink`).
* В `view="default"` не используется.
*/
variant?: GrSelectVariant;
/**
* Подчёркивание для `view="link"` (аналогично `GrLink`).
* В `view="default"` не используется.
*/
underline?: GrSelectUnderline;
/**
* Контролируемое состояние панели (`v-model:open`). Без пропа панель ведёт
* себя сама (uncontrolled), с ним — слушайте `update:open` и меняйте проп.
* Только для `optionsView="panel"`: у нативного `<select>` панель браузерная.
*/
open?: boolean;
/**
* Имя для нативной формы: в native-режиме уходит на сам `<select>`, в
* panel-режиме значения сериализуются hidden-инпутами (ключ — `keyOf`).
*/
name?: string;
/**
* Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета
* (`docs/form-controls.md`). Аддоны живут в панельном триггере: внутрь
* нативного `<select>` разметку положить нельзя.
*/
prefixMinWidth?: string;
/** Максимальная ширина префикса: содержимое шире обрезается. */
prefixMaxWidth?: string;
/** Минимальная ширина суффикса. По умолчанию — по ступени размера. */
suffixMinWidth?: string;
/** Максимальная ширина суффикса: содержимое шире обрезается. */
suffixMaxWidth?: string;
/** Жёсткая ширина префикса: `prefixMaxWidth`, иначе `prefixMinWidth`, иначе по ступени размера. */
prefixFixed?: boolean;
/** Жёсткая ширина суффикса: `suffixMaxWidth`, иначе `suffixMinWidth`, иначе по ступени размера. */
suffixFixed?: boolean;
}interface GrSelectEmits<TValue extends GrSelectValue = string> {
/** Новое значение (`v-model`): выбор опции, снятие чипа или очистка. */
(e: 'update:modelValue', value: GrSelectModelValue<TValue>): void;
/** Значение изменилось — тот же payload, что у `update:modelValue`. */
(e: 'change', value: GrSelectModelValue<TValue>): void;
/** Значение снято кнопкой очистки. */
(e: 'clear'): void;
/** Панель открылась/закрылась (`v-model:open`). */
(e: 'update:open', value: boolean): void;
/** Текст поиска как контролируемое значение (`v-model:search`). */
(e: 'update:search', value: string): void;
/** Фокус вошёл в контрол. Переход между полем и его панелью событием не считается. */
(e: 'focus', event: FocusEvent): void;
/** Фокус ушёл из контрола — из поля и из панели вместе. */
(e: 'blur', event: FocusEvent): void;
/** Пользователь набрал запрос — сигнал сходить за опциями. */
(e: 'search', value: string): void;
}Файлdist/types/components/GrSelect/safelist.d.ts
const grSelectSafelist: string[];Файлdist/types/components/GrSelect/index.d.ts
type GrSelectInstance = ComponentExposed<typeof GrSelectComponent>;Доступность
Паттерн APG
combobox + listbox
Клавиши
↑ / ↓Home / EndEnterEsc
| Клавиша | Действие |
|---|---|
| ↑ / ↓ | по опциям |
| Home / End | к краям |
| Enter | выбрать активный элемент (при allowCustomValue строка «Add …» — первый элемент навигации, Enter на ней добавляет набранное) |
| Esc | закрыть |
| Tab | закрыть и уйти |
| — | печатные символы — typeahead |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Удалённая подгрузка
<GrSelect
v-model="value"
v-model:search="query"
:options="options"
:loading="pending"
filterable
@search="fetchOptions"
/>loading без v-model:search/@search был декоративным: набранный текст жил
внутри компонента и наружу не выходил — сходить на сервер было не с чем. Теперь
запрос доступен обоими способами: v-model:search для контролируемого значения
и @search для побочного эффекта.
Пока loading, списка в DOM нет, поэтому aria-controls с триггера снимается —
ссылка не висит в пустоту, а вместо неё стоит aria-busy.
Состав панели
Прямые потомки role="listbox" — только опции и группы: роль объявляет
остальных недопустимыми детьми. Заголовок группы лежит внутри своей
role="group" и даёт ей имя через aria-labelledby; загрузка и пустой
результат вынесены из списка в живые регионы role="status" aria-live="polite"
— иначе состояния менялись бы молча.
Опции не забирают фокус (tabindex="-1", mousedown подавлен): у combobox он
живёт на триггере или в поле поиска, а активную опцию называет
aria-activedescendant. Панель с полем поиска на закрытии возвращает фокус на
триггер — если он ещё внутри панели.
Слоты #empty и #loading подменяют оба состояния.
Высота панели
dropdownMaxHeight — потолок списка, а не панели. Саму панель ограничивает
место, оставшееся до края вьюпорта: слой сообщает замер в
--gr-floating-available-height, и на коротком экране сжимается именно список —
поле поиска и строки состояний остаются на виду, а полоса прокрутки одна. Механизм общий на списочные
панели пакета и описан в ../overlays.md.
Виртуализация
virtual оставляет в DOM только окно вокруг вьюпорта; его высоту задаёт
dropdownMaxHeight — или доступное место, если его меньше. Режим только для optionsView="panel" — у нативного
<select> своей панели нет вовсе.
<GrSelect v-model="city" :options="cities" options-view="panel" virtual />Группы переживают окно. Если панель прокручена внутрь группы, её заголовка
в разметке уже нет — обёртка role="group" всё равно создаётся и берёт имя
через aria-label вместо aria-labelledby. Иначе опции середины группы стали
бы прямыми детьми listbox’а и потеряли бы имя набора.
Набор считается по группе, а не по списку. При virtual опции несут
aria-setsize/aria-posinset, и для опции внутри группы это размер её группы —
так того требует ARIA. Опции вне групп вместе с кнопкой «Add …» образуют набор
уровня listbox’а. В обычном режиме атрибутов нет: там набор виден по DOM.
Не сочетается с view="link". В этом виде опции несут w-max, то есть
ширина панели равна ширине самой широкой отрисованной опции — при
виртуализации она прыгала бы на каждой прокрутке. Оба несовместимых сочетания
пакет сообщает dev-предупреждением. Устройство примитива —
virtual-list.md.
Активная опция и фокус
aria-activedescendant работает только на элементе, который держит фокус. При
filterable/allowCustomValue фокус уходит в поле поиска внутри панели —
связка с активной опцией живёт там же, и поле объявляет себя role="combobox".
Без поля поиска всё остаётся на триггере.
При открытии панели активной становится выбранная опция, и список сразу
прокручен к ней — с virtual тоже: окно встаёт вокруг неё, и
aria-activedescendant всегда указывает на отрисованный элемент. Стрелки
держат активную опцию в окне.
Теги
<GrSelect v-model="values" multiple tags :max-tag-count="3" :options="options" />Чипы живут рядом с кнопкой-комбобоксом, а не внутри неё: role="combobox"
объявляет потомков презентационными, и крестик внутри был недостижим с
клавиатуры (axe: nested-interactive). Теперь это настоящие кнопки в
таб-порядке.
maxTagCount сворачивает хвост в «+N» — без него длинный выбор превращался в
простыню чипов.
Поле растёт вместе с рядами чипов. В узкой колонке чипы переносятся, и поле становится выше — не ниже обычной строки контрола своего размера; рамка и кольцо фокуса обходят все ряды. Шеврон и кнопка очистки стоят по центру высоты. Раньше поле держало высоту одной строки, и второй ряд висел под рамкой поверх того, что идёт ниже.
Цвет тега задаётся и на опции: tone и dark в самой опции перекрывают
общие tagTone/tagDark. Нужно это чаще, чем кажется — метки, категории и
статусы обычно приходят со своим цветом, — а без такой возможности потребитель
уходил рисовать выбранное в слот #value и терял ровно то, ради чего берут
tags: чипы снаружи role="combobox", снятие крестиком, сворачивание в «+N» и
клавиатуру.
const options = [
{ value: 'bug', label: 'Баг', tone: 'danger' },
{ value: 'idea', label: 'Идея', tone: 'success', dark: true },
]События
| Событие | Когда |
|---|---|
update:modelValue | значение изменилось |
change | то же значение, отдельным каналом (паритет с GrTreeSelect); в обоих режимах отрисовки |
clear | значение снято кнопкой очистки |
update:open | панель открылась/закрылась (v-model:open) |
update:search / search | пользователь набрал запрос |
Панель управляема через v-model:open (контракт панельных оверлеев пакета,
как у GrPopover): без пропа open селект ведёт себя сам, с ним состоянием
владеет родитель. Проп name включает участие в нативной форме: в
native-режиме имя уходит на сам <select>, в panel-режиме значения
сериализуются hidden-инпутами (ключ — valueKey/keyOf, по одному на
значение при multiple).
Свои <option> в нативном режиме
Дефолтный слот принимает собственные <option> и <optgroup> — например,
недоступную подсказку первой строкой. Выбор у них ставит компонент по
v-model, как у опций из options: свой :selected писать не нужно, и сервер
отдаёт уже выбранную опцию.
<template>
<GrSelect v-model="country" name="country">
<option value="" disabled>
Choose a country
</option>
<optgroup label="Next working day">
<option value="ie">
Ireland
</option>
</optgroup>
</GrSelect>
</template>Раньше опции из слота не получали ничего: при country = '' браузер показывал
первую доступную опцию, а программная смена модели на экран не доходила.
Пустое значение в нативном режиме
У одиночного нативного выбора пустая модель (undefined, null, '') всегда видна как пустая.
С clearable её показывает опция очистки. Без него компонент ставит первой опцию-подсказку
<option value="" disabled hidden selected> с текстом placeholder, а без него — переведённым
«Select…» (gr.select.placeholder). Без такой опции браузер выбирает первую сам: поле показывало
бы «Germany» при пустом значении, а select.value уходил бы в форму мимо модели.
Пока поле показывает подсказку или опцию очистки, у <select> стоит data-gr-select-empty, а его
текст — цветом подсказки (--gr-muted-fg), как ::placeholder у GrInput: иначе подсказка читается
выбранным значением. Опции раскрытого списка возвращают обычный цвет, недоступные — браузерный.
Цвета ставятся стилем в строке, а не утилитами: правило с тем же телом слилось бы с общим и сдвинуло
порядок правил у соседей.
Подсказку не выбрать обратно (disabled) и нет в раскрытом списке (hidden): вернуть «ничего»
без clearable нельзя — так и задумано у обязательного поля. Как только значение выбрано,
подсказки нет. Если в options есть опция с пустым значением, она сама и есть «ничего не выбрано»,
и подсказка не добавляется; своя пустая <option> в слоте перекрывает её тем же образом.
Значения-объекты
<GrSelect v-model="owner" :options="ownerOptions" value-key="id" />Значением опции может быть объект — тогда обязателен valueKey с именем
поля-идентификатора. Через него компонент строит ключ, по которому сравнивает
значения и кладёт их в DOM: === означал бы сравнение ссылок, а модель обычно
приходит снаружи отдельной копией — с тем же id, но другим объектом, и не
совпала бы ни с одной опцией. Без valueKey объектные значения в dev-сборке
дают предупреждение.
Наружу уходит сам объект, а не строка из DOM. allowCustomValue с объектными
значениями не работает по природе: пользователь вводит текст.
Состояния
state (default | success | warning | danger) задаёт оттенок рамки, invalid
форсирует красную и объявляет aria-invalid — ошибка перекрывает любую другую
подсветку. В view="link" рамки нет, и состояние туда не применяется.
readonly показывает значение, но не открывает панель и не меняет выбор;
disabled гасит контрол целиком. Кнопка очистки не прячет шеврон: поле с
выбранным значением обязано выглядеть выпадающим списком.
Заголовок группы связан с опциями через aria-describedby — так группа
слышна, не превращая плоский список в дерево.
Аддоны prefix / suffix
Слоты доступны только при optionsView="panel" — внутрь нативного
<select> разметку положить нельзя (в dev об этом предупредит консоль). Кладут в оболочку иконку, единицу или метку; ширина ограничивается шестью
пропами (prefixMinWidth/prefixMaxWidth/prefixFixed и то же для суффикса).
Общий контракт контролов — form-controls.md.
Императивный API
focus() и blur() через ref компонента — в нативном режиме адресуют
<select>, в панельном — кнопку-триггер.
Пропы этого примера