GrTreeSelect

  • core
  • forms
  • API · 41 props
  • ⌨ keys

Selection of a value from a tree structure in a single control.

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

Overview

When to take it

  • the options are nested — categories, an org structure, regions, the sections of a catalogue: a flat list would lose the levels;
  • the path to a node matters — the selection is shown together with the branches rather than as a single leaf;
  • several nodes are selected — with or without taking the parents into account (checkStrictly);
  • the tree is large — filtering by typing and virtualisation arrive from GrTree.

When to take something else

NeedTake
The options are flatGrSelect
There are many options and they are searched by typingGrAutocomplete
The tree is needed on the screen rather than in a panelGrTree
There is only one level, but with group headingsGrSelect

Variants every tone and view, rendered by the real component

Variants · GrTreeSelect
state \ shapeboxpill
default
success
warning
danger

Sizes

  • xs
  • sm
  • md
  • lg

States

  • normal
  • disabled
  • loading
  • readonly
  • invalid

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • The account an expense is booked to

    «Hotels» alone is ambiguous, «Travel / Hotels» is not: value-display="path" shows the branch in the field. filterable with filter-node-method finds an account by its code as well as by name, and the node slot puts the code on the right of each row. A group is not a bookable account, so the form says so through invalid.

    Hotel Adlon · Berlin2 nights · receipt attached€412.80
    Travel
    Flights6410
    Hotels6420
    Taxis and trains6430
  • Where a shipping rate applies

    multiple with show-checkbox: checking «Nordics» covers its four countries, and a region with some of them is half-checked. tags shows the choice as chips, max-tag-count folds the rest into «+N» so the field keeps its height, and name posts one hidden input per key. The value holds parents too, so the summary counts the countries itself.

    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
  • A manager from the org chart

    The chart is fetched on the first update:open, and until it arrives loading swaps «No data» for the loading slot. A hundred and fifty rows stay light with virtual inside dropdown-max-height. The node slot adds a title next to each name, the value slot puts a face in the field, and a profile synced from HR is readonly.

    NLNora LindqvistBackend engineer · joined in March

    Search by name or browse by team.

    AOAmara OkaforBilling lead
    Engineering
    Billing
    Amara OkaforBilling lead

API generated from the package · cannot drift

Props

PropTypedefaultDescription
modelValuerequiredv-modelGrTreeSelectModelValue—The selected key or `null`; with `multiple` — an array of keys (`v-model`).
datarequiredT[]—The top-level nodes of the tree; the children lie in the field from `props.children`.
propsGrTreePropsMap | undefined{ children: "children", label: "label", }The names of the node fields: the children and the label. `children` and `label` by default.
nodeKey"id" | NodeKeyProp<T> | undefined"id" as anyThe key field of a node — its values go into the model. `id` by default; without a value the position of the node in the tree becomes the key.
defaultExpandedKeysGrTreeKey[] | undefined[]The keys of the nodes expanded in the panel tree from the very start.
disabledboolean | undefinedfalseUnavailable: the panel does not open, the value does not change. It adds up by "or" with the field and the form.
placeholderstring | undefinedundefinedThe text of the trigger while nothing is selected.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe height of the trigger and the type size by the controls scale; the tree in the panel takes the same step. `md` by default.
shape"box" | "pill" | undefinedundefinedThe shape of the border. `box` — the rounding of the controls scale; `pill` — a pill.
loadingboolean | undefinedfalseThe data is still on its way. The panel shows an indicator instead of "No data" — otherwise an empty answer and one not loaded look the same.
invalidboolean | undefinedfalseThe visual and ARIA state of an error; stronger than `state`. It adds up by "or" with `GrFormField`.
readonlyboolean | undefinedfalseRead only: the value is visible but does not change.
requiredboolean | undefinedfalseA mandatory field (`aria-required`).
ariaLabelstring | undefinedundefinedThe accessible name outside `GrFormField`.

Slots

SlotTypeDescription
value
{ value: GrTreeSelectModelValue; labels: string[]; displayVa{ value: GrTreeSelectModelValue; labels: string[]; displayValue: string; pathLabels?: string[] | undefined; }
The rendering of the value inside the trigger (instead of the default text).
node{ node: GrTreeNode<T>; data: T; selected: boolean; }The rendering of a row of the tree.
emptyanyThe content of the empty state (when there is no data).
loadinganyThe content of the panel while the data is on its way.
prefixanyAn addon to the left of the value: an icon, a currency code, a mark.
suffixanyAn addon to the right of the value, before the cross and the chevron.

Events

EventSignatureDescription
update:modelValue(e: "update:modelValue", value: GrTreeSelectModelValue): voidThe new value for `v-model`: a key, an array of keys with `multiple`, or `null` after clearing.
change(e: "change", value: GrTreeSelectModelValue): voidThe value has changed — the same payload as in `update:modelValue`.
update:open(e: "update:open", value: boolean): voidThe panel has opened or closed (`v-model:open`).
clear(e: "clear"): voidThe value has been removed by the cross; only with `clearable`.
nodeClick(e: "nodeClick", data: T, node: GrTreeNode<T>): voidA click on a node in the panel: its data and the tree node. Arrives before the value changes.
focus(e: "focus", event: FocusEvent): voidFocus entered the control. Moving between the trigger and the panel does not count as an event.
blur(e: "blur", event: FocusEvent): voidFocus left the control — the trigger and the panel together.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrTreeSelectModelValue, GrTreeSelectValueDisplay, GrTreeSelectProps } from '@feugene/granularity/components/GrTreeSelect'

Filedist/types/components/GrTreeSelect/grTreeSelectTypes.d.ts

GrTreeSelectModelValuetype
type GrTreeSelectModelValue = GrTreeKey | GrTreeKey[] | null;
GrTreeSelectValueDisplaytype
type GrTreeSelectValueDisplay = 'label' | 'path';
GrTreeSelectPropsinterface
interface GrTreeSelectProps<T extends object = any> {
    modelValue: GrTreeSelectModelValue;
    data: T[];
    props?: GrTreePropsMap;
    nodeKey?: NodeKeyProp<T> | 'id';
    defaultExpandedKeys?: GrTreeKey[];
    disabled?: boolean;
    placeholder?: string;
    size?: GrInputSize;
    shape?: GrControlShape;
    loading?: boolean;
    invalid?: boolean;
    readonly?: boolean;
    required?: boolean;
    ariaLabel?: string;
    state?: GrTreeSelectState;
    multiple?: boolean;
    showCheckbox?: boolean;
    checkStrictly?: boolean;
    tags?: boolean;
    maxTagCount?: number;
    tagTone?: GrBadgeTone;
    tagDark?: boolean;
    tagSize?: GrBadgeSize;
    tagRadius?: GrBadgeRadius;
    clearable?: boolean;
    open?: boolean;
    name?: string;
    valueDisplay?: GrTreeSelectValueDisplay;
    filterable?: boolean;
    filterPlaceholder?: string;
    filterInputmode?: InputHTMLAttributes['inputmode'];
    filterNodeMethod?: GrTreeFilterNodeMethod<T>;
    closeOnSelect?: boolean;
    dropdownMaxHeight?: number;
    virtual?: boolean;
    prefixMinWidth?: string;
    prefixMaxWidth?: string;
    suffixMinWidth?: string;
    suffixMaxWidth?: string;
    prefixFixed?: boolean;
    suffixFixed?: boolean;
}

Filedist/types/components/GrTreeSelect/config.d.ts

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

Filedist/types/components/GrTreeSelect/defaults.d.ts

GrTreeSelectConfigurablePropsinterface
interface GrTreeSelectConfigurableProps {
    size: GrComponentSize;
    shape: GrControlShape;
}

Filedist/types/components/GrTreeSelect/safelist.d.ts

grTreeSelectSafelistconst
const grTreeSelectSafelist: string[];

Filedist/types/components/GrTreeSelect/GrTreeSelect.vue.d.ts

GrTreeSelectEmitsinterface
interface GrTreeSelectEmits<T extends Record<string, any> = any> {
    (e: 'update:modelValue', value: GrTreeSelectModelValue): void;
    (e: 'change', value: GrTreeSelectModelValue): void;
    (e: 'update:open', value: boolean): void;
    (e: 'clear'): void;
    (e: 'nodeClick', data: T, node: GrTreeNode<T>): void;
    (e: 'focus', event: FocusEvent): void;
    (e: 'blur', event: FocusEvent): void;
}

Filedist/types/components/GrTreeSelect/index.d.ts

GrTreeSelectInstancetype
type GrTreeSelectInstance = ComponentExposed<typeof GrTreeSelectComponent>;

Accessibility

APG pattern

combobox + tree

Keys

↓ / ↑ / Enter / SpaceEsc

Usage from the library prose

Multiple selection with checkboxes
vue
<GrTreeSelect v-model="areas" :data="tree" multiple show-checkbox />

show-checkbox switches on the checkboxes of GrTree instead of a tick of its own: ticking a parent covers the subtree in a cascade, and a partly ticked parent is announced aria-checked="mixed". It works only together with multiple — in a single selection the current node is highlighted anyway.

The cascade is computed by the tree itself rather than by the select: a click on a row, a click on the square and Space go the same path, so there is no double toggling. check-strictly unties the parent from the children.

All of the ticked keys, the parent ones included, land in modelValue. If only the leaves are needed, switch check-strictly on and tick them yourself, or filter the value from the outside.

The keyboard: from the trigger into the tree

↓, ↑, Enter and Space on the trigger open the panel and move the focus into the tree — from there all of the keys of GrTree work. The panel is teleported into body, so Tab does not lead there and there is no other way inside.

With filterable the focus first goes into the search field (typing the filter is the first thing expected), and ↓/↑ from there lead into the tree. Tab out of the panel closes it, and Escape closes it and returns the focus to the trigger by the means of the shared layer stack.

The search is an auxiliary field of the panel rather than the control of a form field: the context of GrFormField (id, hint, error, required, invalid) goes to the trigger, while the search is named by its own filterPlaceholder and shows it while empty — under focus as well. The tree rows in the panel sit on the step of a control of the same size.

ARIA

The trigger is a role="combobox" with aria-haspopup="tree" and an aria-controls on the tree while the panel is open. Referring to a tree that is not in the DOM (empty data, loading) is not allowed — in those states aria-controls is not output.

Inside GrFormField the control takes id, aria-describedby, aria-invalid and aria-required from the context; outside a field the accessible name is given by ariaLabel.

The states

state="success" and "warning" show an icon and a hidden label that goes into the aria-describedby of the control: the colour of the border as the only carrier of meaning is WCAG 1.4.1, and these two states have no ARIA equivalent of aria-invalid. danger gets no icon — its meaning is already carried by aria-invalid and the error text from GrFormField — and the mark goes out as soon as the control is declared invalid.

disabled is coloured with a background and a text colour rather than with transparency: opacity dilutes tokens tuned to AA and drops the contrast. readonly leaves the value visible but removes both the clear button and the opening of the panel.

loading shows an indicator instead of “No data”: an empty answer and data that has not loaded must not look the same. The markup can be replaced with the #loading slot.

The size

size is read through useGrComponentSize(), so both GrConfigProvider and a pointed componentDefaults.GrTreeSelect apply. The same size travels into the tree inside the panel — otherwise the control and its list would be set in different type sizes.

The display of the value

valueDisplay="path" in the single mode shows the path through / instead of a single label. With multiple the “first label +N” stays in the trigger by default; the full list is given to the #value slot — that is where any markup of your own is assembled.

Chips of the selected nodes

tags (only together with multiple) shows the selection as chips instead of the “a, b, c” line. The cross on a chip removes its node without opening the panel. max-tag-count keeps the row to a single line: the remainder folds into “+N”. The look is configured with the same tagTone/tagDark/tagSize/tagRadius as in GrSelect — moving from one component to the other, the consumer does not relearn.

The height of the trigger does not follow the number of chips: without max-tag-count a long set goes past the edge. That is precisely its purpose, not an omission — a growing field would move the whole form under it on every selection.

The chips lie beside the trigger, not inside it: role="combobox" declares its descendants presentational, and a cross inside would be unreachable from the keyboard (axe: nested-interactive). The strip of chips is transparent to the pointer, while each chip is not: a click past a chip opens the panel, like any other place on the trigger.

In the show-checkbox mode removing a chip goes through the tree rather than by striking the key out: the cascade over parents and children is computed by GrTree, and removing a key past it could produce a ticked parent without a single ticked child.

The prefix / suffix addons

The slots put an icon, a unit or a label into the shell; the width is bounded by six props (prefixMinWidth/prefixMaxWidth/prefixFixed and the same for the suffix). The shared contract of the controls — form-controls.md.

The height of the panel

dropdownMaxHeight is the ceiling of the list, not of the panel. The panel itself is bounded by the room left to the edge of the viewport: the layer reports the measurement in --gr-floating-available-height, and on a short screen it is precisely the list that shrinks — the filter field and the state rows stay in view, and there is a single scrollbar. The mechanism is shared by the list panels of the package and is described in ../overlays.md.

Panel control and the native form

The panel is controllable through v-model:open (the shared contract of panel overlays, as in GrPopover): without the open prop the behaviour is uncontrolled, with it the parent owns the state.

The name prop enables participation in a native form: for every selected key an input[type="hidden"] with that name is rendered (the standard serialisation with a repeated key), and an empty selection sends nothing.