PlTreeSelect
A value chosen out of a hierarchy rather than out of a list. A PlTree behind a field, for a category, a folder, a region or an org chart node, the shapes a flat list flattens away.
import { PlTreeSelect, type PlTreeSelectNode } from 'plass-ui';
const items: PlTreeSelectNode[] = [
{
id: 'europe',
label: 'Europe',
children: [{ id: 'france', label: 'France' }]
},
{ id: 'antarctica', label: 'Antarctica' }
];
<PlTreeSelect items={items} label="Region" placeholder="Pick a region" />;import 'package:plass_ui/plass_ui.dart';
const List<PlTreeSelectNode> items = <PlTreeSelectNode>[
PlTreeSelectNode(
id: 'europe',
label: 'Europe',
children: <PlTreeSelectNode>[PlTreeSelectNode(id: 'france', label: 'France')],
),
PlTreeSelectNode(id: 'antarctica', label: 'Antarctica'),
];
PlTreeSelect(
items: items,
label: const Text('Region'),
value: chosen,
onValueChanged: (Set<String> next) => setState(() => chosen = next),
);The popup lifts itself out of the tree, so a tree select needs an Overlay above it, WidgetsApp with a navigator and MaterialApp both provide one.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items * | readonly PlTreeSelectNode[] | — | The whole tree, as data |
| value | readonly string[] | — | The ids of the chosen nodes. Use with onValueChange to control the picker |
| defaultValue | readonly string[] | — | What starts chosen |
| onValueChange | (value: string[]) => void | — | Called when the selection changes |
| multiple | boolean | false | Whether more than one node may be held at once |
| selectableBranches | boolean | false | Whether a node with children may itself be chosen. A node's own selectable overrides it either way |
| expanded | readonly string[] | — | The ids of the branches that are open. Use with onExpandedChange to control them |
| defaultExpanded | readonly string[] | — | The branches that start open |
| onExpandedChange | (expanded: string[]) => void | — | Called when a branch opens or closes |
| open | boolean | — | Whether the popup is open. Use with onOpenChange to control it |
| defaultOpen | boolean | false | Whether the popup starts open |
| onOpenChange | (open: boolean) => void | — | Called when the popup opens or closes |
| placeholder | ReactNode | — | Shown in the trigger while nothing is chosen |
| clearable | boolean | false | Offers the × that empties the control |
| closeOnSelect | boolean | !multiple | Closes the popup as soon as a node is chosen |
| searchable | boolean | false | Offers a field above the tree that filters it. A match keeps its ancestors |
| searchLabel | string | 'Search' | The word in the filter field |
| emptyLabel | string | 'Nothing here' | What the popup says when the filter matched nothing |
| format | (chosen: PlTreeSelectNode[]) => ReactNode | — | How the trigger writes what is held. Defaults to the labels, comma-joined |
| name | string | — | Identifies the field when a form is submitted. One hidden input per value |
| variantshared | 'solid' | 'glass' | 'ghost' | 'glass' | The material of the trigger, drawn on PlTextField's shell |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth of the trigger. The popup has its own, fixed at 3 |
| label | ReactNode | — | Label above the trigger |
| description | ReactNode | — | Helper text below the trigger |
| error | ReactNode | — | Error message below. Its presence also turns the control invalid |
| invalid | boolean | — | Forces the invalid state without a message |
| startIcon | ReactNode | — | The glyph before the value |
| fullWidth | boolean | false | Stretches to the width of the container |
| readOnly | boolean | false | The value is shown but cannot be changed, and the popup does not open |
| disabled | boolean | false | Unavailable |
| required | boolean | false | Whether a value must be chosen before the form is submitted |
| Prop | Type | Default | Description |
|---|---|---|---|
| items * | List<PlTreeSelectNode> | — | The whole tree, as data |
| value | Set<String> | {} | The ids of the chosen nodes. Controlled |
| onValueChanged | ValueChanged<Set<String>>? | — | Called with the whole selection after a node is chosen or dropped |
| multiple | bool | false | Whether more than one node may be held at once |
| selectableBranches | bool | false | Whether a node with children may itself be chosen. A node's own selectable overrides it either way |
| expanded | Set<String>? | — | The ids of the branches that are open. Leave it out and the picker holds its own |
| onExpandedChanged | ValueChanged<Set<String>>? | — | Called with the whole open set after a branch is opened or closed |
| open | bool? | — | Whether the popup is up. Leave it out and the picker holds its own |
| onOpenChanged | ValueChanged<bool>? | — | Called when the popup opens or closes |
| placeholder | Widget? | — | Shown in the trigger while nothing is chosen |
| clearable | bool | false | Offers the × that empties the control |
| closeOnSelect | bool? | !multiple | Closes the popup as soon as a node is chosen |
| searchable | bool | false | Offers a field above the tree that filters it. A match keeps its ancestors |
| searchLabel | String? | 'Search' | The word in the filter field |
| emptyLabel | String? | 'Nothing here' | What the popup says when the filter matched nothing |
| format | String Function(List<PlTreeSelectNode>)? | — | How the trigger writes what is held. Defaults to the labels, comma-joined |
| variantshared | PlassVariant | PlassVariant.glass | The material of the trigger, drawn on PlTextField's shell |
| sizeshared | PlassSize | PlassSize.md | Height and type scale |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.standard | Padding only — never the height, never the type scale |
| elevationshared | int | 0 | Drop shadow depth of the trigger. The popup has its own, fixed at 3 |
| label | Widget? | — | Label above the trigger |
| description | Widget? | — | Helper text below the trigger |
| error | Widget? | — | Error message below. Its presence also turns the control invalid |
| invalid | bool? | — | Forces the invalid state without a message |
| startIcon | Widget? | — | The glyph before the value |
| fullWidth | bool | false | Stretches to the width of the container |
| readOnly | bool | false | The value is shown but cannot be changed, and the popup does not open |
| disabled | bool | false | Unavailable |
| semanticLabel | String? | — | The name a screen reader gives the trigger |
| focusNode | FocusNode? | — | Drive focus from outside |
| autofocus | bool | false | Takes focus as it is inserted into the tree |
PlTreeSelectNode
| Prop | Type | Default | Description |
|---|---|---|---|
| id * | string | — | What identifies it, unique across the whole tree |
| label * | ReactNode | — | What the row says |
| searchLabel | string | — | What a search matches against. Falls back to the label when that is a string, and to the id when it is not |
| icon | ReactNode | — | A glyph before the label |
| children | readonly PlTreeSelectNode[] | — | Its own children. An empty array is a **branch** with nothing in it; undefined is a **leaf** |
| selectable | boolean | — | Whether this node may itself be chosen. Defaults to true for a leaf and to selectableBranches for a branch |
| disabled | boolean | — | In the tree but not selectable, and not a stop for the arrow keys |
| Prop | Type | Default | Description |
|---|---|---|---|
| id * | String | — | What identifies it, unique across the whole tree |
| label * | String | — | What the row says, what the filter matches, and what the trigger writes. A String rather than a widget, unlike the React build |
| icon | Widget? | — | A glyph before the label |
| children | List<PlTreeSelectNode>? | — | Its own children. An empty list is a **branch** with nothing in it; null is a **leaf** |
| selectable | bool? | — | Whether this node may itself be chosen. Defaults to true for a leaf and to selectableBranches for a branch |
| disabled | bool | false | In the tree but not selectable, and not a stop for the arrow keys |
Every native <div> attribute passes straight through to the field wrapper. color is excluded because it collides with the color in the table above, defaultValue because the picker spells it as a list of ids rather than as a DOM attribute, and children because the tree is items.
A className lands on the stack that holds the label, the control and the two lines under it. classNames reaches the four parts inside it: label, control, description and error.
value is a Set<String> and it is controlled. There is no uncontrolled form, which is the package's rule for every input in it. expanded and open are the two exceptions: leave either out and the picker keeps it itself.
A node's label is a String here and a ReactNode in React, which is the divergence PlTransferItem already carries. The filter reads the label, the trigger writes it and a screen reader is handed it, so text is what keeps every node searchable by construction. That is also why there is no searchLabel on this side: the label is already the words.
What the shared axes mean across the library is in prop conventions.
Examples
searchable
A field above the tree that filters it. A match keeps its ancestors, a "Seoul" under nothing at all does not say which taxonomy it came out of, and every branch the filter kept is opened, since a match folded inside a shut parent is a match nobody was shown.
Clearing the field hands the folds back to the reader: the branches they had opened are still open, and the ones the filter opened are shut again.
The fold is accent- and case-insensitive, so jose finds José.
The fold is case-insensitive. It does not strip accents, because Dart's core has no String.normalize and this package has no dependencies. The React build folds accents as well.
selectableBranches
Off by default, which is the shape most of these trees have: the branches are the taxonomy and the leaves are the answers. A branch that cannot be chosen still opens and closes. Pressing it is how you get at what is under it.
A node's own selectable overrides it either way, so a "Home" that is a real category can be chosen while the rest of the branches stay roads.
multiple
Every node a press adds is kept, and the trigger writes them comma-joined. The popup stays open, because a picker that shut after the first of several answers would have to be reopened for each of the rest.
format takes the chosen nodes and writes them however you like, for a trigger that stops growing with its answer.
<PlTreeSelect items={items} multiple format={(chosen) => chosen.length + ' regions'} />Controlled
The value, the folds and the popup are three separate questions, and each has its own pair.
<PlTreeSelect
items={items}
value={chosen}
onValueChange={setChosen}
expanded={open}
onExpandedChange={setOpen}
/>Opening a folder is not choosing it, which is why the second pair exists at all.
In a form
name puts one <input type="hidden"> on the page per held id, so a multiple picker submits as a repeated field.
<PlTreeSelect items={items} multiple name="region" defaultValue={['france', 'spain']} />Accessibility
- The trigger is a button, exactly as every other picker's is, and it carries the label, the description, the error and
aria-invalid. - What is inside the popup is a real
PlTree,role="tree"ofrole="treeitem"s,aria-level,aria-expanded,aria-selected, and one tab stop for the whole thing. - ↓ and ↑ walk the rows that are visible, → opens a branch and steps into it, ← closes it or steps out, and Enter or Space chooses.
- A node that cannot be chosen is not marked
aria-disabledwhen it is only a branch: it is still an operable row, because pressing it opens what is under it. Adisablednode is marked, and is not a stop for the arrow keys. - The filter field names itself with
searchLabel, so it is announced without a visible label above it.