@spaethtech/svelte-ui 0.21.1-dev.127.89441ce → 0.21.1-dev.128.59d3487
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/dist/components/Tree/Tree.svelte +173 -58
- package/dist/components/Tree/Tree.svelte.d.ts +17 -1
- package/dist/i18n.d.ts +3 -0
- package/dist/i18n.js +2 -0
- package/docs/components.md +11 -2
- package/package.json +1 -1
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Tree — a hierarchical, expandable tree view (ARIA `tree`). Data-driven nodes; expand/collapse +
|
|
4
4
|
* single-select with roving-tabindex keyboard nav (↑/↓ move, →/← expand/collapse, Enter/Space select).
|
|
5
|
-
* Shared `variant`/`size` axes, `--ui-*` tokens.
|
|
5
|
+
* Shared `variant`/`size` axes, `--ui-*` tokens. Supports LAZY children (a `hasChildren` hint +
|
|
6
|
+
* `loadChildren`/`onexpand`), a per-node `oncontextmenu`/`data-key` for right-click menus, and
|
|
7
|
+
* `expandOnSelect={false}` to keep selection and expansion separate. See Tree.spec.md.
|
|
6
8
|
*/
|
|
7
9
|
-->
|
|
8
10
|
<script lang="ts" module>
|
|
@@ -14,22 +16,33 @@
|
|
|
14
16
|
icon?: Snippet;
|
|
15
17
|
children?: TreeNode[];
|
|
16
18
|
disabled?: boolean;
|
|
19
|
+
/** Lazy-tree hint: `true` = has children (show the chevron) even before they're loaded; `false` =
|
|
20
|
+
* definitely a leaf (no chevron); `undefined` = inferred from `children`. */
|
|
21
|
+
hasChildren?: boolean;
|
|
17
22
|
};
|
|
18
23
|
</script>
|
|
19
24
|
|
|
20
25
|
<script lang="ts">
|
|
21
26
|
import IconChevronRight from "~icons/mdi/chevron-right";
|
|
27
|
+
import IconLoading from "~icons/mdi/loading";
|
|
22
28
|
import type { HTMLAttributes } from "svelte/elements";
|
|
23
29
|
import type { Variant } from "../../types/variants.js";
|
|
24
30
|
import { variantToken } from "../../types/variants.js";
|
|
25
31
|
import type { Size } from "../../types/sizes.js";
|
|
26
32
|
import { responsiveClasses, type Responsive } from "../../types/responsive.js";
|
|
33
|
+
import { uiI18n } from "../../i18n.js";
|
|
34
|
+
|
|
35
|
+
const i18n = uiI18n();
|
|
27
36
|
|
|
28
37
|
let {
|
|
29
38
|
items,
|
|
30
39
|
expanded = $bindable([]),
|
|
31
40
|
selected = $bindable(""),
|
|
32
41
|
onselect,
|
|
42
|
+
onexpand,
|
|
43
|
+
oncontextmenu,
|
|
44
|
+
loadChildren,
|
|
45
|
+
expandOnSelect = true,
|
|
33
46
|
variant = "primary",
|
|
34
47
|
size = "md",
|
|
35
48
|
class: cls = "",
|
|
@@ -41,12 +54,25 @@
|
|
|
41
54
|
/** Key of the selected node (bindable). */
|
|
42
55
|
selected?: string;
|
|
43
56
|
onselect?: (node: TreeNode, key: string) => void;
|
|
57
|
+
/** Fired when a node is expanded — use with a consumer-managed `items` to load children yourself
|
|
58
|
+
* (or use `loadChildren` to let the Tree own the fetch + spinner). */
|
|
59
|
+
onexpand?: (node: TreeNode, key: string) => void;
|
|
60
|
+
/** Right-click on a row. The event is NOT prevented, so a wrapping `ContextMenu` still opens;
|
|
61
|
+
* each row also carries `data-key` for consumers that read the target instead. */
|
|
62
|
+
oncontextmenu?: (event: MouseEvent, node: TreeNode, key: string) => void;
|
|
63
|
+
/** Lazily load a node's children. Called once when an unloaded node (`hasChildren: true`, no
|
|
64
|
+
* `children`) is first expanded: the Tree shows a spinner row, caches the resolved children, and
|
|
65
|
+
* shows an inline error row on rejection. */
|
|
66
|
+
loadChildren?: (node: TreeNode, key: string) => Promise<TreeNode[]>;
|
|
67
|
+
/** `true` (default): selecting a parent also toggles it (current behaviour). `false`: select and
|
|
68
|
+
* expand are separate — expand only via the chevron or →/←, as desktop explorers do. */
|
|
69
|
+
expandOnSelect?: boolean;
|
|
44
70
|
variant?: Variant;
|
|
45
71
|
size?: Responsive<Size>;
|
|
46
72
|
class?: string;
|
|
47
|
-
// `style` omitted
|
|
48
|
-
//
|
|
49
|
-
} & Omit<HTMLAttributes<HTMLElement>, "class" | "style" | "onselect"> = $props();
|
|
73
|
+
// `style`/`onselect`/`oncontextmenu` omitted from the DOM attrs: rows compute their own style, and
|
|
74
|
+
// the latter two have this component's own (node-aware) signatures.
|
|
75
|
+
} & Omit<HTMLAttributes<HTMLElement>, "class" | "style" | "onselect" | "oncontextmenu"> = $props();
|
|
50
76
|
|
|
51
77
|
const token = $derived(variantToken[variant]);
|
|
52
78
|
const rowH: Record<Size, string> = { sm: "h-7 text-xs", md: "h-8 text-sm", lg: "h-10 text-base" };
|
|
@@ -56,37 +82,95 @@
|
|
|
56
82
|
lg: "[&_svg]:w-5 [&_svg]:h-5",
|
|
57
83
|
};
|
|
58
84
|
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
85
|
+
// Lazily-loaded children + per-node load state, keyed by node key. Kept internal so `items` stays
|
|
86
|
+
// immutable for the consumer; `loadChildren`'s result is cached here and merged into the flat view.
|
|
87
|
+
let loadedChildren = $state<Record<string, TreeNode[]>>({});
|
|
88
|
+
let loadingKeys = $state<Record<string, boolean>>({});
|
|
89
|
+
let loadErrors = $state<Record<string, string>>({});
|
|
90
|
+
|
|
91
|
+
const childrenOf = (node: TreeNode, key: string): TreeNode[] | undefined =>
|
|
92
|
+
node.children ?? loadedChildren[key];
|
|
93
|
+
const nodeHasChildren = (node: TreeNode, key: string): boolean =>
|
|
94
|
+
node.hasChildren ?? (childrenOf(node, key)?.length ?? 0) > 0;
|
|
95
|
+
|
|
96
|
+
type Flat = {
|
|
97
|
+
key: string;
|
|
98
|
+
node: TreeNode;
|
|
99
|
+
level: number;
|
|
100
|
+
hasChildren: boolean;
|
|
101
|
+
open: boolean;
|
|
102
|
+
/** A synthetic, non-interactive row for a lazy node's load state. */
|
|
103
|
+
status?: "loading" | "error";
|
|
104
|
+
message?: string;
|
|
105
|
+
};
|
|
106
|
+
// Flattened list of VISIBLE rows (children shown only under expanded parents), plus synthetic
|
|
107
|
+
// loading/error rows for lazy nodes. Drives rendering; `navFlat` (real nodes only) drives keyboard nav.
|
|
62
108
|
const flat = $derived.by<Flat[]>(() => {
|
|
63
109
|
const out: Flat[] = [];
|
|
64
110
|
const walk = (nodes: TreeNode[], level: number, prefix: string) => {
|
|
65
111
|
nodes.forEach((node, i) => {
|
|
66
112
|
const key = node.id ?? `${prefix}${i}`;
|
|
67
|
-
const hasChildren =
|
|
113
|
+
const hasChildren = nodeHasChildren(node, key);
|
|
68
114
|
const open = expanded.includes(key);
|
|
69
115
|
out.push({ key, node, level, hasChildren, open });
|
|
70
|
-
if (hasChildren && open)
|
|
116
|
+
if (hasChildren && open) {
|
|
117
|
+
const kids = childrenOf(node, key);
|
|
118
|
+
if (kids && kids.length) walk(kids, level + 1, `${key}>`);
|
|
119
|
+
else if (loadingKeys[key])
|
|
120
|
+
out.push({ key: `${key}::loading`, node, level: level + 1, hasChildren: false, open: false, status: "loading" });
|
|
121
|
+
else if (loadErrors[key])
|
|
122
|
+
out.push({ key: `${key}::error`, node, level: level + 1, hasChildren: false, open: false, status: "error", message: loadErrors[key] });
|
|
123
|
+
}
|
|
71
124
|
});
|
|
72
125
|
};
|
|
73
126
|
walk(items, 0, "");
|
|
74
127
|
return out;
|
|
75
128
|
});
|
|
129
|
+
const navFlat = $derived(flat.filter((f) => !f.status));
|
|
76
130
|
|
|
77
131
|
let rowRefs = $state<Record<string, HTMLElement | undefined>>({});
|
|
78
132
|
let activeKey = $state("");
|
|
79
133
|
const tabKey = $derived(
|
|
80
|
-
|
|
134
|
+
navFlat.some((f) => f.key === activeKey)
|
|
135
|
+
? activeKey
|
|
136
|
+
: (navFlat.find((f) => !f.node.disabled)?.key ?? ""),
|
|
81
137
|
);
|
|
82
138
|
|
|
83
|
-
|
|
84
|
-
|
|
139
|
+
function maybeLoad(f: Flat) {
|
|
140
|
+
if (!loadChildren) return;
|
|
141
|
+
const kids = childrenOf(f.node, f.key);
|
|
142
|
+
if ((kids && kids.length) || loadingKeys[f.key] || loadedChildren[f.key]) return;
|
|
143
|
+
loadingKeys = { ...loadingKeys, [f.key]: true };
|
|
144
|
+
const { [f.key]: _drop, ...restErrors } = loadErrors;
|
|
145
|
+
loadErrors = restErrors;
|
|
146
|
+
loadChildren(f.node, f.key)
|
|
147
|
+
.then((res) => {
|
|
148
|
+
loadedChildren = { ...loadedChildren, [f.key]: res };
|
|
149
|
+
})
|
|
150
|
+
.catch((err) => {
|
|
151
|
+
loadErrors = { ...loadErrors, [f.key]: err instanceof Error ? err.message : String(err) };
|
|
152
|
+
})
|
|
153
|
+
.finally(() => {
|
|
154
|
+
const { [f.key]: _done, ...restLoading } = loadingKeys;
|
|
155
|
+
loadingKeys = restLoading;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function setOpen(f: Flat, open: boolean) {
|
|
160
|
+
if (open === f.open) return;
|
|
161
|
+
expanded = open ? [...expanded, f.key] : expanded.filter((k) => k !== f.key);
|
|
162
|
+
if (open) {
|
|
163
|
+
onexpand?.(f.node, f.key);
|
|
164
|
+
maybeLoad(f);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const toggleOpen = (f: Flat) => setOpen(f, !f.open);
|
|
168
|
+
|
|
85
169
|
function selectNode(f: Flat) {
|
|
86
170
|
if (f.node.disabled) return;
|
|
87
171
|
selected = f.key;
|
|
88
172
|
onselect?.(f.node, f.key);
|
|
89
|
-
if (f.hasChildren)
|
|
173
|
+
if (f.hasChildren && expandOnSelect) toggleOpen(f);
|
|
90
174
|
}
|
|
91
175
|
const focusKey = (key: string) => {
|
|
92
176
|
activeKey = key;
|
|
@@ -94,42 +178,42 @@
|
|
|
94
178
|
};
|
|
95
179
|
|
|
96
180
|
function onKeydown(e: KeyboardEvent) {
|
|
97
|
-
const idx =
|
|
181
|
+
const idx = navFlat.findIndex((f) => f.key === tabKey);
|
|
98
182
|
if (idx < 0) return;
|
|
99
|
-
const f =
|
|
183
|
+
const f = navFlat[idx];
|
|
100
184
|
switch (e.key) {
|
|
101
185
|
case "ArrowDown":
|
|
102
186
|
e.preventDefault();
|
|
103
|
-
if (idx <
|
|
187
|
+
if (idx < navFlat.length - 1) focusKey(navFlat[idx + 1].key);
|
|
104
188
|
break;
|
|
105
189
|
case "ArrowUp":
|
|
106
190
|
e.preventDefault();
|
|
107
|
-
if (idx > 0) focusKey(
|
|
191
|
+
if (idx > 0) focusKey(navFlat[idx - 1].key);
|
|
108
192
|
break;
|
|
109
193
|
case "ArrowRight":
|
|
110
194
|
e.preventDefault();
|
|
111
|
-
if (f.hasChildren && !f.open)
|
|
112
|
-
else if (f.hasChildren &&
|
|
195
|
+
if (f.hasChildren && !f.open) setOpen(f, true);
|
|
196
|
+
else if (f.hasChildren && navFlat[idx + 1]) focusKey(navFlat[idx + 1].key);
|
|
113
197
|
break;
|
|
114
198
|
case "ArrowLeft":
|
|
115
199
|
e.preventDefault();
|
|
116
|
-
if (f.hasChildren && f.open)
|
|
200
|
+
if (f.hasChildren && f.open) setOpen(f, false);
|
|
117
201
|
else {
|
|
118
202
|
// move to parent (previous node at a lower level)
|
|
119
203
|
for (let j = idx - 1; j >= 0; j--)
|
|
120
|
-
if (
|
|
121
|
-
focusKey(
|
|
204
|
+
if (navFlat[j].level < f.level) {
|
|
205
|
+
focusKey(navFlat[j].key);
|
|
122
206
|
break;
|
|
123
207
|
}
|
|
124
208
|
}
|
|
125
209
|
break;
|
|
126
210
|
case "Home":
|
|
127
211
|
e.preventDefault();
|
|
128
|
-
focusKey(
|
|
212
|
+
focusKey(navFlat[0].key);
|
|
129
213
|
break;
|
|
130
214
|
case "End":
|
|
131
215
|
e.preventDefault();
|
|
132
|
-
focusKey(
|
|
216
|
+
focusKey(navFlat[navFlat.length - 1].key);
|
|
133
217
|
break;
|
|
134
218
|
case "Enter":
|
|
135
219
|
case " ":
|
|
@@ -148,40 +232,71 @@
|
|
|
148
232
|
onkeydown={onKeydown}
|
|
149
233
|
>
|
|
150
234
|
{#each flat as f (f.key)}
|
|
151
|
-
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
aria-selected={active}
|
|
159
|
-
aria-expanded={f.hasChildren ? f.open : undefined}
|
|
160
|
-
aria-disabled={f.node.disabled || undefined}
|
|
161
|
-
tabindex={f.key === tabKey ? 0 : -1}
|
|
162
|
-
onclick={() => selectNode(f)}
|
|
163
|
-
onfocus={() => (activeKey = f.key)}
|
|
164
|
-
style="padding-inline-start: {f.level * 1.25 + 0.5}rem; {active
|
|
165
|
-
? `background-color: color-mix(in srgb, var(${token}) 16%, transparent);`
|
|
166
|
-
: ''}"
|
|
167
|
-
class="group flex items-center gap-1.5 rounded-[var(--ui-border-radius)] pr-2 transition-colors {responsiveClasses(
|
|
168
|
-
size,
|
|
169
|
-
rowH,
|
|
170
|
-
)} {f.node.disabled
|
|
171
|
-
? 'opacity-50 cursor-not-allowed'
|
|
172
|
-
: 'cursor-pointer hover:[background-color:var(--ui-color-hover)]'} focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-text)_70%,transparent)] focus-visible:[outline-offset:-2px]"
|
|
173
|
-
>
|
|
174
|
-
{#if f.hasChildren}
|
|
175
|
-
<span
|
|
176
|
-
class="inline-flex shrink-0 items-center justify-center transition-transform duration-150 {f.open
|
|
177
|
-
? 'rotate-90'
|
|
178
|
-
: ''}"><IconChevronRight /></span>
|
|
179
|
-
{:else}
|
|
235
|
+
{#if f.status}
|
|
236
|
+
<!-- Synthetic lazy-load status row (non-interactive). -->
|
|
237
|
+
<li role="none" class="list-none">
|
|
238
|
+
<div
|
|
239
|
+
class="flex items-center gap-1.5 pr-2 {responsiveClasses(size, rowH)} [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]"
|
|
240
|
+
style="padding-inline-start: {f.level * 1.25 + 0.5}rem;"
|
|
241
|
+
>
|
|
180
242
|
<span class="inline-block w-4 shrink-0"></span>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
243
|
+
{#if f.status === "loading"}
|
|
244
|
+
<span class="inline-flex shrink-0 animate-spin"><IconLoading /></span>
|
|
245
|
+
<span class="truncate">{i18n.messages.loading}</span>
|
|
246
|
+
{:else}
|
|
247
|
+
<span class="truncate [color:var(--ui-color-error)]">{f.message || i18n.messages.loadFailed}</span>
|
|
248
|
+
{/if}
|
|
249
|
+
</div>
|
|
250
|
+
</li>
|
|
251
|
+
{:else}
|
|
252
|
+
{@const active = selected === f.key}
|
|
253
|
+
<li role="none" class="list-none">
|
|
254
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
255
|
+
<div
|
|
256
|
+
bind:this={rowRefs[f.key]}
|
|
257
|
+
role="treeitem"
|
|
258
|
+
data-key={f.key}
|
|
259
|
+
aria-level={f.level + 1}
|
|
260
|
+
aria-selected={active}
|
|
261
|
+
aria-expanded={f.hasChildren ? f.open : undefined}
|
|
262
|
+
aria-disabled={f.node.disabled || undefined}
|
|
263
|
+
tabindex={f.key === tabKey ? 0 : -1}
|
|
264
|
+
onclick={() => selectNode(f)}
|
|
265
|
+
oncontextmenu={(e) => oncontextmenu?.(e, f.node, f.key)}
|
|
266
|
+
onfocus={() => (activeKey = f.key)}
|
|
267
|
+
style="padding-inline-start: {f.level * 1.25 + 0.5}rem; {active
|
|
268
|
+
? `background-color: color-mix(in srgb, var(${token}) 16%, transparent);`
|
|
269
|
+
: ''}"
|
|
270
|
+
class="group flex items-center gap-1.5 rounded-[var(--ui-border-radius)] pr-2 transition-colors {responsiveClasses(
|
|
271
|
+
size,
|
|
272
|
+
rowH,
|
|
273
|
+
)} {f.node.disabled
|
|
274
|
+
? 'opacity-50 cursor-not-allowed'
|
|
275
|
+
: 'cursor-pointer hover:[background-color:var(--ui-color-hover)]'} focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-text)_70%,transparent)] focus-visible:[outline-offset:-2px]"
|
|
276
|
+
>
|
|
277
|
+
{#if f.hasChildren && !expandOnSelect}
|
|
278
|
+
<!-- Select and expand are separate: the chevron is its own control (keyboard expand is
|
|
279
|
+
still →/←). A real <button> keeps it accessible without a roving tab stop. -->
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
tabindex="-1"
|
|
283
|
+
aria-label={f.open ? i18n.messages.collapse : i18n.messages.expand}
|
|
284
|
+
class="inline-flex shrink-0 cursor-pointer items-center justify-center rounded transition-transform duration-150 {f.open
|
|
285
|
+
? 'rotate-90'
|
|
286
|
+
: ''}"
|
|
287
|
+
onclick={(e) => (e.stopPropagation(), toggleOpen(f))}><IconChevronRight /></button>
|
|
288
|
+
{:else if f.hasChildren}
|
|
289
|
+
<span
|
|
290
|
+
class="inline-flex shrink-0 items-center justify-center transition-transform duration-150 {f.open
|
|
291
|
+
? 'rotate-90'
|
|
292
|
+
: ''}"><IconChevronRight /></span>
|
|
293
|
+
{:else}
|
|
294
|
+
<span class="inline-block w-4 shrink-0"></span>
|
|
295
|
+
{/if}
|
|
296
|
+
{#if f.node.icon}<span class="inline-flex shrink-0">{@render f.node.icon()}</span>{/if}
|
|
297
|
+
<span class="truncate [color:var(--ui-color-text)]">{f.node.label}</span>
|
|
298
|
+
</div>
|
|
299
|
+
</li>
|
|
300
|
+
{/if}
|
|
186
301
|
{/each}
|
|
187
302
|
</ul>
|
|
@@ -6,6 +6,9 @@ export type TreeNode = {
|
|
|
6
6
|
icon?: Snippet;
|
|
7
7
|
children?: TreeNode[];
|
|
8
8
|
disabled?: boolean;
|
|
9
|
+
/** Lazy-tree hint: `true` = has children (show the chevron) even before they're loaded; `false` =
|
|
10
|
+
* definitely a leaf (no chevron); `undefined` = inferred from `children`. */
|
|
11
|
+
hasChildren?: boolean;
|
|
9
12
|
};
|
|
10
13
|
import type { HTMLAttributes } from "svelte/elements";
|
|
11
14
|
import type { Variant } from "../../types/variants.js";
|
|
@@ -18,10 +21,23 @@ type $$ComponentProps = {
|
|
|
18
21
|
/** Key of the selected node (bindable). */
|
|
19
22
|
selected?: string;
|
|
20
23
|
onselect?: (node: TreeNode, key: string) => void;
|
|
24
|
+
/** Fired when a node is expanded — use with a consumer-managed `items` to load children yourself
|
|
25
|
+
* (or use `loadChildren` to let the Tree own the fetch + spinner). */
|
|
26
|
+
onexpand?: (node: TreeNode, key: string) => void;
|
|
27
|
+
/** Right-click on a row. The event is NOT prevented, so a wrapping `ContextMenu` still opens;
|
|
28
|
+
* each row also carries `data-key` for consumers that read the target instead. */
|
|
29
|
+
oncontextmenu?: (event: MouseEvent, node: TreeNode, key: string) => void;
|
|
30
|
+
/** Lazily load a node's children. Called once when an unloaded node (`hasChildren: true`, no
|
|
31
|
+
* `children`) is first expanded: the Tree shows a spinner row, caches the resolved children, and
|
|
32
|
+
* shows an inline error row on rejection. */
|
|
33
|
+
loadChildren?: (node: TreeNode, key: string) => Promise<TreeNode[]>;
|
|
34
|
+
/** `true` (default): selecting a parent also toggles it (current behaviour). `false`: select and
|
|
35
|
+
* expand are separate — expand only via the chevron or →/←, as desktop explorers do. */
|
|
36
|
+
expandOnSelect?: boolean;
|
|
21
37
|
variant?: Variant;
|
|
22
38
|
size?: Responsive<Size>;
|
|
23
39
|
class?: string;
|
|
24
|
-
} & Omit<HTMLAttributes<HTMLElement>, "class" | "style" | "onselect">;
|
|
40
|
+
} & Omit<HTMLAttributes<HTMLElement>, "class" | "style" | "onselect" | "oncontextmenu">;
|
|
25
41
|
declare const Tree: import("svelte").Component<$$ComponentProps, {}, "selected" | "expanded">;
|
|
26
42
|
type Tree = ReturnType<typeof Tree>;
|
|
27
43
|
export default Tree;
|
package/dist/i18n.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export type UiMessages = {
|
|
|
25
25
|
next: Msg;
|
|
26
26
|
first: Msg;
|
|
27
27
|
last: Msg;
|
|
28
|
+
/** Tree: the per-row chevron toggle (accessible name), when select and expand are separate. */
|
|
29
|
+
expand: Msg;
|
|
30
|
+
collapse: Msg;
|
|
28
31
|
pagination: Msg;
|
|
29
32
|
pageNumber: Msg;
|
|
30
33
|
perPage: Msg;
|
package/dist/i18n.js
CHANGED
package/docs/components.md
CHANGED
|
@@ -850,8 +850,17 @@ Hierarchical, expandable tree view (ARIA `tree`).
|
|
|
850
850
|
|
|
851
851
|
- **Location**: `src/lib/components/Tree/Tree.svelte`
|
|
852
852
|
- **Axes**: `variant` (selected-row tint), `size`
|
|
853
|
-
- **Props**: `items` (`TreeNode[]` = `{ label, id?, icon?, children?, disabled? }`),
|
|
854
|
-
(bindable `string[]`), `selected` (bindable key), `onselect`, `class`
|
|
853
|
+
- **Props**: `items` (`TreeNode[]` = `{ label, id?, icon?, children?, disabled?, hasChildren? }`),
|
|
854
|
+
`expanded` (bindable `string[]`), `selected` (bindable key), `onselect`, `class`
|
|
855
|
+
- **Lazy children**: set `hasChildren: true` on a node whose `children` aren't loaded (it gets a
|
|
856
|
+
chevron). Provide **`loadChildren(node, key) => Promise<TreeNode[]>`** — it's called once on first
|
|
857
|
+
expand; the Tree shows a spinner row, caches the result (so `items` stays immutable), and shows an
|
|
858
|
+
inline error row (the rejection's message, else localized `loadFailed`) on failure. Or use
|
|
859
|
+
**`onexpand(node, key)`** to load into your own `items`.
|
|
860
|
+
- **Right-click**: **`oncontextmenu(event, node, key)`** per row (the event isn't prevented, so a
|
|
861
|
+
wrapping `ContextMenu` still opens); every `role="treeitem"` also carries **`data-key`**.
|
|
862
|
+
- **Select vs. expand**: **`expandOnSelect`** (default `true`) — set `false` so a row click only
|
|
863
|
+
selects and expansion is via the chevron (a real `<button>`) or →/←, as desktop explorers do.
|
|
855
864
|
- **Keyboard**: roving tabindex — ↑/↓ move, → expand/into child, ← collapse/to parent, Enter/Space
|
|
856
865
|
select, Home/End
|
|
857
866
|
- **Accessibility**: `role="tree"`/`treeitem` with `aria-level`/`-expanded`/`-selected`/`-disabled`
|