GrTreeSelect
Selection of a value from a tree structure in a single control.
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
| Need | Take |
|---|---|
| The options are flat | GrSelect |
| There are many options and they are searched by typing | GrAutocomplete |
| The tree is needed on the screen rather than in a panel | GrTree |
| There is only one level, but with group headings | GrSelect |
Variants every tone and view, rendered by the real component
state \ shape | box | pill |
|---|---|---|
default | ||
success | ||
warning | ||
danger |
Sizes
xssmmdlg
States
normaldisabledloadingreadonlyinvalid
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
Loading…
0 changed· state is in the URL
<GrTreeSelect
:data="data"
v-model="value"
:defaultExpandedKeys="defaultExpandedKeys"
/>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.filterablewithfilter-node-methodfinds an account by its code as well as by name, and thenodeslot puts the code on the right of each row. A group is not a bookable account, so the form says so throughinvalid.Hotel Adlon · Berlin2 nights · receipt attached€412.80Where a shipping rate applies
multiplewithshow-checkbox: checking «Nordics» covers its four countries, and a region with some of them is half-checked.tagsshows the choice as chips,max-tag-countfolds the rest into «+N» so the field keeps its height, andnameposts 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 countriesCheck a region to cover every country in it.
EU · WestGermanyFrance +3A manager from the org chart
The chart is fetched on the first
update:open, and until it arrivesloadingswaps «No data» for theloadingslot. A hundred and fifty rows stay light withvirtualinsidedropdown-max-height. Thenodeslot adds a title next to each name, thevalueslot puts a face in the field, and a profile synced from HR isreadonly.NLNora LindqvistBackend engineer · joined in MarchSearch by name or browse by team.
AOAmara OkaforBilling lead
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
modelValuerequiredv-model | GrTreeSelectModelValue | — | The selected key or `null`; with `multiple` — an array of keys (`v-model`). |
datarequired | T[] | — | The top-level nodes of the tree; the children lie in the field from `props.children`. |
props | GrTreePropsMap | 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 any | The 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. |
defaultExpandedKeys | GrTreeKey[] | undefined | [] | The keys of the nodes expanded in the panel tree from the very start. |
disabled | boolean | undefined | false | Unavailable: the panel does not open, the value does not change. It adds up by "or" with the field and the form. |
placeholder | string | undefined | undefined | The text of the trigger while nothing is selected. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The 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" | undefined | undefined | The shape of the border. `box` — the rounding of the controls scale; `pill` — a pill. |
loading | boolean | undefined | false | The 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. |
invalid | boolean | undefined | false | The visual and ARIA state of an error; stronger than `state`. It adds up by "or" with `GrFormField`. |
readonly | boolean | undefined | false | Read only: the value is visible but does not change. |
required | boolean | undefined | false | A mandatory field (`aria-required`). |
ariaLabel | string | undefined | undefined | The accessible name outside `GrFormField`. |
state | "default" | "success" | "warning" | "danger" | undefined | "default" | The highlight of the border at the developer's choice. `success` and `warning` add an icon and a hidden label; `invalid` is stronger. `default` by default. |
multiple | boolean | undefined | false | Multiple selection: the model is an array of keys, and a click on a node toggles it. |
showCheckbox | boolean | undefined | false | Checkboxes in the tree instead of a tick of its own: checking a parent closes the subtree in a cascade, and a half-chosen parent is shown as `mixed`. It works only together with `multiple`. |
checkStrictly | boolean | undefined | false | Untie the parents from the children: the check stops spreading in a cascade. |
tags | boolean | undefined | false | Chips of the selected nodes in the trigger instead of the string "a, b, c". Works only together with `multiple`: with a single selection a chip would be a plate for one value. |
maxTagCount | number | undefined | undefined | How many chips to show before the rest is folded into "+N". Without it the whole set grows, and the height of the trigger does not follow it — the extra chips go past the edge. |
tagTone |
| "neutral" | The look of the chips. A chip is a `GrBadge`: a plate of our own would be nearly indistinguishable from the background of the field on the light theme. The names and scales are the same as in `GrSelect` — moving from one component to the other, the consumer does not relearn. |
tagDark | boolean | undefined | false | A dense fill of the chips in the `tags` mode. |
tagSize | "xs" | "sm" | "md" | "lg" | undefined | "sm" | The size of the chips by the `GrBadge` scale. `sm` by default. |
tagRadius | GrBadgeRadius | undefined | "round" | The rounding of the chips: `square`, `semi` or `round`. `round` by default. |
clearable | boolean | undefined | false | A clear cross in place of the chevron when something is selected. `false` by default. |
openv-model | boolean | undefined | undefined | The controlled state of the panel (`v-model:open`). Without the prop the panel behaves on its own (uncontrolled), with it — listen to `update:open` and change the prop. |
name | string | undefined | undefined | The name for a native form: a hidden input for every chosen key. |
valueDisplay | GrTreeSelectValueDisplay | undefined | "label" | How to display the chosen value in the single mode. |
filterable | boolean | undefined | false | A search field above the tree; without `filterNodeMethod` the nodes are searched by a substring of the label. |
filterPlaceholder | string | undefined | undefined | The hint in the search field. By default — the translated "Search…". |
filterInputmode |
| undefined | The `inputmode` of the search field — which on-screen keyboard to show on mobile devices. |
filterNodeMethod | GrTreeFilterNodeMethod<T> | undefined | undefined | A rule of your own for matching a node against the query instead of the substring search by the label. |
closeOnSelect | boolean | undefined | undefined | Close the panel after a node is selected. By default — only with a single selection. |
dropdownMaxHeight | number | undefined | 320 | The maximum height of the panel with the tree, px. `320` by default. |
virtual | boolean | undefined | false | Virtualisation of the tree in the panel: only a window around the viewport lives in the DOM. The tree itself becomes the scroller in this mode rather than the container of the panel — two nested scrollers would give two scrollbars on one list. |
prefixMinWidth | string | undefined | undefined | The widths of the `prefix`/`suffix` addons — the common contract of the controls of the package (`docs/form-controls.md`). |
prefixMaxWidth | string | undefined | undefined | The maximum width of the prefix: wider content is clipped. |
suffixMinWidth | string | undefined | undefined | The minimum width of the suffix. By default — by the size step. |
suffixMaxWidth | string | undefined | undefined | The maximum width of the suffix: wider content is clipped. |
prefixFixed | boolean | undefined | false | A fixed width of the prefix: `prefixMaxWidth`, otherwise `prefixMinWidth`, otherwise by the size step. |
suffixFixed | boolean | undefined | false | A fixed width of the suffix: `suffixMaxWidth`, otherwise `suffixMinWidth`, otherwise by the size step. |
Slots
| Slot | Type | Description |
|---|---|---|
value |
| 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. |
empty | any | The content of the empty state (when there is no data). |
loading | any | The content of the panel while the data is on its way. |
prefix | any | An addon to the left of the value: an icon, a currency code, a mark. |
suffix | any | An addon to the right of the value, before the cross and the chevron. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (e: "update:modelValue", value: GrTreeSelectModelValue): void | The new value for `v-model`: a key, an array of keys with `multiple`, or `null` after clearing. |
change | (e: "change", value: GrTreeSelectModelValue): void | The value has changed — the same payload as in `update:modelValue`. |
update:open | (e: "update:open", value: boolean): void | The panel has opened or closed (`v-model:open`). |
clear | (e: "clear"): void | The value has been removed by the cross; only with `clearable`. |
nodeClick | (e: "nodeClick", data: T, node: GrTreeNode<T>): void | A click on a node in the panel: its data and the tree node. Arrives before the value changes. |
focus | (e: "focus", event: FocusEvent): void | Focus entered the control. Moving between the trigger and the panel does not count as an event. |
blur | (e: "blur", event: FocusEvent): void | Focus 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 type { GrTreeSelectModelValue, GrTreeSelectValueDisplay, GrTreeSelectProps } from '@feugene/granularity/components/GrTreeSelect'Filedist/types/components/GrTreeSelect/grTreeSelectTypes.d.ts
type GrTreeSelectModelValue = GrTreeKey | GrTreeKey[] | null;type GrTreeSelectValueDisplay = 'label' | 'path';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
const grTreeSelectConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTreeSelect">;Filedist/types/components/GrTreeSelect/defaults.d.ts
interface GrTreeSelectConfigurableProps {
size: GrComponentSize;
shape: GrControlShape;
}Filedist/types/components/GrTreeSelect/safelist.d.ts
const grTreeSelectSafelist: string[];Filedist/types/components/GrTreeSelect/GrTreeSelect.vue.d.ts
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
type GrTreeSelectInstance = ComponentExposed<typeof GrTreeSelectComponent>;Accessibility
APG pattern
combobox + tree
Keys
↓ / ↑ / Enter / SpaceEsc
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
Multiple selection with checkboxes
<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.
Props in this example