@ui-sdk/vue 0.0.0-stage → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +10 -2
- package/dist/accordion.d.ts +561 -0
- package/dist/accordion.js +24 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +7 -0
- package/dist/alert-dialog.d.ts +231 -0
- package/dist/alert-dialog.js +33 -0
- package/dist/async-options.d.ts +18 -0
- package/dist/async-options.js +40 -0
- package/dist/autocomplete.d.ts +384 -0
- package/dist/autocomplete.js +344 -0
- package/dist/avatar.d.ts +40 -0
- package/dist/avatar.js +28 -0
- package/dist/button.d.ts +52 -0
- package/dist/button.js +43 -0
- package/dist/calendar-context.d.ts +10 -0
- package/dist/calendar-context.js +7 -0
- package/dist/calendar.d.ts +311 -0
- package/dist/calendar.js +349 -0
- package/dist/carousel.d.ts +236 -0
- package/dist/carousel.js +204 -0
- package/dist/checkbox.d.ts +211 -0
- package/dist/checkbox.js +151 -0
- package/dist/children.d.ts +41 -0
- package/dist/children.js +135 -0
- package/dist/collapsible.d.ts +54 -0
- package/dist/collapsible.js +18 -0
- package/dist/color-picker.d.ts +187 -0
- package/dist/color-picker.js +158 -0
- package/dist/color.d.ts +2 -0
- package/dist/color.js +21 -0
- package/dist/combobox.d.ts +517 -0
- package/dist/combobox.js +531 -0
- package/dist/command.d.ts +322 -0
- package/dist/command.js +345 -0
- package/dist/controller-target.d.ts +49 -0
- package/dist/controller-target.js +112 -0
- package/dist/create-parts.d.ts +126 -0
- package/dist/create-parts.js +206 -0
- package/dist/date-input.d.ts +133 -0
- package/dist/date-input.js +31 -0
- package/dist/date-picker.d.ts +328 -0
- package/dist/date-picker.js +354 -0
- package/dist/date.d.ts +2 -0
- package/dist/date.js +59 -0
- package/dist/dialog-parts.d.ts +259 -0
- package/dist/dialog-parts.js +300 -0
- package/dist/dialog.d.ts +251 -0
- package/dist/dialog.js +25 -0
- package/dist/drawer.d.ts +304 -0
- package/dist/drawer.js +373 -0
- package/dist/editor-context.d.ts +4 -0
- package/dist/editor-context.js +5 -0
- package/dist/field-context.d.ts +26 -0
- package/dist/field-context.js +44 -0
- package/dist/field.d.ts +154 -0
- package/dist/field.js +240 -0
- package/dist/file-input.d.ts +212 -0
- package/dist/file-input.js +255 -0
- package/dist/form-inputs.d.ts +16 -0
- package/dist/form-inputs.js +25 -0
- package/dist/gesture.d.ts +1 -0
- package/dist/gesture.js +15 -0
- package/dist/history.d.ts +2 -0
- package/dist/history.js +11 -0
- package/dist/hover-card.d.ts +137 -0
- package/dist/hover-card.js +153 -0
- package/dist/image-cropper.d.ts +74 -0
- package/dist/image-cropper.js +36 -0
- package/dist/image.d.ts +2 -0
- package/dist/image.js +5 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +60 -0
- package/dist/input.d.ts +120 -0
- package/dist/input.js +73 -0
- package/dist/item-icon.d.ts +34 -0
- package/dist/item-icon.js +63 -0
- package/dist/label.d.ts +16 -0
- package/dist/label.js +8 -0
- package/dist/lifecycle.d.ts +11 -0
- package/dist/lifecycle.js +91 -0
- package/dist/list-items.d.ts +38 -0
- package/dist/list-items.js +59 -0
- package/dist/list-text.d.ts +7 -0
- package/dist/list-text.js +15 -0
- package/dist/listbox.d.ts +273 -0
- package/dist/listbox.js +126 -0
- package/dist/menu.d.ts +678 -0
- package/dist/menu.js +607 -0
- package/dist/menubar-context.d.ts +5 -0
- package/dist/menubar-context.js +5 -0
- package/dist/menubar.d.ts +61 -0
- package/dist/menubar.js +48 -0
- package/dist/meter.d.ts +41 -0
- package/dist/meter.js +37 -0
- package/dist/navigation-menu.d.ts +228 -0
- package/dist/navigation-menu.js +195 -0
- package/dist/number-input.d.ts +176 -0
- package/dist/number-input.js +231 -0
- package/dist/number.d.ts +2 -0
- package/dist/number.js +39 -0
- package/dist/nuxt/entries.json +977 -0
- package/dist/nuxt/module.d.ts +18 -0
- package/dist/nuxt/module.js +31 -0
- package/dist/nuxt/registrations.d.ts +22 -0
- package/dist/nuxt/registrations.js +22 -0
- package/dist/observe.d.ts +2 -0
- package/dist/observe.js +7 -0
- package/dist/otp-input.d.ts +142 -0
- package/dist/otp-input.js +130 -0
- package/dist/overlays.d.ts +40 -0
- package/dist/overlays.js +118 -0
- package/dist/pagination.d.ts +201 -0
- package/dist/pagination.js +146 -0
- package/dist/password-input.d.ts +144 -0
- package/dist/password-input.js +134 -0
- package/dist/popover.d.ts +211 -0
- package/dist/popover.js +258 -0
- package/dist/popup.d.ts +60 -0
- package/dist/popup.js +89 -0
- package/dist/preferences.d.ts +44 -0
- package/dist/preferences.js +102 -0
- package/dist/primitive.d.ts +2 -0
- package/dist/primitive.js +11 -0
- package/dist/progress.d.ts +41 -0
- package/dist/progress.js +37 -0
- package/dist/radio.d.ts +164 -0
- package/dist/radio.js +145 -0
- package/dist/rating.d.ts +149 -0
- package/dist/rating.js +146 -0
- package/dist/renderless.d.ts +4 -0
- package/dist/renderless.js +28 -0
- package/dist/resizable.d.ts +54 -0
- package/dist/resizable.js +22 -0
- package/dist/scroll-area.d.ts +72 -0
- package/dist/scroll-area.js +77 -0
- package/dist/segmented-control.d.ts +151 -0
- package/dist/segmented-control.js +110 -0
- package/dist/segmented-input.d.ts +76 -0
- package/dist/segmented-input.js +115 -0
- package/dist/select.d.ts +458 -0
- package/dist/select.js +468 -0
- package/dist/separator.d.ts +14 -0
- package/dist/separator.js +8 -0
- package/dist/shortcuts.d.ts +32 -0
- package/dist/shortcuts.js +56 -0
- package/dist/slider.d.ts +65 -0
- package/dist/slider.js +194 -0
- package/dist/switch.d.ts +103 -0
- package/dist/switch.js +71 -0
- package/dist/tabs.d.ts +338 -0
- package/dist/tabs.js +27 -0
- package/dist/text-control.d.ts +89 -0
- package/dist/text-control.js +51 -0
- package/dist/text-match.d.ts +2 -0
- package/dist/text-match.js +7 -0
- package/dist/textarea.d.ts +106 -0
- package/dist/textarea.js +68 -0
- package/dist/time-input.d.ts +131 -0
- package/dist/time-input.js +32 -0
- package/dist/timeline.d.ts +81 -0
- package/dist/timeline.js +30 -0
- package/dist/toast-parts.d.ts +203 -0
- package/dist/toast-parts.js +270 -0
- package/dist/toast.d.ts +2 -0
- package/dist/toast.js +21 -0
- package/dist/toggle-props.d.ts +22 -0
- package/dist/toggle-props.js +14 -0
- package/dist/toggle.d.ts +199 -0
- package/dist/toggle.js +129 -0
- package/dist/token-input.d.ts +136 -0
- package/dist/token-input.js +214 -0
- package/dist/toolbar-context.d.ts +7 -0
- package/dist/toolbar-context.js +20 -0
- package/dist/toolbar.d.ts +120 -0
- package/dist/toolbar.js +77 -0
- package/dist/tooltip.d.ts +160 -0
- package/dist/tooltip.js +224 -0
- package/dist/tree.d.ts +300 -0
- package/dist/tree.js +209 -0
- package/dist/trigger-menu.d.ts +452 -0
- package/dist/trigger-menu.js +254 -0
- package/dist/use-primitive.d.ts +132 -0
- package/dist/use-primitive.js +462 -0
- package/dist/waterfall.d.ts +201 -0
- package/dist/waterfall.js +171 -0
- package/docs/README.md +89 -0
- package/docs/agents.md +1523 -0
- package/docs/components/accordion.md +1369 -0
- package/docs/components/alert-dialog.md +799 -0
- package/docs/components/autocomplete.md +1857 -0
- package/docs/components/avatar.md +272 -0
- package/docs/components/button.md +435 -0
- package/docs/components/calendar.md +1589 -0
- package/docs/components/carousel.md +1171 -0
- package/docs/components/checkbox.md +1008 -0
- package/docs/components/collapsible.md +536 -0
- package/docs/components/color-picker.md +706 -0
- package/docs/components/combobox.md +3504 -0
- package/docs/components/command.md +1481 -0
- package/docs/components/date-input.md +536 -0
- package/docs/components/date-picker.md +1244 -0
- package/docs/components/dialog.md +1397 -0
- package/docs/components/drawer.md +1166 -0
- package/docs/components/field.md +538 -0
- package/docs/components/file-input.md +611 -0
- package/docs/components/hover-card.md +505 -0
- package/docs/components/image-cropper.md +556 -0
- package/docs/components/input.md +306 -0
- package/docs/components/label.md +203 -0
- package/docs/components/listbox.md +1004 -0
- package/docs/components/menu.md +1229 -0
- package/docs/components/menubar.md +443 -0
- package/docs/components/meter.md +315 -0
- package/docs/components/navigation-menu.md +1523 -0
- package/docs/components/number-input.md +1029 -0
- package/docs/components/otp-input.md +512 -0
- package/docs/components/pagination.md +613 -0
- package/docs/components/password-input.md +649 -0
- package/docs/components/popover.md +1448 -0
- package/docs/components/preferences.md +208 -0
- package/docs/components/progress.md +423 -0
- package/docs/components/radio.md +785 -0
- package/docs/components/rating.md +416 -0
- package/docs/components/resizable.md +910 -0
- package/docs/components/scroll-area.md +915 -0
- package/docs/components/segmented-control.md +435 -0
- package/docs/components/select.md +2015 -0
- package/docs/components/separator.md +229 -0
- package/docs/components/slider.md +1084 -0
- package/docs/components/switch.md +565 -0
- package/docs/components/tabs.md +944 -0
- package/docs/components/textarea.md +299 -0
- package/docs/components/time-input.md +224 -0
- package/docs/components/timeline.md +654 -0
- package/docs/components/toast.md +1224 -0
- package/docs/components/toggle.md +500 -0
- package/docs/components/token-input.md +953 -0
- package/docs/components/toolbar.md +739 -0
- package/docs/components/tooltip.md +818 -0
- package/docs/components/tree.md +1306 -0
- package/docs/components/trigger-menu.md +1223 -0
- package/docs/components/waterfall.md +717 -0
- package/docs/controllers.md +71 -0
- package/docs/core.md +8027 -0
- package/docs/custom-parts.md +89 -0
- package/docs/first-component.md +83 -0
- package/docs/forms.md +122 -0
- package/docs/installation.md +72 -0
- package/docs/introduction.md +32 -0
- package/docs/lists-and-menus.md +122 -0
- package/docs/llms.txt +1502 -0
- package/docs/parts-and-composition.md +85 -0
- package/docs/popups.md +108 -0
- package/docs/rendering.md +56 -0
- package/docs/server-rendering.md +180 -0
- package/docs/state-and-events.md +100 -0
- package/docs/styling-and-animation.md +148 -0
- package/docs/utilities/async-options.md +348 -0
- package/docs/utilities/shortcuts.md +159 -0
- package/package.json +352 -3
|
@@ -0,0 +1,1306 @@
|
|
|
1
|
+
# Tree
|
|
2
|
+
|
|
3
|
+
Browse a hierarchy of files, pages, or choices with expandable branches.
|
|
4
|
+
|
|
5
|
+
Use a Tree when items have children, like files in folders or pages in a docs sidebar. Start with nested parts, then add selection, checkboxes, or children loaded from a server as you need them.
|
|
6
|
+
|
|
7
|
+
```vue title="demo.vue"
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import {
|
|
10
|
+
ChevronRightIcon,
|
|
11
|
+
FileIcon,
|
|
12
|
+
FolderIcon,
|
|
13
|
+
FolderOpenIcon,
|
|
14
|
+
} from "@lucide/vue"
|
|
15
|
+
import {
|
|
16
|
+
Tree,
|
|
17
|
+
TreeGroup,
|
|
18
|
+
TreeItem,
|
|
19
|
+
TreeItemIndicator,
|
|
20
|
+
TreeItemLabel,
|
|
21
|
+
} from "@ui-sdk/vue/tree"
|
|
22
|
+
import { h, type FunctionalComponent } from "vue"
|
|
23
|
+
|
|
24
|
+
type Node = { value: string; label: string; children?: Node[] }
|
|
25
|
+
|
|
26
|
+
const files: Node[] = [
|
|
27
|
+
{
|
|
28
|
+
value: "app",
|
|
29
|
+
label: "app",
|
|
30
|
+
children: [
|
|
31
|
+
{ value: "app/layout.tsx", label: "layout.tsx" },
|
|
32
|
+
{ value: "app/page.tsx", label: "page.tsx" },
|
|
33
|
+
{
|
|
34
|
+
value: "app/settings",
|
|
35
|
+
label: "settings",
|
|
36
|
+
children: [{ value: "app/settings/page.tsx", label: "page.tsx" }],
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: "components",
|
|
42
|
+
label: "components",
|
|
43
|
+
children: [
|
|
44
|
+
{ value: "components/button.tsx", label: "button.tsx" },
|
|
45
|
+
{ value: "components/card.tsx", label: "card.tsx" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{ value: "components.json", label: "components.json" },
|
|
49
|
+
{ value: "package.json", label: "package.json" },
|
|
50
|
+
{ value: "README.md", label: "README.md" },
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const Items: FunctionalComponent<{ items: Node[] }> = ({ items }) =>
|
|
54
|
+
items.map(({ value, label, children }) =>
|
|
55
|
+
h(TreeItem, { key: value, value }, () => [
|
|
56
|
+
h(TreeItemLabel, {}, () => [
|
|
57
|
+
...(children
|
|
58
|
+
? [
|
|
59
|
+
h(TreeItemIndicator, {}, () =>
|
|
60
|
+
h(ChevronRightIcon, { "aria-hidden": "true" })
|
|
61
|
+
),
|
|
62
|
+
h(FolderIcon, { "aria-hidden": "true" }),
|
|
63
|
+
h(FolderOpenIcon, { "aria-hidden": "true" }),
|
|
64
|
+
]
|
|
65
|
+
: [h(FileIcon, { "aria-hidden": "true" })]),
|
|
66
|
+
h("span", {}, label),
|
|
67
|
+
]),
|
|
68
|
+
children && h(TreeGroup, {}, () => h(Items, { items: children })),
|
|
69
|
+
])
|
|
70
|
+
)
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<template>
|
|
74
|
+
<Tree
|
|
75
|
+
aria-label="Files"
|
|
76
|
+
:default-expanded="['app']"
|
|
77
|
+
default-value="app/page.tsx"
|
|
78
|
+
>
|
|
79
|
+
<Items :items="files" />
|
|
80
|
+
</Tree>
|
|
81
|
+
</template>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Expand components to see its files. Use the arrow keys to move between rows and open or close branches.
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm install @ui-sdk/vue @ui-sdk/core
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Composition
|
|
93
|
+
|
|
94
|
+
Use the following composition to build a `Tree`:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
Tree
|
|
98
|
+
├── TreeItem
|
|
99
|
+
│ ├── TreeItemLabel
|
|
100
|
+
│ │ └── TreeItemIndicator
|
|
101
|
+
│ └── TreeGroup
|
|
102
|
+
│ └── TreeItem
|
|
103
|
+
│ └── TreeItemLabel
|
|
104
|
+
└── TreeItem
|
|
105
|
+
└── TreeItemLabel
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Give the tree an accessible name and every item a unique `value`. An item with a `TreeGroup` inside it is a branch. The group holds its children; the label names the item. The indicator and checkbox are optional, and they belong to the item around them, so put them inside its label to make them part of the row.
|
|
109
|
+
|
|
110
|
+
## Usage
|
|
111
|
+
|
|
112
|
+
Import the parts and put them together:
|
|
113
|
+
|
|
114
|
+
```vue
|
|
115
|
+
<script setup lang="ts">
|
|
116
|
+
import {
|
|
117
|
+
Tree,
|
|
118
|
+
TreeGroup,
|
|
119
|
+
TreeItem,
|
|
120
|
+
TreeItemIndicator,
|
|
121
|
+
TreeItemLabel,
|
|
122
|
+
} from "@ui-sdk/vue/tree"
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<template>
|
|
126
|
+
<Tree aria-label="Project files">
|
|
127
|
+
<TreeItem value="src">
|
|
128
|
+
<TreeItemLabel>
|
|
129
|
+
<TreeItemIndicator>›</TreeItemIndicator>
|
|
130
|
+
src
|
|
131
|
+
</TreeItemLabel>
|
|
132
|
+
<TreeGroup>
|
|
133
|
+
<TreeItem value="src/index.ts">
|
|
134
|
+
<TreeItemLabel>index.ts</TreeItemLabel>
|
|
135
|
+
</TreeItem>
|
|
136
|
+
</TreeGroup>
|
|
137
|
+
</TreeItem>
|
|
138
|
+
<TreeItem value="package.json">
|
|
139
|
+
<TreeItemLabel>package.json</TreeItemLabel>
|
|
140
|
+
</TreeItem>
|
|
141
|
+
</Tree>
|
|
142
|
+
</template>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Items as Data
|
|
146
|
+
|
|
147
|
+
Pass `items` to give the tree its full hierarchy, including collapsed branches. Each `TreeItemData` has a `value`, a `label`, and optional `children`. With no children of its own, the tree renders bare parts from the data:
|
|
148
|
+
|
|
149
|
+
```vue
|
|
150
|
+
<script setup lang="ts">
|
|
151
|
+
import { Tree, type TreeItemData } from "@ui-sdk/vue/tree"
|
|
152
|
+
|
|
153
|
+
const items: TreeItemData[] = [
|
|
154
|
+
{
|
|
155
|
+
value: "fruit",
|
|
156
|
+
label: "Fruit",
|
|
157
|
+
children: [
|
|
158
|
+
{ value: "apple", label: "Apple" },
|
|
159
|
+
{ value: "banana", label: "Banana" },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{ value: "bread", label: "Bread" },
|
|
163
|
+
]
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<template>
|
|
167
|
+
<Tree aria-label="Groceries" :items="items" />
|
|
168
|
+
</template>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
To render the rows yourself, keep `items` on the tree and map them with a recursive component, so keyboard navigation, item counts, and controllers still know the whole hierarchy:
|
|
172
|
+
|
|
173
|
+
```vue title="Items.vue"
|
|
174
|
+
<script setup lang="ts">
|
|
175
|
+
import {
|
|
176
|
+
TreeGroup,
|
|
177
|
+
TreeItem,
|
|
178
|
+
TreeItemLabel,
|
|
179
|
+
type TreeItemData,
|
|
180
|
+
} from "@ui-sdk/vue/tree"
|
|
181
|
+
|
|
182
|
+
defineProps<{ items: TreeItemData[] }>()
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<template>
|
|
186
|
+
<TreeItem v-for="item in items" :key="item.value" :value="item.value">
|
|
187
|
+
<TreeItemLabel>{{ item.label }}</TreeItemLabel>
|
|
188
|
+
<TreeGroup v-if="item.children">
|
|
189
|
+
<Items :items="item.children" />
|
|
190
|
+
</TreeGroup>
|
|
191
|
+
</TreeItem>
|
|
192
|
+
</template>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```vue
|
|
196
|
+
<template>
|
|
197
|
+
<Tree aria-label="Groceries" :items="items">
|
|
198
|
+
<Items :items="items" />
|
|
199
|
+
</Tree>
|
|
200
|
+
</template>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## File Paths
|
|
204
|
+
|
|
205
|
+
Pass `paths` for a file explorer. Folders become branches and sort before files; a path ending in `/` represents an empty folder. `treeFromPaths()` returns the same data when you want to render the rows yourself.
|
|
206
|
+
|
|
207
|
+
Selection and activation are separate. This example uses `activation="automatic"` so arrow keys select a file, then `@action` opens it. Return a promise from the action handler to show a pending state while it opens.
|
|
208
|
+
|
|
209
|
+
```vue title="paths.vue"
|
|
210
|
+
<script setup lang="ts">
|
|
211
|
+
import { ChevronRightIcon, LoaderCircleIcon } from "@lucide/vue"
|
|
212
|
+
import {
|
|
213
|
+
Tree,
|
|
214
|
+
TreeGroup,
|
|
215
|
+
TreeItem,
|
|
216
|
+
TreeItemIndicator,
|
|
217
|
+
TreeItemLabel,
|
|
218
|
+
treeFromPaths,
|
|
219
|
+
type TreeItemData,
|
|
220
|
+
type TreeItemState,
|
|
221
|
+
} from "@ui-sdk/vue/tree"
|
|
222
|
+
import { h, type FunctionalComponent } from "vue"
|
|
223
|
+
import { useLog } from "./log"
|
|
224
|
+
|
|
225
|
+
const paths = [
|
|
226
|
+
"src/index.ts",
|
|
227
|
+
"src/lib/format.ts",
|
|
228
|
+
"src/lib/parse.ts",
|
|
229
|
+
"src/routes/home.tsx",
|
|
230
|
+
"src/routes/about.tsx",
|
|
231
|
+
"tests/",
|
|
232
|
+
"package.json",
|
|
233
|
+
"tsconfig.json",
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
const items = treeFromPaths(paths)
|
|
237
|
+
const files = new Set(paths.filter((path) => !path.endsWith("/")))
|
|
238
|
+
|
|
239
|
+
const Items: FunctionalComponent<{ items: TreeItemData[] }> = ({ items }) =>
|
|
240
|
+
items.map(({ value, label, children }) =>
|
|
241
|
+
h(TreeItem, { key: value, value }, () => [
|
|
242
|
+
h(
|
|
243
|
+
TreeItemLabel,
|
|
244
|
+
{},
|
|
245
|
+
{
|
|
246
|
+
default: ({ state }: { state: TreeItemState }) => [
|
|
247
|
+
children &&
|
|
248
|
+
h(TreeItemIndicator, {}, () =>
|
|
249
|
+
h(ChevronRightIcon, { "aria-hidden": "true" })
|
|
250
|
+
),
|
|
251
|
+
h("span", {}, label),
|
|
252
|
+
state.pending && h(LoaderCircleIcon, { "aria-hidden": "true" }),
|
|
253
|
+
],
|
|
254
|
+
}
|
|
255
|
+
),
|
|
256
|
+
children && h(TreeGroup, {}, () => h(Items, { items: children })),
|
|
257
|
+
])
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const log = useLog()
|
|
261
|
+
|
|
262
|
+
async function open(value: string) {
|
|
263
|
+
if (!files.has(value)) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
await new Promise((resolve) => setTimeout(resolve, 600))
|
|
267
|
+
log(`Opened ${value}`)
|
|
268
|
+
}
|
|
269
|
+
</script>
|
|
270
|
+
|
|
271
|
+
<template>
|
|
272
|
+
<Tree
|
|
273
|
+
aria-label="Project"
|
|
274
|
+
:paths="paths"
|
|
275
|
+
:default-expanded="['src']"
|
|
276
|
+
default-value="src/index.ts"
|
|
277
|
+
activation="automatic"
|
|
278
|
+
@action="open"
|
|
279
|
+
>
|
|
280
|
+
<Items :items="items" />
|
|
281
|
+
</Tree>
|
|
282
|
+
</template>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Arrow down to package.json, then press Enter. It shows a spinner while it opens, then the console logs Opened package.json.
|
|
286
|
+
|
|
287
|
+
## Selection
|
|
288
|
+
|
|
289
|
+
The default `mode="single"` uses one string value or `null`. Use `"multiple"` for an array, or `"none"` when items should only navigate or perform actions.
|
|
290
|
+
|
|
291
|
+
`activation="automatic"` selects while arrow keys move in single selection mode. Pressing a branch also expands or collapses it; set `expand-on-press` to `false` to leave expansion to the indicator and arrow keys.
|
|
292
|
+
|
|
293
|
+
## Multiple Selection
|
|
294
|
+
|
|
295
|
+
`mode="multiple"` makes the value an array. Cmd-click or Ctrl-click toggles one item, Shift extends a range, and Cmd+A or Ctrl+A selects the visible items. This example sets `expand-on-press` to `false`, so clicking Inbox or Projects selects the folder without closing it.
|
|
296
|
+
|
|
297
|
+
```vue title="multiple.vue"
|
|
298
|
+
<script setup lang="ts">
|
|
299
|
+
import { ChevronRightIcon } from "@lucide/vue"
|
|
300
|
+
import {
|
|
301
|
+
Tree,
|
|
302
|
+
TreeGroup,
|
|
303
|
+
TreeItem,
|
|
304
|
+
TreeItemIndicator,
|
|
305
|
+
TreeItemLabel,
|
|
306
|
+
type TreeItemData,
|
|
307
|
+
} from "@ui-sdk/vue/tree"
|
|
308
|
+
import { h, type FunctionalComponent } from "vue"
|
|
309
|
+
import { useLog } from "./log"
|
|
310
|
+
|
|
311
|
+
const items: TreeItemData[] = [
|
|
312
|
+
{
|
|
313
|
+
value: "inbox",
|
|
314
|
+
label: "Inbox",
|
|
315
|
+
children: [
|
|
316
|
+
{ value: "updates", label: "Updates" },
|
|
317
|
+
{ value: "forums", label: "Forums" },
|
|
318
|
+
{ value: "promotions", label: "Promotions" },
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
value: "projects",
|
|
323
|
+
label: "Projects",
|
|
324
|
+
children: [
|
|
325
|
+
{ value: "website", label: "Website" },
|
|
326
|
+
{ value: "mobile", label: "Mobile app" },
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
{ value: "archive", label: "Archive" },
|
|
330
|
+
{ value: "trash", label: "Trash" },
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
const Items: FunctionalComponent<{ items: TreeItemData[] }> = ({ items }) =>
|
|
334
|
+
items.map(({ value, label, children }) =>
|
|
335
|
+
h(TreeItem, { key: value, value }, () => [
|
|
336
|
+
h(TreeItemLabel, {}, () => [
|
|
337
|
+
children &&
|
|
338
|
+
h(TreeItemIndicator, {}, () =>
|
|
339
|
+
h(ChevronRightIcon, { "aria-hidden": "true" })
|
|
340
|
+
),
|
|
341
|
+
label,
|
|
342
|
+
]),
|
|
343
|
+
children && h(TreeGroup, {}, () => h(Items, { items: children })),
|
|
344
|
+
])
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const log = useLog()
|
|
348
|
+
</script>
|
|
349
|
+
|
|
350
|
+
<template>
|
|
351
|
+
<Tree
|
|
352
|
+
aria-label="Folders"
|
|
353
|
+
:items="items"
|
|
354
|
+
mode="multiple"
|
|
355
|
+
:default-value="['updates', 'website']"
|
|
356
|
+
@value-change="
|
|
357
|
+
(value) =>
|
|
358
|
+
Array.isArray(value) &&
|
|
359
|
+
log(`${value.length} selected: ${value.join(', ')}`)
|
|
360
|
+
"
|
|
361
|
+
:expand-on-press="false"
|
|
362
|
+
:default-expanded="['inbox', 'projects']"
|
|
363
|
+
>
|
|
364
|
+
<Items :items="items" />
|
|
365
|
+
</Tree>
|
|
366
|
+
</template>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Cmd-click or Ctrl-click Archive to add it to the selection. Shift-click or Shift+Arrow extends a range, and the console logs what's selected.
|
|
370
|
+
|
|
371
|
+
## Checkboxes
|
|
372
|
+
|
|
373
|
+
Add `checkable` and `TreeItemCheckbox` for a checked state separate from selection. Space checks the focused item. Checking a branch checks its descendants, and a partly checked branch reports a mixed state.
|
|
374
|
+
|
|
375
|
+
`check-strategy` chooses what the `checked` array reports:
|
|
376
|
+
|
|
377
|
+
| Strategy | Reported Values |
|
|
378
|
+
| --------------- | ---------------------------------------- |
|
|
379
|
+
| `"leaf"` | Checked leaves. This is the default. |
|
|
380
|
+
| `"trunk"` | The outermost fully checked items. |
|
|
381
|
+
| `"branch"` | Every fully checked item. |
|
|
382
|
+
| `"independent"` | Each item on its own, without cascading. |
|
|
383
|
+
|
|
384
|
+
```vue title="checkboxes.vue"
|
|
385
|
+
<script setup lang="ts">
|
|
386
|
+
import { ChevronRightIcon } from "@lucide/vue"
|
|
387
|
+
import {
|
|
388
|
+
Tree,
|
|
389
|
+
TreeGroup,
|
|
390
|
+
TreeItem,
|
|
391
|
+
TreeItemCheckbox,
|
|
392
|
+
TreeItemIndicator,
|
|
393
|
+
TreeItemLabel,
|
|
394
|
+
type TreeItemData,
|
|
395
|
+
} from "@ui-sdk/vue/tree"
|
|
396
|
+
import { h, type FunctionalComponent } from "vue"
|
|
397
|
+
import { useLog } from "./log"
|
|
398
|
+
|
|
399
|
+
const items: TreeItemData[] = [
|
|
400
|
+
{
|
|
401
|
+
value: "frontend",
|
|
402
|
+
label: "Frontend",
|
|
403
|
+
children: [
|
|
404
|
+
{ value: "react", label: "React" },
|
|
405
|
+
{ value: "vue", label: "Vue" },
|
|
406
|
+
{ value: "svelte", label: "Svelte" },
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
value: "backend",
|
|
411
|
+
label: "Backend",
|
|
412
|
+
children: [
|
|
413
|
+
{ value: "node", label: "Node" },
|
|
414
|
+
{
|
|
415
|
+
value: "databases",
|
|
416
|
+
label: "Databases",
|
|
417
|
+
children: [
|
|
418
|
+
{ value: "postgres", label: "Postgres" },
|
|
419
|
+
{ value: "sqlite", label: "SQLite" },
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
},
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
const Items: FunctionalComponent<{ items: TreeItemData[] }> = ({ items }) =>
|
|
427
|
+
items.map(({ value, label, children }) =>
|
|
428
|
+
h(TreeItem, { key: value, value }, () => [
|
|
429
|
+
h(TreeItemLabel, {}, () => [
|
|
430
|
+
children &&
|
|
431
|
+
h(TreeItemIndicator, {}, () =>
|
|
432
|
+
h(ChevronRightIcon, { "aria-hidden": "true" })
|
|
433
|
+
),
|
|
434
|
+
h(TreeItemCheckbox, {}),
|
|
435
|
+
label,
|
|
436
|
+
]),
|
|
437
|
+
children && h(TreeGroup, {}, () => h(Items, { items: children })),
|
|
438
|
+
])
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
const log = useLog()
|
|
442
|
+
</script>
|
|
443
|
+
|
|
444
|
+
<template>
|
|
445
|
+
<Tree
|
|
446
|
+
aria-label="Stack"
|
|
447
|
+
:items="items"
|
|
448
|
+
checkable
|
|
449
|
+
mode="none"
|
|
450
|
+
check-strategy="trunk"
|
|
451
|
+
:default-checked="['react', 'postgres']"
|
|
452
|
+
@checked-change="
|
|
453
|
+
(checked) => log(`Checked ${checked.join(', ') || 'nothing'}`)
|
|
454
|
+
"
|
|
455
|
+
:default-expanded="['frontend', 'backend', 'databases']"
|
|
456
|
+
>
|
|
457
|
+
<Items :items="items" />
|
|
458
|
+
</Tree>
|
|
459
|
+
</template>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Press Space on Vue, then on Frontend. Frontend is mixed while only some of its items are checked, and the console logs the trunk strategy's values.
|
|
463
|
+
|
|
464
|
+
## Links
|
|
465
|
+
|
|
466
|
+
Set `mode="none"` for a navigation tree, then render labels as links. `current` identifies the current page with `aria-current="page"`, expands its ancestors, and gives it the initial Tab stop. Update it when your route changes.
|
|
467
|
+
|
|
468
|
+
```vue title="links.vue"
|
|
469
|
+
<script setup lang="ts">
|
|
470
|
+
import { ChevronRightIcon } from "@lucide/vue"
|
|
471
|
+
import {
|
|
472
|
+
Tree,
|
|
473
|
+
TreeGroup,
|
|
474
|
+
TreeItem,
|
|
475
|
+
TreeItemIndicator,
|
|
476
|
+
TreeItemLabel,
|
|
477
|
+
type TreeActionDetails,
|
|
478
|
+
type TreeItemData,
|
|
479
|
+
} from "@ui-sdk/vue/tree"
|
|
480
|
+
import { h, ref, type FunctionalComponent } from "vue"
|
|
481
|
+
|
|
482
|
+
const pages: TreeItemData[] = [
|
|
483
|
+
{
|
|
484
|
+
value: "getting-started",
|
|
485
|
+
label: "Getting started",
|
|
486
|
+
children: [
|
|
487
|
+
{ value: "introduction", label: "Introduction", href: "#introduction" },
|
|
488
|
+
{ value: "installation", label: "Installation", href: "#installation" },
|
|
489
|
+
],
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
value: "components",
|
|
493
|
+
label: "Components",
|
|
494
|
+
children: [
|
|
495
|
+
{ value: "accordion", label: "Accordion", href: "#accordion" },
|
|
496
|
+
{ value: "dialog", label: "Dialog", href: "#dialog" },
|
|
497
|
+
{ value: "tree", label: "Tree", href: "#tree" },
|
|
498
|
+
],
|
|
499
|
+
},
|
|
500
|
+
{ value: "changelog", label: "Changelog", href: "#changelog" },
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
const links = new Set(
|
|
504
|
+
pages
|
|
505
|
+
.flatMap((page) => [page, ...(page.children ?? [])])
|
|
506
|
+
.filter((page) => page.href)
|
|
507
|
+
.map((page) => page.value)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
const Items: FunctionalComponent<{ items: TreeItemData[] }> = ({ items }) =>
|
|
511
|
+
items.map(({ value, label, href, children }) => {
|
|
512
|
+
const content = () => [
|
|
513
|
+
children &&
|
|
514
|
+
h(TreeItemIndicator, {}, () =>
|
|
515
|
+
h(ChevronRightIcon, { "aria-hidden": "true" })
|
|
516
|
+
),
|
|
517
|
+
label,
|
|
518
|
+
]
|
|
519
|
+
return h(TreeItem, { key: value, value }, () => [
|
|
520
|
+
h(
|
|
521
|
+
TreeItemLabel,
|
|
522
|
+
{},
|
|
523
|
+
href
|
|
524
|
+
? {
|
|
525
|
+
default: content,
|
|
526
|
+
render: ({
|
|
527
|
+
props,
|
|
528
|
+
children,
|
|
529
|
+
}: {
|
|
530
|
+
props: Record<string, unknown>
|
|
531
|
+
children: FunctionalComponent
|
|
532
|
+
}) => h("a", { ...props, href }, h(children)),
|
|
533
|
+
}
|
|
534
|
+
: content
|
|
535
|
+
),
|
|
536
|
+
children && h(TreeGroup, {}, () => h(Items, { items: children })),
|
|
537
|
+
])
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
const current = ref("tree")
|
|
541
|
+
|
|
542
|
+
function onAction(value: string, { event }: TreeActionDetails) {
|
|
543
|
+
if (!links.has(value)) {
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
event?.preventDefault()
|
|
547
|
+
current.value = value
|
|
548
|
+
}
|
|
549
|
+
</script>
|
|
550
|
+
|
|
551
|
+
<template>
|
|
552
|
+
<Tree aria-label="Docs" :items="pages" mode="none" :current="current" @action="onAction">
|
|
553
|
+
<Items :items="pages" />
|
|
554
|
+
</Tree>
|
|
555
|
+
</template>
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Choose Dialog. The current-page marker moves to it, and you stay on this docs page.
|
|
559
|
+
|
|
560
|
+
## Disabled
|
|
561
|
+
|
|
562
|
+
Disabled items can't be selected, checked, expanded, or run. They stay focusable so screen readers can find them. Set `focusable-when-disabled` to `false` to skip them, or `disabled` on the root to disable the whole tree. A disabled `<fieldset>` around the tree disables it too.
|
|
563
|
+
|
|
564
|
+
```vue title="disabled.vue"
|
|
565
|
+
<script setup lang="ts">
|
|
566
|
+
import { ChevronRightIcon } from "@lucide/vue"
|
|
567
|
+
import { Switch, SwitchThumb } from "@ui-sdk/vue/switch"
|
|
568
|
+
import {
|
|
569
|
+
Tree,
|
|
570
|
+
TreeGroup,
|
|
571
|
+
TreeItem,
|
|
572
|
+
TreeItemIndicator,
|
|
573
|
+
TreeItemLabel,
|
|
574
|
+
type TreeItemData,
|
|
575
|
+
} from "@ui-sdk/vue/tree"
|
|
576
|
+
import { h, ref, type FunctionalComponent } from "vue"
|
|
577
|
+
|
|
578
|
+
const items: TreeItemData[] = [
|
|
579
|
+
{
|
|
580
|
+
value: "plans",
|
|
581
|
+
label: "Plans",
|
|
582
|
+
children: [
|
|
583
|
+
{ value: "free", label: "Free" },
|
|
584
|
+
{ value: "pro", label: "Pro" },
|
|
585
|
+
{ value: "enterprise", label: "Enterprise", disabled: true },
|
|
586
|
+
],
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
value: "legacy",
|
|
590
|
+
label: "Legacy plans",
|
|
591
|
+
disabled: true,
|
|
592
|
+
children: [{ value: "starter", label: "Starter" }],
|
|
593
|
+
},
|
|
594
|
+
{ value: "custom", label: "Custom" },
|
|
595
|
+
]
|
|
596
|
+
|
|
597
|
+
const Items: FunctionalComponent<{ items: TreeItemData[] }> = ({ items }) =>
|
|
598
|
+
items.map(({ value, label, disabled, children }) =>
|
|
599
|
+
h(TreeItem, { key: value, value, disabled }, () => [
|
|
600
|
+
h(TreeItemLabel, {}, () => [
|
|
601
|
+
children &&
|
|
602
|
+
h(TreeItemIndicator, {}, () =>
|
|
603
|
+
h(ChevronRightIcon, { "aria-hidden": "true" })
|
|
604
|
+
),
|
|
605
|
+
label,
|
|
606
|
+
]),
|
|
607
|
+
children && h(TreeGroup, {}, () => h(Items, { items: children })),
|
|
608
|
+
])
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
const focusable = ref(true)
|
|
612
|
+
</script>
|
|
613
|
+
|
|
614
|
+
<template>
|
|
615
|
+
<div data-slot="stack">
|
|
616
|
+
<div data-slot="controls">
|
|
617
|
+
<label data-slot="label">
|
|
618
|
+
<Switch v-model:checked="focusable">
|
|
619
|
+
<SwitchThumb />
|
|
620
|
+
</Switch>
|
|
621
|
+
Focusable when disabled
|
|
622
|
+
</label>
|
|
623
|
+
</div>
|
|
624
|
+
<Tree
|
|
625
|
+
aria-label="Plans"
|
|
626
|
+
:items="items"
|
|
627
|
+
:default-expanded="['plans']"
|
|
628
|
+
:focusable-when-disabled="focusable"
|
|
629
|
+
>
|
|
630
|
+
<Items :items="items" />
|
|
631
|
+
</Tree>
|
|
632
|
+
</div>
|
|
633
|
+
</template>
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Arrow through Enterprise while Focusable when disabled is on. Turn it off and the same arrow keys skip disabled items.
|
|
637
|
+
|
|
638
|
+
## Loading Children
|
|
639
|
+
|
|
640
|
+
Mark an item as a `branch` before its children exist. `load-children` runs the first time that branch expands while empty. Load and render its children, then let the promise resolve; the tree expands the branch once they are ready.
|
|
641
|
+
|
|
642
|
+
Use the supplied `signal` to cancel a request. Trying to expand the branch again while it loads aborts it. A rejection sets `data-failed`, and the next attempt retries. `announcement` lets you customize loading and failure messages.
|
|
643
|
+
|
|
644
|
+
```vue title="lazy.vue"
|
|
645
|
+
<script setup lang="ts">
|
|
646
|
+
import { ChevronRightIcon, LoaderCircleIcon, RotateCwIcon } from "@lucide/vue"
|
|
647
|
+
import {
|
|
648
|
+
Tree,
|
|
649
|
+
TreeGroup,
|
|
650
|
+
TreeItem,
|
|
651
|
+
TreeItemIndicator,
|
|
652
|
+
TreeItemLabel,
|
|
653
|
+
type TreeAnnouncement,
|
|
654
|
+
type TreeItemState,
|
|
655
|
+
} from "@ui-sdk/vue/tree"
|
|
656
|
+
import { h, ref, type FunctionalComponent } from "vue"
|
|
657
|
+
|
|
658
|
+
type Node = { value: string; label: string; branch?: boolean }
|
|
659
|
+
|
|
660
|
+
const top: Node[] = [
|
|
661
|
+
{ value: "users", label: "users", branch: true },
|
|
662
|
+
{ value: "orders", label: "orders", branch: true },
|
|
663
|
+
{ value: "archive", label: "archive", branch: true },
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
let archiveAsked = false
|
|
667
|
+
async function fetchChildren(value: string, signal: AbortSignal) {
|
|
668
|
+
await new Promise((resolve, reject) => {
|
|
669
|
+
const timer = setTimeout(resolve, 800)
|
|
670
|
+
signal.addEventListener(
|
|
671
|
+
"abort",
|
|
672
|
+
() => (clearTimeout(timer), reject(signal.reason))
|
|
673
|
+
)
|
|
674
|
+
})
|
|
675
|
+
if (value === "archive" && !archiveAsked) {
|
|
676
|
+
archiveAsked = true
|
|
677
|
+
throw new Error("Network error")
|
|
678
|
+
}
|
|
679
|
+
return Array.from({ length: 3 }, (_, index) => ({
|
|
680
|
+
value: `${value}/${index + 1}`,
|
|
681
|
+
label: `${value.slice(0, -1)}_${index + 1}.json`,
|
|
682
|
+
}))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const Items: FunctionalComponent<{
|
|
686
|
+
items: Node[]
|
|
687
|
+
loaded: Record<string, Node[]>
|
|
688
|
+
}> = ({ items, loaded }) =>
|
|
689
|
+
items.map(({ value, label, branch }) =>
|
|
690
|
+
h(TreeItem, { key: value, value, branch }, () => [
|
|
691
|
+
h(TreeItemLabel, {}, () => [
|
|
692
|
+
branch &&
|
|
693
|
+
h(
|
|
694
|
+
TreeItemIndicator,
|
|
695
|
+
{},
|
|
696
|
+
{
|
|
697
|
+
default: ({ state }: { state: TreeItemState }) =>
|
|
698
|
+
state.loading
|
|
699
|
+
? h(LoaderCircleIcon, { "aria-hidden": "true" })
|
|
700
|
+
: state.failed
|
|
701
|
+
? h(RotateCwIcon, { "aria-hidden": "true" })
|
|
702
|
+
: h(ChevronRightIcon, { "aria-hidden": "true" }),
|
|
703
|
+
}
|
|
704
|
+
),
|
|
705
|
+
label,
|
|
706
|
+
]),
|
|
707
|
+
loaded[value] &&
|
|
708
|
+
h(TreeGroup, {}, () => h(Items, { items: loaded[value], loaded })),
|
|
709
|
+
])
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
const loaded = ref<Record<string, Node[]>>({})
|
|
713
|
+
|
|
714
|
+
async function loadChildren(
|
|
715
|
+
value: string,
|
|
716
|
+
{ signal }: { signal: AbortSignal }
|
|
717
|
+
) {
|
|
718
|
+
const children = await fetchChildren(value, signal)
|
|
719
|
+
loaded.value = { ...loaded.value, [value]: children }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function announcement({ type, label }: TreeAnnouncement) {
|
|
723
|
+
return {
|
|
724
|
+
expanded: "Expanded",
|
|
725
|
+
collapsed: "Collapsed",
|
|
726
|
+
loading: `Loading ${label}`,
|
|
727
|
+
"load-failed": `${label} didn't load. Try again.`,
|
|
728
|
+
}[type]
|
|
729
|
+
}
|
|
730
|
+
</script>
|
|
731
|
+
|
|
732
|
+
<template>
|
|
733
|
+
<Tree aria-label="Database" :load-children="loadChildren" :announcement="announcement">
|
|
734
|
+
<Items :items="top" :loaded="loaded" />
|
|
735
|
+
</Tree>
|
|
736
|
+
</template>
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Expand users to load its files. Archive fails on its first attempt and shows a retry icon; expand it again to load it.
|
|
740
|
+
|
|
741
|
+
## Virtualized
|
|
742
|
+
|
|
743
|
+
Pass the full `items` hierarchy and use `visibleTreeRows(items, expanded)` to flatten the open branches. Render the nearby rows with their position; the tree supplies the correct ARIA hierarchy even though the DOM is flat.
|
|
744
|
+
|
|
745
|
+
`@highlight` reports the focused item's index among visible rows. Use it to scroll before mounting a row that is outside the rendered range. Every item sets `--tree-level`, so the stylesheet indents the flat rows by their level.
|
|
746
|
+
|
|
747
|
+
```vue title="virtualized.vue"
|
|
748
|
+
<script setup lang="ts">
|
|
749
|
+
import { ChevronRightIcon } from "@lucide/vue"
|
|
750
|
+
import {
|
|
751
|
+
Tree,
|
|
752
|
+
TreeItem,
|
|
753
|
+
TreeItemIndicator,
|
|
754
|
+
TreeItemLabel,
|
|
755
|
+
visibleTreeRows,
|
|
756
|
+
type ListHighlightDetails,
|
|
757
|
+
type TreeItemData,
|
|
758
|
+
} from "@ui-sdk/vue/tree"
|
|
759
|
+
import { computed, ref } from "vue"
|
|
760
|
+
|
|
761
|
+
const items: TreeItemData[] = Array.from({ length: 100 }, (_, folder) => ({
|
|
762
|
+
value: `folder-${folder + 1}`,
|
|
763
|
+
label: `Folder ${folder + 1}`,
|
|
764
|
+
children: Array.from({ length: 100 }, (_, file) => ({
|
|
765
|
+
value: `folder-${folder + 1}/file-${file + 1}`,
|
|
766
|
+
label: `File ${file + 1}.txt`,
|
|
767
|
+
})),
|
|
768
|
+
}))
|
|
769
|
+
|
|
770
|
+
const row = 32
|
|
771
|
+
const height = 288
|
|
772
|
+
|
|
773
|
+
const scroller = ref<HTMLDivElement | null>(null)
|
|
774
|
+
const scrollTop = ref(0)
|
|
775
|
+
const expanded = ref(["folder-1"])
|
|
776
|
+
const rows = computed(() => visibleTreeRows(items, expanded.value))
|
|
777
|
+
const visible = computed(() => {
|
|
778
|
+
const start = Math.max(0, Math.floor(scrollTop.value / row) - 4)
|
|
779
|
+
const end = Math.min(
|
|
780
|
+
rows.value.length,
|
|
781
|
+
Math.ceil((scrollTop.value + height) / row) + 4
|
|
782
|
+
)
|
|
783
|
+
return rows.value.slice(start, end)
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
function onHighlight(_value: string, { index }: ListHighlightDetails) {
|
|
787
|
+
const el = scroller.value
|
|
788
|
+
if (!el) {
|
|
789
|
+
return
|
|
790
|
+
}
|
|
791
|
+
const top = index * row
|
|
792
|
+
if (top < el.scrollTop) {
|
|
793
|
+
el.scrollTop = top
|
|
794
|
+
} else if (top + row > el.scrollTop + el.clientHeight - 10) {
|
|
795
|
+
el.scrollTop = top + row - el.clientHeight + 10
|
|
796
|
+
}
|
|
797
|
+
scrollTop.value = el.scrollTop
|
|
798
|
+
}
|
|
799
|
+
</script>
|
|
800
|
+
|
|
801
|
+
<template>
|
|
802
|
+
<div
|
|
803
|
+
data-slot="scroll"
|
|
804
|
+
ref="scroller"
|
|
805
|
+
@scroll="scrollTop = ($event.currentTarget as HTMLDivElement).scrollTop"
|
|
806
|
+
>
|
|
807
|
+
<Tree
|
|
808
|
+
v-model:expanded="expanded"
|
|
809
|
+
aria-label="Archive"
|
|
810
|
+
:items="items"
|
|
811
|
+
@highlight="onHighlight"
|
|
812
|
+
:style="{ height: `${rows.length * row}px` }"
|
|
813
|
+
>
|
|
814
|
+
<TreeItem
|
|
815
|
+
v-for="{ item, index } in visible"
|
|
816
|
+
:key="item.value"
|
|
817
|
+
:value="item.value"
|
|
818
|
+
:style="{ top: `${index * row}px` }"
|
|
819
|
+
>
|
|
820
|
+
<TreeItemLabel>
|
|
821
|
+
<TreeItemIndicator v-if="item.children">
|
|
822
|
+
<ChevronRightIcon aria-hidden="true" />
|
|
823
|
+
</TreeItemIndicator>
|
|
824
|
+
{{ item.label }}
|
|
825
|
+
</TreeItemLabel>
|
|
826
|
+
</TreeItem>
|
|
827
|
+
</Tree>
|
|
828
|
+
</div>
|
|
829
|
+
</template>
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
Expand folders or press End to focus Folder 100. The tree has 10,100 items, but only nearby rows render.
|
|
833
|
+
|
|
834
|
+
## Find in Page
|
|
835
|
+
|
|
836
|
+
`hidden-until-found` keeps collapsed groups in the DOM with `hidden="until-found"`. In browsers that support it, Find in Page can match a hidden item and open its ancestors. Use `keep-mounted` when you only need collapsed content to stay mounted, without browser search.
|
|
837
|
+
|
|
838
|
+
```vue title="find-in-page.vue"
|
|
839
|
+
<script setup lang="ts">
|
|
840
|
+
import { ChevronRightIcon } from "@lucide/vue"
|
|
841
|
+
import {
|
|
842
|
+
Tree,
|
|
843
|
+
TreeGroup,
|
|
844
|
+
TreeItem,
|
|
845
|
+
TreeItemIndicator,
|
|
846
|
+
TreeItemLabel,
|
|
847
|
+
type TreeItemData,
|
|
848
|
+
} from "@ui-sdk/vue/tree"
|
|
849
|
+
import { h, type FunctionalComponent } from "vue"
|
|
850
|
+
|
|
851
|
+
const items: TreeItemData[] = [
|
|
852
|
+
{
|
|
853
|
+
value: "planets",
|
|
854
|
+
label: "Planets",
|
|
855
|
+
children: [
|
|
856
|
+
{ value: "mercury", label: "Mercury" },
|
|
857
|
+
{ value: "venus", label: "Venus" },
|
|
858
|
+
{
|
|
859
|
+
value: "earth",
|
|
860
|
+
label: "Earth",
|
|
861
|
+
children: [{ value: "moon", label: "The Moon" }],
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
value: "mars",
|
|
865
|
+
label: "Mars",
|
|
866
|
+
children: [
|
|
867
|
+
{ value: "phobos", label: "Phobos" },
|
|
868
|
+
{ value: "deimos", label: "Deimos" },
|
|
869
|
+
],
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
value: "dwarf-planets",
|
|
875
|
+
label: "Dwarf planets",
|
|
876
|
+
children: [
|
|
877
|
+
{ value: "ceres", label: "Ceres" },
|
|
878
|
+
{
|
|
879
|
+
value: "pluto",
|
|
880
|
+
label: "Pluto",
|
|
881
|
+
children: [{ value: "charon", label: "Charon" }],
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
},
|
|
885
|
+
]
|
|
886
|
+
|
|
887
|
+
const Items: FunctionalComponent<{ items: TreeItemData[] }> = ({ items }) =>
|
|
888
|
+
items.map(({ value, label, children }) =>
|
|
889
|
+
h(TreeItem, { key: value, value }, () => [
|
|
890
|
+
h(TreeItemLabel, {}, () => [
|
|
891
|
+
children &&
|
|
892
|
+
h(TreeItemIndicator, {}, () =>
|
|
893
|
+
h(ChevronRightIcon, { "aria-hidden": "true" })
|
|
894
|
+
),
|
|
895
|
+
label,
|
|
896
|
+
]),
|
|
897
|
+
children && h(TreeGroup, {}, () => h(Items, { items: children })),
|
|
898
|
+
])
|
|
899
|
+
)
|
|
900
|
+
</script>
|
|
901
|
+
|
|
902
|
+
<template>
|
|
903
|
+
<Tree aria-label="Solar system" :items="items" hidden-until-found>
|
|
904
|
+
<Items :items="items" />
|
|
905
|
+
</Tree>
|
|
906
|
+
</template>
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
Use your browser's Find command to look for Phobos, a moon of Mars. The matching branches open to reveal it.
|
|
910
|
+
|
|
911
|
+
## Animation
|
|
912
|
+
|
|
913
|
+
A group exposes its measured height as `--tree-group-height`. Transition between that height and zero from `data-starting-style` and `data-ending-style`:
|
|
914
|
+
|
|
915
|
+
```css
|
|
916
|
+
[data-slot="tree-group"] {
|
|
917
|
+
height: var(--tree-group-height);
|
|
918
|
+
overflow: hidden;
|
|
919
|
+
transition: height 150ms ease-out;
|
|
920
|
+
|
|
921
|
+
&:is([data-starting-style], [data-ending-style]) {
|
|
922
|
+
height: 0;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
The group stays mounted until its closing animation ends. Groups open on the first render, or revealed by Find in Page, don't animate.
|
|
928
|
+
|
|
929
|
+
## Controlled
|
|
930
|
+
|
|
931
|
+
Selection, expanded branches, and checkboxes are independent:
|
|
932
|
+
|
|
933
|
+
| Value | Shape | Change Notification |
|
|
934
|
+
| ---------- | ---------------------------------------------- | ------------------- |
|
|
935
|
+
| `value` | A string or `null`; an array in multiple mode. | `@value-change` |
|
|
936
|
+
| `expanded` | An array of branch values. | `@expanded-change` |
|
|
937
|
+
| `checked` | An array shaped by the check strategy. | `@checked-change` |
|
|
938
|
+
|
|
939
|
+
Bind one with `v-model:value`, `v-model:expanded` or `v-model:checked` to control it, or use `default-value`, `default-expanded`, and `default-checked` for starting state. Call `details.cancel()` in a change handler to reject a change. [State and Events](../state-and-events.md) covers the shared binding pattern.
|
|
940
|
+
|
|
941
|
+
## Controller
|
|
942
|
+
|
|
943
|
+
A `createTreeController()` instance can expand or collapse branches, change selection and checkboxes, or focus an item. `reveal(value)` opens an item's ancestors, scrolls it into view, and focuses it. Supply the hierarchy through `items`, as this example does, or keep collapsed groups mounted so their descendants are known. See [Controllers](../controllers.md) for setup and lifecycle.
|
|
944
|
+
|
|
945
|
+
```vue title="controller.vue"
|
|
946
|
+
<script setup lang="ts">
|
|
947
|
+
import { ChevronRightIcon } from "@lucide/vue"
|
|
948
|
+
import {
|
|
949
|
+
createTreeController,
|
|
950
|
+
Tree,
|
|
951
|
+
TreeGroup,
|
|
952
|
+
TreeItem,
|
|
953
|
+
TreeItemIndicator,
|
|
954
|
+
TreeItemLabel,
|
|
955
|
+
type TreeItemData,
|
|
956
|
+
} from "@ui-sdk/vue/tree"
|
|
957
|
+
import { h, type FunctionalComponent } from "vue"
|
|
958
|
+
|
|
959
|
+
const sections: TreeItemData[] = [
|
|
960
|
+
{
|
|
961
|
+
value: "account",
|
|
962
|
+
label: "Account",
|
|
963
|
+
children: [
|
|
964
|
+
{ value: "profile", label: "Profile" },
|
|
965
|
+
{
|
|
966
|
+
value: "security",
|
|
967
|
+
label: "Security",
|
|
968
|
+
children: [
|
|
969
|
+
{ value: "password", label: "Password" },
|
|
970
|
+
{ value: "two-factor", label: "Two-factor authentication" },
|
|
971
|
+
],
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
value: "workspace",
|
|
977
|
+
label: "Workspace",
|
|
978
|
+
children: [
|
|
979
|
+
{ value: "members", label: "Members" },
|
|
980
|
+
{ value: "billing", label: "Billing" },
|
|
981
|
+
],
|
|
982
|
+
},
|
|
983
|
+
]
|
|
984
|
+
|
|
985
|
+
const Items: FunctionalComponent<{ items: TreeItemData[] }> = ({ items }) =>
|
|
986
|
+
items.map(({ value, label, children }) =>
|
|
987
|
+
h(TreeItem, { key: value, value }, () => [
|
|
988
|
+
h(TreeItemLabel, {}, () => [
|
|
989
|
+
children &&
|
|
990
|
+
h(TreeItemIndicator, {}, () =>
|
|
991
|
+
h(ChevronRightIcon, { "aria-hidden": "true" })
|
|
992
|
+
),
|
|
993
|
+
label,
|
|
994
|
+
]),
|
|
995
|
+
children && h(TreeGroup, {}, () => h(Items, { items: children })),
|
|
996
|
+
])
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
const settings = createTreeController()
|
|
1000
|
+
|
|
1001
|
+
function findTwoFactor() {
|
|
1002
|
+
settings.setValue("two-factor")
|
|
1003
|
+
settings.reveal("two-factor")
|
|
1004
|
+
}
|
|
1005
|
+
</script>
|
|
1006
|
+
|
|
1007
|
+
<template>
|
|
1008
|
+
<div data-slot="stack">
|
|
1009
|
+
<div data-slot="controls">
|
|
1010
|
+
<button type="button" @click="settings.expandAll()">Expand All</button>
|
|
1011
|
+
<button type="button" @click="settings.collapseAll()">
|
|
1012
|
+
Collapse All
|
|
1013
|
+
</button>
|
|
1014
|
+
<button type="button" @click="findTwoFactor">Find Two-Factor</button>
|
|
1015
|
+
</div>
|
|
1016
|
+
<Tree aria-label="Settings" :items="sections" :controller="settings">
|
|
1017
|
+
<Items :items="sections" />
|
|
1018
|
+
</Tree>
|
|
1019
|
+
</div>
|
|
1020
|
+
</template>
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
Press Find Two-Factor to open Account and Security, then select and focus Two-factor authentication. Collapse All closes every branch.
|
|
1024
|
+
|
|
1025
|
+
## Custom Rendering
|
|
1026
|
+
|
|
1027
|
+
Every part takes a `#render` slot. The Links example renders `TreeItemLabel` as an `a`; a link in a label leaves the tab order to its item. The default slots of `TreeItemLabel` and `TreeItemIndicator` also receive the item's state, which the File Paths and Loading Children examples use for their spinners. See [Parts and Composition](../parts-and-composition.md) for prop merging and state-based rendering.
|
|
1028
|
+
|
|
1029
|
+
## Styling
|
|
1030
|
+
|
|
1031
|
+
Style each row through `TreeItemLabel`, which carries its item's `data-selected`, `data-expanded`, `data-branch`, `data-current`, `data-disabled`, `data-loading`, `data-failed` and `data-pending`. An item's own background would also cover its children. Focus stays on the item, so style a focused row as a label inside an item with `:focus-visible`. Turn the indicator with its `data-expanded`, and indent rows with `--tree-level` or padding on each group.
|
|
1032
|
+
|
|
1033
|
+
Each part has a `data-slot`, such as `tree-label`, and its state attributes are listed under [Data Attributes](#data-attributes).
|
|
1034
|
+
|
|
1035
|
+
### TreeItem
|
|
1036
|
+
- `--tree-level`: The item's depth, 1 for a top-level item. Indent a flat tree with it.
|
|
1037
|
+
|
|
1038
|
+
### TreeGroup
|
|
1039
|
+
- `--tree-group-height`: The group's content height in pixels while it animates, auto once expanded and 0px while collapsed.
|
|
1040
|
+
|
|
1041
|
+
## Data Attributes
|
|
1042
|
+
|
|
1043
|
+
### TreeItem
|
|
1044
|
+
`[data-slot="tree-item"]`
|
|
1045
|
+
|
|
1046
|
+
- `data-level` (`1`, `2`): The item's depth, from 1 at the top level. Also in the `--tree-level` variable.
|
|
1047
|
+
- `data-branch`: Present on a branch: an item with children, or with `branch` set.
|
|
1048
|
+
- `data-expanded`: Present while a branch is expanded.
|
|
1049
|
+
- `data-indeterminate`: Present while only some of a branch is checked, with `checkable`.
|
|
1050
|
+
- `data-selected`: Present while the item is selected.
|
|
1051
|
+
- `data-current`: Present on the item for the current page, from `current`.
|
|
1052
|
+
- `data-checked`: Present while the item's checkbox is checked, with `checkable`.
|
|
1053
|
+
- `data-disabled`: Present when the item, or the whole tree, is disabled.
|
|
1054
|
+
- `data-unchecked`: Present while the item's checkbox is unchecked, with `checkable`.
|
|
1055
|
+
- `data-loading`: Present while `loadChildren` loads the branch's children.
|
|
1056
|
+
- `data-failed`: Present after the branch's children failed to load, until it loads again.
|
|
1057
|
+
- `data-pending`: Present while the item's `onAction` Promise hasn't settled.
|
|
1058
|
+
|
|
1059
|
+
### TreeItemLabel
|
|
1060
|
+
`[data-slot="tree-label"]`
|
|
1061
|
+
|
|
1062
|
+
- `data-level` (`1`, `2`): The item's depth, from 1 at the top level. Also in the `--tree-level` variable.
|
|
1063
|
+
- `data-branch`: Present on a branch: an item with children, or with `branch` set.
|
|
1064
|
+
- `data-expanded`: Present while a branch is expanded.
|
|
1065
|
+
- `data-selected`: Present while the item is selected.
|
|
1066
|
+
- `data-current`: Present on the item for the current page, from `current`.
|
|
1067
|
+
- `data-disabled`: Present when the item, or the whole tree, is disabled.
|
|
1068
|
+
- `data-loading`: Present while `loadChildren` loads the branch's children.
|
|
1069
|
+
- `data-failed`: Present after the branch's children failed to load, until it loads again.
|
|
1070
|
+
- `data-pending`: Present while the item's `onAction` Promise hasn't settled.
|
|
1071
|
+
|
|
1072
|
+
### TreeItemIndicator
|
|
1073
|
+
`[data-slot="tree-indicator"]`
|
|
1074
|
+
|
|
1075
|
+
- `data-level` (`1`): The item's depth, from 1 at the top level. Also in the `--tree-level` variable.
|
|
1076
|
+
- `data-branch`: Present on a branch: an item with children, or with `branch` set.
|
|
1077
|
+
- `data-expanded`: Present while a branch is expanded.
|
|
1078
|
+
- `data-loading`: Present while `loadChildren` loads the branch's children.
|
|
1079
|
+
- `data-failed`: Present after the branch's children failed to load, until it loads again.
|
|
1080
|
+
- `data-pending`: Present while the item's `onAction` Promise hasn't settled.
|
|
1081
|
+
|
|
1082
|
+
### TreeItemCheckbox
|
|
1083
|
+
`[data-slot="tree-checkbox"]`
|
|
1084
|
+
|
|
1085
|
+
- `data-indeterminate`: Present while only some of a branch is checked, with `checkable`.
|
|
1086
|
+
- `data-checked`: Present while the item's checkbox is checked, with `checkable`.
|
|
1087
|
+
- `data-unchecked`: Present while the item's checkbox is unchecked, with `checkable`.
|
|
1088
|
+
|
|
1089
|
+
### TreeGroup
|
|
1090
|
+
`[data-slot="tree-group"]`
|
|
1091
|
+
|
|
1092
|
+
- `data-open`: Present while the group's branch is expanded.
|
|
1093
|
+
- `data-closed`: Present while the group's branch is collapsed, including while it animates out.
|
|
1094
|
+
|
|
1095
|
+
## API Reference
|
|
1096
|
+
|
|
1097
|
+
### Tree
|
|
1098
|
+
|
|
1099
|
+
- `items?` (`TreeItemData[]`): The items as data: each a `value`, a `label` and, for a branch, `children`. Renders them for you, and gives the tree its whole shape even where rows aren't rendered.
|
|
1100
|
+
- `paths?` (`readonly string[]`): Items from file paths, like `["src/index.ts", "README.md"]`: each folder becomes a branch, folders first. A path ending in `/` is an empty folder.
|
|
1101
|
+
- `mode?` (`Mode`): How many items can be selected.
|
|
1102
|
+
- `value?` (`TreeValue<Mode>`): The selected value. Pass it to control the selection. `null` selects nothing, and with `mode: "multiple"` it's a list.
|
|
1103
|
+
- `defaultValue?` (`TreeValue<Mode>`): The value selected at first, when uncontrolled.
|
|
1104
|
+
- `@value-change` (`((value: TreeValue<Mode>, details: ChangeDetails<TreeValueChangeReason>) => void)`): Runs when the selection wants to change. `details.reason` says why, and `details.cancel()` stops it.
|
|
1105
|
+
- `activation?` (`"automatic" | "manual"`): Whether moving focus with the keyboard selects, with one item selectable: `"automatic"` selects the focused item, `"manual"` waits for Enter, Space or a press.
|
|
1106
|
+
- `expanded?` (`string[]`): The expanded branches' values. Pass it to control which branches are open.
|
|
1107
|
+
- `defaultExpanded?` (`string[]`): The branches expanded at first, when uncontrolled. The ancestors of `current` are added.
|
|
1108
|
+
- `@expanded-change` (`((expanded: string[], details: ChangeDetails<TreeExpandedChangeReason>) => void)`): Runs when branches want to expand or collapse. `details.reason` says why, and `details.cancel()` stops it.
|
|
1109
|
+
- `expandOnPress?` (`boolean`): Whether pressing a branch's row expands or collapses it. Its indicator always does.
|
|
1110
|
+
- `checkable?` (`boolean`): Gives every item a tri-state checkbox, kept apart from the selection. Checking a branch checks everything in it, and a branch shows mixed while only some of it is checked. Space checks the focused item.
|
|
1111
|
+
- `checked?` (`string[]`): The checked items' values, as `checkStrategy` reports them. Pass it to control the checkboxes.
|
|
1112
|
+
- `defaultChecked?` (`string[]`): The items checked at first, when uncontrolled. A branch stands for everything in it.
|
|
1113
|
+
- `@checked-change` (`((checked: string[], details: ChangeDetails<TreeCheckedChangeReason>) => void)`): Runs when checkboxes want to change. `details.reason` says why, and `details.cancel()` stops it.
|
|
1114
|
+
- `checkStrategy?` (`TreeCheckStrategy`): Which items the checked value lists: `"leaf"` the checked leaves, `"trunk"` the outermost fully checked items, `"branch"` every fully checked item, and `"independent"` each item on its own, with no cascading.
|
|
1115
|
+
- `@action` (`((value: string, details: TreeActionDetails) => unknown)`): Runs when an item is pressed or Enter is pressed on it, apart from selecting it, like opening a file. Return a Promise to mark the item pending until it settles.
|
|
1116
|
+
- `pendingAnnouncement?` (`string`): What screen readers hear once an item's action has been pending for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
|
|
1117
|
+
- `loadChildren?` (`((value: string, details: { signal: AbortSignal; }) => void | Promise<unknown>)`): Loads a branch's children the first time it expands while it has none. The branch shows `data-loading` and expands once the Promise resolves; a rejection marks it `data-failed` and the next expand tries again. Expanding it again while loading cancels through `signal`.
|
|
1118
|
+
- `current?` (`string | null`): The value of the item for the current page. It gets `aria-current="page"`, takes the tab stop first, and its ancestors expand, at first and whenever it changes.
|
|
1119
|
+
- `@highlight` (`((value: string, details: ListHighlightDetails) => void)`): Runs when the keyboard moves focus to an item, with its row among the items that show, from 0. A virtualized list scrolls that row into view here, so an item it hasn't rendered can take focus once it does.
|
|
1120
|
+
- `disabled?` (`boolean`): Disables every item. Items stay focusable unless `focusableWhenDisabled` is false.
|
|
1121
|
+
- `focusableWhenDisabled?` (`boolean`): Whether disabled items take focus, so a screen reader still finds them.
|
|
1122
|
+
- `keepMounted?` (`boolean`): Whether collapsed branches keep their groups in the page, hidden.
|
|
1123
|
+
- `hiddenUntilFound?` (`boolean`): Keeps collapsed groups in the page as `hidden="until-found"`, so the browser's find in page can match their text and expand them.
|
|
1124
|
+
- `announcement?` (`((change: TreeAnnouncement) => string | null | undefined)`): What screen readers hear when a branch expands, collapses, starts loading or fails to load. Return null for nothing. Expanding and collapsing are announced on Apple platforms only, where VoiceOver doesn't read the change.
|
|
1125
|
+
- `controller?` (`object`): Drives the tree from anywhere. Create one with `createTreeController()`.
|
|
1126
|
+
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
|
|
1127
|
+
|
|
1128
|
+
### TreeItem
|
|
1129
|
+
|
|
1130
|
+
- `value` (`string`): Identifies the item, unique in the tree. Passed to `@action`, and listed by the value, `expanded` and `checked`.
|
|
1131
|
+
- `branch?` (`boolean`): Whether it's a branch. Adapters set it when it has a group; set it yourself for a branch whose children load later.
|
|
1132
|
+
- `disabled?` (`boolean`): Stops it from being selected, checked, expanded or run. It stays focusable unless `focusableWhenDisabled` is false.
|
|
1133
|
+
- `label?` (`string`): The text typeahead matches, when its label isn't plain text.
|
|
1134
|
+
|
|
1135
|
+
### TreeGroup
|
|
1136
|
+
|
|
1137
|
+
- `keepMounted?` (`boolean`): Whether the group stays in the page, hidden, while collapsed. Overrides the tree's.
|
|
1138
|
+
- `hiddenUntilFound?` (`boolean`): Whether the group stays findable with `hidden="until-found"` while collapsed. Overrides the tree's.
|
|
1139
|
+
|
|
1140
|
+
### Items as Data
|
|
1141
|
+
|
|
1142
|
+
Each item in `items`:
|
|
1143
|
+
|
|
1144
|
+
- `value` (`string`): Identifies the item. Unique in the tree.
|
|
1145
|
+
- `label` (`string`): What it shows, and what typeahead matches.
|
|
1146
|
+
- `children?` (`TreeItemData[]`): Its children. Leave it out for a leaf. An empty array is a branch with no children, or one whose children load when it opens.
|
|
1147
|
+
- `disabled?` (`boolean`): Whether it can't be selected, checked or activated. It can still take focus.
|
|
1148
|
+
- `href?` (`string`): Makes the item a link to this address.
|
|
1149
|
+
|
|
1150
|
+
Each row `visibleTreeRows(items, expanded)` returns, for a flat list:
|
|
1151
|
+
|
|
1152
|
+
- `item` (`TreeItemData`): The object from your data this row shows, the same one you passed in, children included.
|
|
1153
|
+
- `parent` (`string | null`): The value of the item it's nested in, or `null` at the top level.
|
|
1154
|
+
- `level` (`number`): Its depth, 1 at the top level.
|
|
1155
|
+
- `index` (`number`): Its place among the rows that show, from 0, for a virtualized list.
|
|
1156
|
+
|
|
1157
|
+
### State
|
|
1158
|
+
|
|
1159
|
+
What render functions, state-based class names and slots receive, per part.
|
|
1160
|
+
|
|
1161
|
+
`Tree`:
|
|
1162
|
+
|
|
1163
|
+
- `mode` (`TreeMode`): How many items can be selected.
|
|
1164
|
+
- `checkable` (`boolean`): Whether items have checkboxes.
|
|
1165
|
+
- `disabled` (`boolean`): Whether the whole tree is disabled, by its `disabled` option or a disabled fieldset around it.
|
|
1166
|
+
|
|
1167
|
+
`TreeItem`, and its `TreeItemLabel`, `TreeItemIndicator` and `TreeItemCheckbox`:
|
|
1168
|
+
|
|
1169
|
+
- `expanded` (`boolean`): Whether it's a branch that's expanded.
|
|
1170
|
+
- `selected` (`boolean`): Whether its value is in the tree's value. Separate from `checked`, which its checkbox sets.
|
|
1171
|
+
- `checked` (`TreeCheckedState`): Its checkbox: checked, unchecked or `"indeterminate"`. False without `checkable`.
|
|
1172
|
+
- `disabled` (`boolean`): Whether it can't be selected, checked, expanded or run.
|
|
1173
|
+
- `branch` (`boolean`): Whether it has children, or can load them.
|
|
1174
|
+
- `level` (`number`): Its depth, 1 at the top level.
|
|
1175
|
+
- `current` (`boolean`): Whether it's the current page's item.
|
|
1176
|
+
- `loading` (`boolean`): Whether its children are loading.
|
|
1177
|
+
- `failed` (`boolean`): Whether loading its children failed.
|
|
1178
|
+
- `pending` (`boolean`): Whether its `onAction` is still running.
|
|
1179
|
+
|
|
1180
|
+
`TreeGroup`:
|
|
1181
|
+
|
|
1182
|
+
- `expanded` (`boolean`): Whether its branch is expanded.
|
|
1183
|
+
- `level` (`number`): The depth of the items in it.
|
|
1184
|
+
|
|
1185
|
+
### Reasons
|
|
1186
|
+
|
|
1187
|
+
Why branches expanded or collapsed:
|
|
1188
|
+
|
|
1189
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
1190
|
+
- `trigger-press` (`MouseEvent | KeyboardEvent | TouchEvent`): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its `touchend`.
|
|
1191
|
+
- `list-navigation` (`KeyboardEvent`): Arrow, Home or End keys moved the selection, typing picked an item, or Ctrl or ⌘ with A selected every item of a listbox.
|
|
1192
|
+
- `find-in-page` (`Event`): The browser's find in page matched hidden content.
|
|
1193
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1194
|
+
|
|
1195
|
+
Why the selection changed:
|
|
1196
|
+
|
|
1197
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
1198
|
+
- `list-navigation` (`KeyboardEvent`): Arrow, Home or End keys moved the selection, typing picked an item, or Ctrl or ⌘ with A selected every item of a listbox.
|
|
1199
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1200
|
+
|
|
1201
|
+
And why the checkboxes changed:
|
|
1202
|
+
|
|
1203
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
1204
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1205
|
+
|
|
1206
|
+
### Controller
|
|
1207
|
+
|
|
1208
|
+
Create one with `createTreeController()` and pass it as `controller`.
|
|
1209
|
+
|
|
1210
|
+
- `getExpanded()` (`() => string[]`): The values of the expanded branches.
|
|
1211
|
+
- `setExpanded(expanded, options?)` (`(expanded: string[], options?: TreeChangeOptions<TreeExpandedChangeReason> | undefined) => boolean`): Expands exactly these branches. Returns false when canceled.
|
|
1212
|
+
- `isExpanded(value)` (`(value: string) => boolean`): Whether a branch is expanded.
|
|
1213
|
+
- `expand(value, options?)` (`(value: string, options?: TreeChangeOptions<TreeExpandedChangeReason> | undefined) => boolean`): Expands a branch, loading its children first with `loadChildren` when it has none.
|
|
1214
|
+
- `collapse(value, options?)` (`(value: string, options?: TreeChangeOptions<TreeExpandedChangeReason> | undefined) => boolean`): Collapses a branch. Focus inside it moves to the branch.
|
|
1215
|
+
- `toggle(value, options?)` (`(value: string, options?: TreeChangeOptions<TreeExpandedChangeReason> | undefined) => boolean`): Expands a collapsed branch, or collapses an expanded one.
|
|
1216
|
+
- `expandAll(options?)` (`(options?: TreeChangeOptions<TreeExpandedChangeReason> | undefined) => boolean`): Expands every branch, and every branch that renders inside one later.
|
|
1217
|
+
- `collapseAll(options?)` (`(options?: TreeChangeOptions<TreeExpandedChangeReason> | undefined) => boolean`): Collapses every branch.
|
|
1218
|
+
- `getValue()` (`() => string | string[] | null`): The selected value: a value or `null`, or a list with `mode: "multiple"`.
|
|
1219
|
+
- `setValue(value, options?)` (`(value: string | string[] | null, options?: TreeChangeOptions<TreeValueChangeReason> | undefined) => boolean`): Selects these items.
|
|
1220
|
+
- `getChecked()` (`() => string[]`): The checked value, as `checkStrategy` reports it.
|
|
1221
|
+
- `setChecked(checked, options?)` (`(checked: string[], options?: TreeChangeOptions<TreeCheckedChangeReason> | undefined) => boolean`): Sets the checked value.
|
|
1222
|
+
- `checkedState(value)` (`(value: string) => TreeCheckedState`): Whether an item's checkbox is checked, unchecked or `"indeterminate"`.
|
|
1223
|
+
- `reveal(value)` (`(value: string) => void`): Expands an item's ancestors, scrolls it into view and focuses it.
|
|
1224
|
+
- `focus(value?)` (`(value?: string | undefined) => void`): Focuses an item, or the item with the tab stop.
|
|
1225
|
+
|
|
1226
|
+
## Accessibility
|
|
1227
|
+
|
|
1228
|
+
The tree follows the [WAI-ARIA tree view pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/). Items have the `treeitem` role with `aria-level`, `aria-expanded` on branches, and `aria-checked` when the tree is checkable.
|
|
1229
|
+
|
|
1230
|
+
### Keyboard
|
|
1231
|
+
|
|
1232
|
+
The tree has one tab stop. Tab enters at the current page, the selected item, or the first item, then returns to the item focused last.
|
|
1233
|
+
|
|
1234
|
+
| Key | Action |
|
|
1235
|
+
| --------------------------- | ------------------------------------------------------ |
|
|
1236
|
+
| Down / Up Arrow | Focus the next or previous visible item. |
|
|
1237
|
+
| Right Arrow | Expand a branch, or enter its first child. |
|
|
1238
|
+
| Left Arrow | Collapse a branch, or focus its parent. |
|
|
1239
|
+
| Home / End | Focus the first or last visible item. |
|
|
1240
|
+
| Alt+Right / Alt+Left Arrow | Expand or collapse a branch and its descendants. |
|
|
1241
|
+
| `*` | Expand sibling branches. |
|
|
1242
|
+
| Enter | Select and run the item's action, or follow its link. |
|
|
1243
|
+
| Space | Check the item when checkable, otherwise select it. |
|
|
1244
|
+
| Shift+Down / Shift+Up Arrow | Extend a multiple selection. |
|
|
1245
|
+
| Cmd+A / Ctrl+A | Select visible items in multiple selection mode. |
|
|
1246
|
+
| Typing | Focus the next item whose label starts with that text. |
|
|
1247
|
+
|
|
1248
|
+
Option is Alt on a Mac. In right-to-left text, Left and Right swap.
|
|
1249
|
+
|
|
1250
|
+
### Tested Behavior
|
|
1251
|
+
|
|
1252
|
+
Every line below is an automated test.
|
|
1253
|
+
|
|
1254
|
+
**Expanding and collapsing**
|
|
1255
|
+
|
|
1256
|
+
- Branches start collapsed, and only top-level items show.
|
|
1257
|
+
- Clicking a branch expands it and shows its children.
|
|
1258
|
+
- Clicking the indicator expands without selecting.
|
|
1259
|
+
- A change can be canceled.
|
|
1260
|
+
- A default can expand nested branches.
|
|
1261
|
+
- A controller expands and collapses everything.
|
|
1262
|
+
- Reveal expands an item's ancestors and focuses it.
|
|
1263
|
+
|
|
1264
|
+
**Keyboard**
|
|
1265
|
+
|
|
1266
|
+
- Tab reaches the tree once, on the first item.
|
|
1267
|
+
- Down and Up move between the items that show.
|
|
1268
|
+
- Right expands a branch, then moves to its first child.
|
|
1269
|
+
- Right on a leaf does nothing.
|
|
1270
|
+
- Left collapses a branch, then moves to its parent.
|
|
1271
|
+
- Home and End move to the first and last items that show.
|
|
1272
|
+
- * expands every branch beside the focused item.
|
|
1273
|
+
- Alt+Right expands a branch and everything in it.
|
|
1274
|
+
- Typing moves to the next item that starts with the text.
|
|
1275
|
+
- Enter selects the item and runs its action.
|
|
1276
|
+
- A key on a nested item acts on that item only.
|
|
1277
|
+
|
|
1278
|
+
**Selection**
|
|
1279
|
+
|
|
1280
|
+
- Clicking an item selects it alone.
|
|
1281
|
+
- The selected item takes the tab stop.
|
|
1282
|
+
- Disabled items take focus but can't be selected.
|
|
1283
|
+
- With multiple selection, Shift+Down extends and Ctrl+A selects everything.
|
|
1284
|
+
- With automatic activation, moving focus selects.
|
|
1285
|
+
- Without selection, items have no aria-selected.
|
|
1286
|
+
|
|
1287
|
+
**Checkboxes**
|
|
1288
|
+
|
|
1289
|
+
- Checking a branch checks everything in it.
|
|
1290
|
+
- Space checks the focused item, and a disabled item keeps its check.
|
|
1291
|
+
- Checking a leaf leaves the selection and the branch alone.
|
|
1292
|
+
|
|
1293
|
+
**Positions**
|
|
1294
|
+
|
|
1295
|
+
- Every item says its level and place among its siblings.
|
|
1296
|
+
- The current page's item is marked.
|
|
1297
|
+
|
|
1298
|
+
Runs in React, Vue and web components.
|
|
1299
|
+
|
|
1300
|
+
Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:
|
|
1301
|
+
|
|
1302
|
+
- Announcements while using the tree with a keyboard.
|
|
1303
|
+
- Expanding the focused item is announced on Apple platforms.
|
|
1304
|
+
- Nothing is announced elsewhere, where screen readers read the change.
|
|
1305
|
+
|
|
1306
|
+
Runs in React, Vue and web components.
|