GrTreeSelect

  • ядро
  • Формы
  • API · 41 props
  • ⌨ клавиши

Берут, когда варианты вложены.

import { GrTreeSelect } from '@feugene/granularity/components/GrTreeSelect'

Обзор

Когда брать

  • варианты вложены — категории, оргструктура, регионы, разделы каталога: плоский список потерял бы уровни;
  • важен путь до узла — выбранное показывается вместе с ветками, а не одним листом;
  • выбирают несколько узлов — с учётом или без учёта родителей (checkStrictly);
  • дерево большое — фильтрация вводом и виртуализация приезжают из GrTree.

Когда взять другое

НужноБерите
Варианты плоскиеGrSelect
Вариантов много и их ищут вводомGrAutocomplete
Дерево нужно на экране, а не в панелиGrTree
Уровень всего один, но с заголовками группGrSelect

Варианты все тона и виды — настоящим компонентом

Варианты · GrTreeSelect
state \ shapeboxpill
default
success
warning
danger

Размеры

  • xs
  • sm
  • md
  • lg

Состояния

  • обычная
  • disabled
  • loading
  • readonly
  • invalid

Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 3 у каждого — живое демо и его код

  • Счёт, на который относят расход

    «Hotels» сам по себе двусмыслен, «Travel / Hotels» — нет: value-display="path" показывает ветку прямо в поле. filterable с filter-node-method находит счёт по коду так же, как по имени, а слот node ставит код справа в каждой строке. Группа — не счёт для проводки, и форма говорит об этом через invalid.

    Hotel Adlon · Berlin2 nights · receipt attached€412.80
    Travel
    Flights6410
    Hotels6420
    Taxis and trains6430
  • Где действует тариф доставки

    multiple с show-checkbox: отметка «Nordics» закрывает все четыре страны, а регион, где отмечены не все, — наполовину. tags показывает выбор чипами, max-tag-count сворачивает остаток в «+N», чтобы поле держало высоту, а name отправляет по скрытому полю на ключ. В значении есть и родители, поэтому сводка считает страны сама.

    Express · €14.90 per parcelShips to 5 of 14 countries

    Check a region to cover every country in it.

    EU · WestGermanyFrance +3
    Europe
    United Kingdom
  • Руководитель из оргструктуры

    Структура приезжает при первом update:open, а пока её нет, loading подменяет «Нет данных» слотом loading. Полторы сотни строк остаются лёгкими благодаря virtual внутри dropdown-max-height. Слот node добавляет должность к имени, слот value ставит в поле лицо, а профиль, синхронизированный из HR, — readonly.

    NLNora LindqvistBackend engineer · joined in March

    Search by name or browse by team.

    AOAmara OkaforBilling lead
    Engineering
    Billing
    Amara OkaforBilling lead

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
modelValueобязательныйv-modelGrTreeSelectModelValue—Выбранный ключ или `null`; при `multiple` — массив ключей (`v-model`).
dataобязательныйT[]—Узлы дерева верхнего уровня; дети лежат в поле из `props.children`.
propsGrTreePropsMap | undefined{ children: "children", label: "label", }Имена полей узла: детей и подписи. По умолчанию `children` и `label`.
nodeKey"id" | NodeKeyProp<T> | undefined"id" as anyПоле-ключ узла — его значения уходят в модель. По умолчанию `id`; без значения ключом становится позиция узла в дереве.
defaultExpandedKeysGrTreeKey[] | undefined[]Ключи узлов, раскрытых в дереве панели с самого начала.
disabledboolean | undefinedfalseНедоступен: панель не открывается, значение не меняется. Складывается по «или» с полем и формой.
placeholderstring | undefinedundefinedТекст триггера, пока ничего не выбрано.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedВысота триггера и кегль по шкале контролов; дерево в панели берёт ту же ступень. По умолчанию `md`.
shape"box" | "pill" | undefinedundefinedФорма рамки. `box` — скругление шкалы контролов; `pill` — пилюля.
loadingboolean | undefinedfalseДанные ещё едут. Панель показывает индикатор вместо «Нет данных» — иначе пустой ответ и незагруженный выглядят одинаково.
invalidboolean | undefinedfalseВизуальное и ARIA-состояние ошибки; сильнее `state`. Складывается по «или» с `GrFormField`.
readonlyboolean | undefinedfalseТолько для чтения: значение видно, но не меняется.
requiredboolean | undefinedfalseОбязательное поле (`aria-required`).
ariaLabelstring | undefinedundefinedДоступное имя вне `GrFormField`.

Слоты

SlotTypeОписание
value
{ value: GrTreeSelectModelValue; labels: string[]; displayVa{ value: GrTreeSelectModelValue; labels: string[]; displayValue: string; pathLabels?: string[] | undefined; }
Рендер значения внутри триггера (вместо дефолтного текста).
node{ node: GrTreeNode<T>; data: T; selected: boolean; }Рендер строки дерева.
emptyanyСодержимое пустого состояния (когда нет данных).
loadinganyСодержимое панели, пока данные едут.
prefixanyАддон слева от значения: иконка, код валюты, метка.
suffixanyАддон справа от значения, перед крестиком и шевроном.

События

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

GrTreeSelectModelValuetype
type GrTreeSelectModelValue = GrTreeKey | GrTreeKey[] | null;
GrTreeSelectValueDisplaytype
type GrTreeSelectValueDisplay = 'label' | 'path';
GrTreeSelectPropsinterface

Пропсы публичного 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

grTreeSelectConfigconst
const grTreeSelectConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTreeSelect">;

Файлdist/types/components/GrTreeSelect/defaults.d.ts

GrTreeSelectConfigurablePropsinterface

Пропы GrTreeSelect, настраиваемые глобально через componentDefaults.

interface GrTreeSelectConfigurableProps {
    size: GrComponentSize;
    /** Форма рамки — см. `components/shared/controlShape.ts`. */
    shape: GrControlShape;
}

Файлdist/types/components/GrTreeSelect/safelist.d.ts

grTreeSelectSafelistconst

Пуст: составных классов у поля нет. Размер, форма и состояние выбираются из мап grTreeSelectStyles.ts и shared/ целыми литералами; шаблонные строки grTreeSelectPanelClass и иконки состояния лишь склеивают через пробел целые классы и новых имён не порождают. granum извлекает всё это из чанков компонента сам, включая общие, — переход панели и формы контрола тоже.

const grTreeSelectSafelist: string[];

Файлdist/types/components/GrTreeSelect/GrTreeSelect.vue.d.ts

GrTreeSelectEmitsinterface
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

GrTreeSelectInstancetype
type GrTreeSelectInstance = ComponentExposed<typeof GrTreeSelectComponent>;

Доступность

Паттерн APG

combobox + tree

Клавиши

↓ / ↑ / Enter / SpaceEsc

КлавишаДействие
↓ / ↑ / Enter / Spaceоткрыть панель и перевести фокус в дерево (при filterable — сначала в поле поиска, оттуда в дерево ведёт ↓/↑)
Escзакрыть и вернуть фокус на триггер
—Tab из панели — закрыть; внутри дерева — клавиши GrTree

Использование из прозы библиотеки

Множественный выбор чекбоксами
vue
<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"] с этим именем (стандартная сериализация повторяющимся ключом), пустой выбор не отправляет ничего.