GrTreeSelect
Берут, когда варианты вложены.
Обзор
Когда брать
- варианты вложены — категории, оргструктура, регионы, разделы каталога: плоский список потерял бы уровни;
- важен путь до узла — выбранное показывается вместе с ветками, а не одним листом;
- выбирают несколько узлов — с учётом или без учёта родителей (
checkStrictly); - дерево большое — фильтрация вводом и виртуализация приезжают из
GrTree.
Когда взять другое
| Нужно | Берите |
|---|---|
| Варианты плоские | GrSelect |
| Вариантов много и их ищут вводом | GrAutocomplete |
| Дерево нужно на экране, а не в панели | GrTree |
| Уровень всего один, но с заголовками групп | GrSelect |
Варианты все тона и виды — настоящим компонентом
state \ shape | box | pill |
|---|---|---|
default | ||
success | ||
warning | ||
danger |
Размеры
xssmmdlg
Состояния
обычнаяdisabledloadingreadonlyinvalid
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrTreeSelect
:data="data"
v-model="value"
:defaultExpandedKeys="defaultExpandedKeys"
/>Примеры 3 у каждого — живое демо и его код
Счёт, на который относят расход
«Hotels» сам по себе двусмыслен, «Travel / Hotels» — нет:
value-display="path"показывает ветку прямо в поле.filterableсfilter-node-methodнаходит счёт по коду так же, как по имени, а слотnodeставит код справа в каждой строке. Группа — не счёт для проводки, и форма говорит об этом черезinvalid.Hotel Adlon · Berlin2 nights · receipt attached€412.80Где действует тариф доставки
multipleсshow-checkbox: отметка «Nordics» закрывает все четыре страны, а регион, где отмечены не все, — наполовину.tagsпоказывает выбор чипами,max-tag-countсворачивает остаток в «+N», чтобы поле держало высоту, аnameотправляет по скрытому полю на ключ. В значении есть и родители, поэтому сводка считает страны сама.Express · €14.90 per parcelShips to 5 of 14 countriesCheck a region to cover every country in it.
EU · WestGermanyFrance +3Руководитель из оргструктуры
Структура приезжает при первом
update:open, а пока её нет,loadingподменяет «Нет данных» слотомloading. Полторы сотни строк остаются лёгкими благодаряvirtualвнутриdropdown-max-height. Слотnodeдобавляет должность к имени, слотvalueставит в поле лицо, а профиль, синхронизированный из HR, —readonly.NLNora LindqvistBackend engineer · joined in MarchSearch by name or browse by team.
AOAmara OkaforBilling lead
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
modelValueобязательныйv-model | GrTreeSelectModelValue | — | Выбранный ключ или `null`; при `multiple` — массив ключей (`v-model`). |
dataобязательный | T[] | — | Узлы дерева верхнего уровня; дети лежат в поле из `props.children`. |
props | GrTreePropsMap | undefined | {
children: "children",
label: "label",
} | Имена полей узла: детей и подписи. По умолчанию `children` и `label`. |
nodeKey | "id" | NodeKeyProp<T> | undefined | "id" as any | Поле-ключ узла — его значения уходят в модель. По умолчанию `id`; без значения ключом становится позиция узла в дереве. |
defaultExpandedKeys | GrTreeKey[] | undefined | [] | Ключи узлов, раскрытых в дереве панели с самого начала. |
disabled | boolean | undefined | false | Недоступен: панель не открывается, значение не меняется. Складывается по «или» с полем и формой. |
placeholder | string | undefined | undefined | Текст триггера, пока ничего не выбрано. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Высота триггера и кегль по шкале контролов; дерево в панели берёт ту же ступень. По умолчанию `md`. |
shape | "box" | "pill" | undefined | undefined | Форма рамки. `box` — скругление шкалы контролов; `pill` — пилюля. |
loading | boolean | undefined | false | Данные ещё едут. Панель показывает индикатор вместо «Нет данных» — иначе пустой ответ и незагруженный выглядят одинаково. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки; сильнее `state`. Складывается по «или» с `GrFormField`. |
readonly | boolean | undefined | false | Только для чтения: значение видно, но не меняется. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
ariaLabel | string | undefined | undefined | Доступное имя вне `GrFormField`. |
state | "default" | "success" | "warning" | "danger" | undefined | "default" | Подсветка рамки по решению разработчика. `success` и `warning` добавляют иконку и скрытую подпись; `invalid` сильнее. По умолчанию `default`. |
multiple | boolean | undefined | false | Множественный выбор: модель — массив ключей, клик по узлу переключает его. |
showCheckbox | boolean | undefined | false | Чекбоксы в дереве вместо собственной галочки: отметка родителя каскадом закрывает поддерево, полувыбранный родитель показывается `mixed`. Работает только вместе с `multiple`. |
checkStrictly | boolean | undefined | false | Отвязать родителей от детей: отметка перестаёт распространяться каскадом. |
tags | boolean | undefined | false | Чипы выбранных узлов в триггере вместо строки «a, b, c». Работает только вместе с `multiple`: у одиночного выбора чип был бы плашкой на одно значение. |
maxTagCount | number | undefined | undefined | Сколько чипов показать до сворачивания остатка в «+N». Без него растёт весь набор, а высота триггера за ним не идёт — лишние чипы уходят за край. |
tagTone |
| "neutral" | Вид чипов. Чип — это `GrBadge`: своя плашка на светлой теме почти не отличалась бы от фона поля. Имена и шкалы те же, что у `GrSelect`, — переходя с одного компонента на другой, потребитель не переучивается. |
tagDark | boolean | undefined | false | Плотная заливка чипов в режиме `tags`. |
tagSize | "xs" | "sm" | "md" | "lg" | undefined | "sm" | Размер чипов по шкале `GrBadge`. По умолчанию `sm`. |
tagRadius | GrBadgeRadius | undefined | "round" | Скругление чипов: `square`, `semi` или `round`. По умолчанию `round`. |
clearable | boolean | undefined | false | Крестик очистки на месте шеврона, когда что-то выбрано. По умолчанию `false`. |
openv-model | boolean | undefined | undefined | Контролируемое состояние панели (`v-model:open`). Без пропа панель ведёт себя сама (uncontrolled), с ним — слушайте `update:open` и меняйте проп. |
name | string | undefined | undefined | Имя для нативной формы: hidden input на каждый выбранный ключ. |
valueDisplay | GrTreeSelectValueDisplay | undefined | "label" | Как отображать выбранное значение в single-режиме. |
filterable | boolean | undefined | false | Поле поиска над деревом; без `filterNodeMethod` узлы ищутся подстрокой в подписи. |
filterPlaceholder | string | undefined | undefined | Подсказка в поле поиска. По умолчанию — переведённое «Search…». |
filterInputmode |
| undefined | `inputmode` поля поиска — какую экранную клавиатуру показать на мобильных. |
filterNodeMethod | GrTreeFilterNodeMethod<T> | undefined | undefined | Своё правило совпадения узла с запросом вместо подстрочного поиска по подписи. |
closeOnSelect | boolean | undefined | undefined | Закрывать панель после выбора узла. По умолчанию — только при одиночном выборе. |
dropdownMaxHeight | number | undefined | 320 | Максимальная высота панели с деревом, px. По умолчанию `320`. |
virtual | boolean | undefined | false | Виртуализация дерева в панели: в DOM живёт только окно вокруг вьюпорта. Скроллером в этом режиме становится само дерево, а не контейнер панели — два вложенных скроллера дали бы две полосы прокрутки на одном списке. |
prefixMinWidth | string | undefined | undefined | Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета (`docs/form-controls.md`). |
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 | Описание |
|---|---|---|
value |
| Рендер значения внутри триггера (вместо дефолтного текста). |
node | { node: GrTreeNode<T>; data: T; selected: boolean; } | Рендер строки дерева. |
empty | any | Содержимое пустого состояния (когда нет данных). |
loading | any | Содержимое панели, пока данные едут. |
prefix | any | Аддон слева от значения: иконка, код валюты, метка. |
suffix | any | Аддон справа от значения, перед крестиком и шевроном. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (e: "update:modelValue", value: GrTreeSelectModelValue): void | Новое значение для `v-model`: ключ, массив ключей при `multiple` или `null` после очистки. |
change | (e: "change", value: GrTreeSelectModelValue): void | Значение изменилось — тот же payload, что у `update:modelValue`. |
update:open | (e: "update:open", value: boolean): void | Панель открылась/закрылась (`v-model:open`). |
clear | (e: "clear"): void | Значение снято крестиком; только при `clearable`. |
nodeClick | (e: "nodeClick", data: T, node: GrTreeNode<T>): void | Клик по узлу в панели: его данные и узел дерева. Приходит до смены значения. |
focus | (e: "focus", event: FocusEvent): void | Фокус вошёл в контрол. Переход между триггером и панелью событием не считается. |
blur | (e: "blur", event: FocusEvent): void | Фокус ушёл из контрола — из триггера и из панели вместе. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrTreeSelectModelValue, GrTreeSelectValueDisplay, GrTreeSelectProps } from '@feugene/granularity/components/GrTreeSelect'Файлdist/types/components/GrTreeSelect/grTreeSelectTypes.d.ts
type GrTreeSelectModelValue = GrTreeKey | GrTreeKey[] | null;type GrTreeSelectValueDisplay = 'label' | 'path';Пропсы публичного GR-примитива «TreeSelect».
interface GrTreeSelectProps<T extends object = any> {
/** Выбранный ключ или `null`; при `multiple` — массив ключей (`v-model`). */
modelValue: GrTreeSelectModelValue;
/** Узлы дерева верхнего уровня; дети лежат в поле из `props.children`. */
data: T[];
/** Имена полей узла: детей и подписи. По умолчанию `children` и `label`. */
props?: GrTreePropsMap;
/**
* Поле-ключ узла — его значения уходят в модель. По умолчанию `id`; без значения ключом
* становится позиция узла в дереве.
*/
nodeKey?: NodeKeyProp<T> | 'id';
/** Ключи узлов, раскрытых в дереве панели с самого начала. */
defaultExpandedKeys?: GrTreeKey[];
/**
* Недоступен: панель не открывается, значение не меняется. Складывается по «или» с полем
* и формой.
*/
disabled?: boolean;
/** Текст триггера, пока ничего не выбрано. */
placeholder?: string;
/**
* Высота триггера и кегль по шкале контролов; дерево в панели берёт ту же ступень.
* По умолчанию `md`.
*/
size?: GrInputSize;
/** Форма рамки. `box` — скругление шкалы контролов; `pill` — пилюля. */
shape?: GrControlShape;
/**
* Данные ещё едут. Панель показывает индикатор вместо «Нет данных» — иначе
* пустой ответ и незагруженный выглядят одинаково.
*/
loading?: boolean;
/** Визуальное и ARIA-состояние ошибки; сильнее `state`. Складывается по «или» с `GrFormField`. */
invalid?: boolean;
/** Только для чтения: значение видно, но не меняется. */
readonly?: boolean;
/** Обязательное поле (`aria-required`). */
required?: boolean;
/** Доступное имя вне `GrFormField`. */
ariaLabel?: string;
/**
* Подсветка рамки по решению разработчика. `success` и `warning` добавляют иконку и скрытую
* подпись; `invalid` сильнее. По умолчанию `default`.
*/
state?: GrTreeSelectState;
/** Множественный выбор: модель — массив ключей, клик по узлу переключает его. */
multiple?: boolean;
/**
* Чекбоксы в дереве вместо собственной галочки: отметка родителя каскадом
* закрывает поддерево, полувыбранный родитель показывается `mixed`. Работает
* только вместе с `multiple`.
*/
showCheckbox?: boolean;
/** Отвязать родителей от детей: отметка перестаёт распространяться каскадом. */
checkStrictly?: boolean;
/**
* Чипы выбранных узлов в триггере вместо строки «a, b, c». Работает только
* вместе с `multiple`: у одиночного выбора чип был бы плашкой на одно значение.
*/
tags?: boolean;
/**
* Сколько чипов показать до сворачивания остатка в «+N». Без него растёт весь
* набор, а высота триггера за ним не идёт — лишние чипы уходят за край.
*/
maxTagCount?: number;
/**
* Вид чипов. Чип — это `GrBadge`: своя плашка на светлой теме почти не
* отличалась бы от фона поля. Имена и шкалы те же, что у `GrSelect`, — переходя
* с одного компонента на другой, потребитель не переучивается.
*/
tagTone?: GrBadgeTone;
/** Плотная заливка чипов в режиме `tags`. */
tagDark?: boolean;
/** Размер чипов по шкале `GrBadge`. По умолчанию `sm`. */
tagSize?: GrBadgeSize;
/** Скругление чипов: `square`, `semi` или `round`. По умолчанию `round`. */
tagRadius?: GrBadgeRadius;
/** Крестик очистки на месте шеврона, когда что-то выбрано. По умолчанию `false`. */
clearable?: boolean;
/**
* Контролируемое состояние панели (`v-model:open`). Без пропа панель ведёт
* себя сама (uncontrolled), с ним — слушайте `update:open` и меняйте проп.
*/
open?: boolean;
/** Имя для нативной формы: hidden input на каждый выбранный ключ. */
name?: string;
/** Как отображать выбранное значение в single-режиме. */
valueDisplay?: GrTreeSelectValueDisplay;
/** Поле поиска над деревом; без `filterNodeMethod` узлы ищутся подстрокой в подписи. */
filterable?: boolean;
/** Подсказка в поле поиска. По умолчанию — переведённое «Search…». */
filterPlaceholder?: string;
/** `inputmode` поля поиска — какую экранную клавиатуру показать на мобильных. */
filterInputmode?: InputHTMLAttributes['inputmode'];
/** Своё правило совпадения узла с запросом вместо подстрочного поиска по подписи. */
filterNodeMethod?: GrTreeFilterNodeMethod<T>;
/** Закрывать панель после выбора узла. По умолчанию — только при одиночном выборе. */
closeOnSelect?: boolean;
/** Максимальная высота панели с деревом, px. По умолчанию `320`. */
dropdownMaxHeight?: number;
/**
* Виртуализация дерева в панели: в DOM живёт только окно вокруг вьюпорта.
*
* Скроллером в этом режиме становится само дерево, а не контейнер панели —
* два вложенных скроллера дали бы две полосы прокрутки на одном списке.
*/
virtual?: boolean;
/**
* Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета
* (`docs/form-controls.md`).
*/
prefixMinWidth?: string;
/** Максимальная ширина префикса: содержимое шире обрезается. */
prefixMaxWidth?: string;
/** Минимальная ширина суффикса. По умолчанию — по ступени размера. */
suffixMinWidth?: string;
/** Максимальная ширина суффикса: содержимое шире обрезается. */
suffixMaxWidth?: string;
/** Жёсткая ширина префикса: `prefixMaxWidth`, иначе `prefixMinWidth`, иначе по ступени размера. */
prefixFixed?: boolean;
/** Жёсткая ширина суффикса: `suffixMaxWidth`, иначе `suffixMinWidth`, иначе по ступени размера. */
suffixFixed?: boolean;
}Файлdist/types/components/GrTreeSelect/config.d.ts
const grTreeSelectConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTreeSelect">;Файлdist/types/components/GrTreeSelect/defaults.d.ts
Пропы GrTreeSelect, настраиваемые глобально через componentDefaults.
interface GrTreeSelectConfigurableProps {
size: GrComponentSize;
/** Форма рамки — см. `components/shared/controlShape.ts`. */
shape: GrControlShape;
}Файлdist/types/components/GrTreeSelect/safelist.d.ts
Пуст: составных классов у поля нет. Размер, форма и состояние выбираются из
мап grTreeSelectStyles.ts и shared/ целыми литералами; шаблонные строки
grTreeSelectPanelClass и иконки состояния лишь склеивают через пробел целые
классы и новых имён не порождают. granum извлекает всё это из чанков
компонента сам, включая общие, — переход панели и формы контрола тоже.
const grTreeSelectSafelist: string[];Файлdist/types/components/GrTreeSelect/GrTreeSelect.vue.d.ts
interface GrTreeSelectEmits<T extends Record<string, any> = any> {
/** Новое значение для `v-model`: ключ, массив ключей при `multiple` или `null` после очистки. */
(e: 'update:modelValue', value: GrTreeSelectModelValue): void;
/** Значение изменилось — тот же payload, что у `update:modelValue`. */
(e: 'change', value: GrTreeSelectModelValue): void;
/** Панель открылась/закрылась (`v-model:open`). */
(e: 'update:open', value: boolean): void;
/** Значение снято крестиком; только при `clearable`. */
(e: 'clear'): void;
/** Клик по узлу в панели: его данные и узел дерева. Приходит до смены значения. */
(e: 'nodeClick', data: T, node: GrTreeNode<T>): void;
/** Фокус вошёл в контрол. Переход между триггером и панелью событием не считается. */
(e: 'focus', event: FocusEvent): void;
/** Фокус ушёл из контрола — из триггера и из панели вместе. */
(e: 'blur', event: FocusEvent): void;
}Файлdist/types/components/GrTreeSelect/index.d.ts
type GrTreeSelectInstance = ComponentExposed<typeof GrTreeSelectComponent>;Доступность
Паттерн APG
combobox + tree
Клавиши
↓ / ↑ / Enter / SpaceEsc
| Клавиша | Действие |
|---|---|
| ↓ / ↑ / Enter / Space | открыть панель и перевести фокус в дерево (при filterable — сначала в поле поиска, оттуда в дерево ведёт ↓/↑) |
| Esc | закрыть и вернуть фокус на триггер |
| — | Tab из панели — закрыть; внутри дерева — клавиши GrTree |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Множественный выбор чекбоксами
<GrTreeSelect v-model="areas" :data="tree" multiple show-checkbox />show-checkbox включает чекбоксы GrTree вместо собственной
галочки: отметка родителя каскадом закрывает поддерево, частично отмеченный
родитель объявляется aria-checked="mixed". Работает только вместе с
multiple — в одиночном выборе текущий узел и так подсвечен.
Каскад считает само дерево, а не селект: клик по строке, клик по квадратику и
Space идут одним путём, поэтому двойного переключения не бывает.
check-strictly отвязывает родителя от детей.
В modelValue попадают все отмеченные ключи, включая родительские. Если
нужны только листья, включите check-strictly и отмечайте их сами либо
отфильтруйте значение снаружи.
Клавиатура: из триггера в дерево
↓, ↑, Enter и Space на триггере открывают панель и переводят фокус в
дерево — дальше работают все клавиши GrTree. Панель телепортирована в
body, поэтому Tab туда не ведёт и другого пути внутрь нет.
При filterable фокус сначала уходит в поле поиска (набирать фильтр — первое,
чего ждут), а ↓/↑ оттуда уводят в дерево. Tab из панели её закрывает,
Escape закрывает и возвращает фокус на триггер силами общего стека слоёв.
Поиск — вспомогательное поле панели, а не контрол поля формы: контекст
GrFormField (id, подсказка, ошибка, required, invalid) достаётся триггеру,
а поиск называется своим filterPlaceholder и показывает его, пока пуст, — и
под фокусом тоже. Строки дерева в панели — на шаге контрола того же size.
ARIA
Триггер — role="combobox" с aria-haspopup="tree" и aria-controls на
дерево, пока панель открыта. Ссылаться на дерево, которого нет в DOM (пустые
данные, загрузка), нельзя — в этих состояниях aria-controls не выводится.
Внутри GrFormField контрол берёт из контекста id, aria-describedby,
aria-invalid и aria-required; вне поля доступное имя даёт ariaLabel.
Состояния
state="success" и "warning" показывают иконку и скрытую подпись, которая
уезжает в aria-describedby контрола: цвет рамки как единственный носитель
смысла — это WCAG 1.4.1, а ARIA-эквивалента aria-invalid у этих двух
состояний нет. danger иконки не получает — его смысл уже несут aria-invalid
и текст ошибки от GrFormField, — и признак гаснет, как только контрол
объявлен невалидным.
disabled красится фоном и цветом текста, а не прозрачностью: opacity
разбавляет выверенные на AA токены и роняет контраст. readonly оставляет
значение видимым, но убирает и кнопку очистки, и открытие панели.
loading показывает индикатор вместо «Нет данных»: пустой ответ и
незагруженные данные не должны выглядеть одинаково. Разметку можно заменить
слотом #loading.
Размер
size читается через useGrComponentSize(), поэтому действует и
GrConfigProvider, и точечный componentDefaults.GrTreeSelect. Тот же размер
уезжает в дерево внутри панели — иначе контрол и его список набирались бы
разным кеглем.
Отображение значения
valueDisplay="path" в одиночном режиме показывает путь через / вместо
одной подписи. При multiple в триггере по умолчанию остаётся «первая метка
+N»; полный список отдаётся слоту #value — там собирается любая своя разметка.
Чипы выбранных узлов
tags (только вместе с multiple) показывает выбранное чипами вместо строки
«a, b, c». Крестик на чипе снимает свой узел, не открывая панель.
max-tag-count держит ряд в одну строку: остаток сворачивается в «+N». Вид
настраивается теми же tagTone/tagDark/tagSize/tagRadius, что у
GrSelect, — переходя с одного компонента на другой, потребитель не
переучивается.
Высота триггера за числом чипов не идёт: без max-tag-count длинный набор
уходит за край. Это и есть его назначение, а не недоделка — растущее поле
двигало бы всю форму под собой на каждый выбор.
Чипы лежат рядом с триггером, а не внутри него: role="combobox" объявляет
потомков презентационными, и крестик внутри был бы недостижим с клавиатуры
(axe: nested-interactive). Полоса чипов прозрачна для указателя, а каждый чип —
нет: клик мимо чипа открывает панель, как и любое другое место триггера.
В режиме show-checkbox снятие чипа идёт через дерево, а не вычёркиванием ключа:
каскад по родителям и детям считает GrTree, и убрав ключ мимо него, можно было
бы получить отмеченного родителя без единого отмеченного ребёнка.
Аддоны prefix / suffix
Слоты кладут в оболочку иконку, единицу или метку; ширина ограничивается шестью
пропами (prefixMinWidth/prefixMaxWidth/prefixFixed и то же для суффикса).
Общий контракт контролов — form-controls.md.
Высота панели
dropdownMaxHeight — потолок списка, а не панели. Саму панель ограничивает
место, оставшееся до края вьюпорта: слой сообщает замер в
--gr-floating-available-height, и на коротком экране сжимается именно список —
поле фильтра и строки состояний остаются на виду, а полоса прокрутки одна. Механизм общий на списочные
панели пакета и описан в ../overlays.md.
Управление панелью и нативная форма
Панель управляема через v-model:open (общий контракт панельных оверлеев, как
у GrPopover): без пропа open — uncontrolled-поведение, с ним состоянием
владеет родитель.
Проп name включает участие в нативной форме: на каждый выбранный ключ
рендерится input[type="hidden"] с этим именем (стандартная сериализация
повторяющимся ключом), пустой выбор не отправляет ничего.
Пропы этого примера