Skip to content

Commit 2457bc9

Browse files
authored
feat/rework-recursive-tree-view (#2069)
1 parent b3f4616 commit 2457bc9

File tree

10 files changed

+683
-293
lines changed

10 files changed

+683
-293
lines changed

.changeset/nine-worms-cheat.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@skeletonlabs/skeleton": minor
3+
---
4+
5+
feat: Multiple revisions and updates to the Tree View feature:
6+
- Enhanced and properly named non-recursive tree-view events.
7+
- Separated the recursive tree-view under the new component RecursiveTreeView.
8+
- RecursiveTreeView now utilizes the `relational` prop to enable relational checking.
9+
- RecursiveTreeView is now using ID arrays with 2-way binding to control the tree-view state, including:
10+
- `expandedNodes`
11+
- `disabledNodes`
12+
- `checkedNodes`
13+
- `indeterminateNodes` (has effect only in multiple relational mode)
14+
- TreeViewNode now requires a unique ID to support the new checking system.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<script lang="ts">
2+
import { setContext } from 'svelte';
3+
4+
// Types
5+
import type { CssClasses, TreeViewNode } from '../../index.js';
6+
import RecursiveTreeViewItem from './RecursiveTreeViewItem.svelte';
7+
8+
// Props (parent)
9+
/** Enable tree-view selection. */
10+
export let selection = false;
11+
/** Enable selection of multiple items. */
12+
export let multiple = false;
13+
/** Enable relational checking. */
14+
export let relational = false;
15+
/**
16+
* Provide data-driven nodes.
17+
* @type {TreeViewNode[]}
18+
*/
19+
export let nodes: TreeViewNode[] = [];
20+
/**
21+
* provides id's of expanded nodes
22+
* @type {string[]}
23+
*/
24+
export let expandedNodes: string[] = [];
25+
/**
26+
* provides id's of disabled nodes
27+
* @type {string[]}
28+
*/
29+
export let disabledNodes: string[] = [];
30+
/**
31+
* provides id's of checked nodes
32+
* @type {string[]}
33+
*/
34+
export let checkedNodes: string[] = [];
35+
/**
36+
* provides id's of indeterminate nodes
37+
* @type {string[]}
38+
*/
39+
export let indeterminateNodes: string[] = [];
40+
/** Provide classes to set the tree width. */
41+
export let width: CssClasses = 'w-full';
42+
/** Provide classes to set the vertical spacing between items. */
43+
export let spacing: CssClasses = 'space-y-1';
44+
45+
// Props (children)
46+
/** Set open by default on load. */
47+
export let open = false;
48+
/** Set the tree disabled state */
49+
export let disabled = false;
50+
/** Provide classes to set the tree item padding styles. */
51+
export let padding: CssClasses = 'py-4 px-4';
52+
/** Provide classes to set the tree children indentation */
53+
export let indent: CssClasses = 'ml-4';
54+
/** Provide classes to set the tree item hover styles. */
55+
export let hover: CssClasses = 'hover:variant-soft';
56+
/** Provide classes to set the tree item rounded styles. */
57+
export let rounded: CssClasses = 'rounded-container-token';
58+
59+
// Props (symbols)
60+
/** Set the rotation of the item caret in the open state. */
61+
export let caretOpen: CssClasses = 'rotate-180';
62+
/** Set the rotation of the item caret in the closed state. */
63+
export let caretClosed: CssClasses = '';
64+
/* Set the hyphen symbol opacity for non-expandable rows. */
65+
export let hyphenOpacity: CssClasses = 'opacity-10';
66+
67+
// Props (regions)
68+
/** Provide arbitrary classes to the tree item summary region. */
69+
export let regionSummary: CssClasses = '';
70+
/** Provide arbitrary classes to the symbol icon region. */
71+
export let regionSymbol: CssClasses = '';
72+
/** Provide arbitrary classes to the children region. */
73+
export let regionChildren: CssClasses = '';
74+
75+
// Props A11y
76+
/** Provide the ARIA labelledby value. */
77+
export let labelledby = '';
78+
79+
// Context API
80+
setContext('open', open);
81+
setContext('selection', selection);
82+
setContext('multiple', multiple);
83+
setContext('relational', relational);
84+
setContext('disabled', disabled);
85+
setContext('padding', padding);
86+
setContext('indent', indent);
87+
setContext('hover', hover);
88+
setContext('rounded', rounded);
89+
setContext('caretOpen', caretOpen);
90+
setContext('caretClosed', caretClosed);
91+
setContext('hyphenOpacity', hyphenOpacity);
92+
setContext('regionSummary', regionSummary);
93+
setContext('regionSymbol', regionSymbol);
94+
setContext('regionChildren', regionChildren);
95+
96+
// Reactive
97+
$: classesBase = `${width} ${spacing} ${$$props.class ?? ''}`;
98+
</script>
99+
100+
<div
101+
class="tree {classesBase}"
102+
data-testid="tree"
103+
role="tree"
104+
aria-multiselectable={multiple}
105+
aria-label={labelledby}
106+
aria-disabled={disabled}
107+
>
108+
{#if nodes && nodes.length > 0}
109+
<RecursiveTreeViewItem {nodes} bind:expandedNodes bind:disabledNodes bind:checkedNodes bind:indeterminateNodes />
110+
{/if}
111+
</div>
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<script lang="ts">
2+
import TreeViewItem from './TreeViewItem.svelte';
3+
import RecursiveTreeViewItem from './RecursiveTreeViewItem.svelte';
4+
import type { TreeViewNode } from './types.js';
5+
import { getContext, onMount, tick } from 'svelte';
6+
7+
// this can't be passed using context, since we have to pass it to recursive children.
8+
/** Provide data-driven nodes. */
9+
export let nodes: TreeViewNode[] = [];
10+
11+
/**
12+
* provides id's of expanded nodes
13+
* @type {string[]}
14+
*/
15+
export let expandedNodes: string[] = [];
16+
/**
17+
* provides id's of disabled nodes
18+
* @type {string[]}
19+
*/
20+
export let disabledNodes: string[] = [];
21+
/**
22+
* provides id's of checked nodes
23+
* @type {string[]}
24+
*/
25+
export let checkedNodes: string[] = [];
26+
/**
27+
* provides id's of indeterminate nodes
28+
* @type {string[]}
29+
*/
30+
export let indeterminateNodes: string[] = [];
31+
32+
// Context API
33+
let selection: boolean = getContext('selection');
34+
let multiple: boolean = getContext('multiple');
35+
let relational: boolean = getContext('relational');
36+
37+
let tempCheckedNodes: string[] = [];
38+
39+
// Locals
40+
let group: unknown;
41+
let name = '';
42+
43+
function toggleNode(node: TreeViewNode, open: boolean) {
44+
// toggle only nodes with children
45+
if (!node.children?.length) return;
46+
if (open) {
47+
// node is not registered as opened
48+
if (!expandedNodes.includes(node.id)) {
49+
expandedNodes.push(node.id);
50+
expandedNodes = expandedNodes;
51+
}
52+
} else {
53+
// node is registered as open
54+
if (expandedNodes.includes(node.id)) {
55+
expandedNodes.splice(expandedNodes.indexOf(node.id), 1);
56+
expandedNodes = expandedNodes;
57+
}
58+
}
59+
}
60+
61+
function checkNode(node: TreeViewNode, checked: boolean, indeterminate: boolean) {
62+
if (checked) {
63+
// node is not registered as checked
64+
if (!checkedNodes.includes(node.id)) {
65+
checkedNodes.push(node.id);
66+
checkedNodes = checkedNodes;
67+
}
68+
69+
// node is not indeterminate but registered as indeterminate
70+
if (!indeterminate && indeterminateNodes.includes(node.id)) {
71+
indeterminateNodes.splice(indeterminateNodes.indexOf(node.id), 1);
72+
indeterminateNodes = indeterminateNodes;
73+
}
74+
} else {
75+
// node is registered as checked
76+
if (checkedNodes.includes(node.id)) {
77+
checkedNodes.splice(checkedNodes.indexOf(node.id), 1);
78+
checkedNodes = checkedNodes;
79+
}
80+
81+
// node is indeterminate but not registered as indeterminate
82+
if (indeterminate && !indeterminateNodes.includes(node.id)) {
83+
indeterminateNodes.push(node.id);
84+
indeterminateNodes = indeterminateNodes;
85+
// node is not indeterminate but registered as indeterminate
86+
} else if (!indeterminate && indeterminateNodes.includes(node.id)) {
87+
indeterminateNodes.splice(indeterminateNodes.indexOf(node.id), 1);
88+
indeterminateNodes = indeterminateNodes;
89+
}
90+
}
91+
}
92+
93+
// init check flow will messup the checked nodes, so we save it to reassign it onMount.
94+
tempCheckedNodes = [...checkedNodes];
95+
onMount(async () => {
96+
if (selection) {
97+
// random number as name
98+
name = String(Math.random());
99+
100+
// init groups if not initialized yet
101+
if (group === undefined) {
102+
if (multiple) {
103+
group = [];
104+
nodes.forEach((node) => {
105+
if (checkedNodes.includes(node.id) && Array.isArray(group)) group.push(node.id);
106+
});
107+
group = group;
108+
} else if (!nodes.some((node) => checkedNodes.includes(node.id))) {
109+
group = '';
110+
}
111+
}
112+
113+
// remove relational links
114+
if (!relational) treeItems = [];
115+
116+
// reassign checkNodes to ensure component starting with the correct check values.
117+
checkedNodes = [];
118+
await tick();
119+
checkedNodes = [...tempCheckedNodes];
120+
}
121+
});
122+
123+
// important to pass children up to items (recursively)
124+
export let treeItems: TreeViewItem[] = [];
125+
let children: TreeViewItem[][] = [];
126+
</script>
127+
128+
{#if nodes && nodes.length > 0}
129+
{#each nodes as node, i}
130+
<TreeViewItem
131+
bind:this={treeItems[i]}
132+
bind:children={children[i]}
133+
bind:group
134+
bind:name
135+
bind:value={node.id}
136+
hideLead={!node.lead}
137+
hideChildren={!node.children || node.children.length === 0}
138+
open={expandedNodes.includes(node.id)}
139+
disabled={disabledNodes.includes(node.id)}
140+
checked={checkedNodes.includes(node.id)}
141+
indeterminate={indeterminateNodes.includes(node.id)}
142+
on:toggle={(e) => toggleNode(node, e.detail.open)}
143+
on:groupChange={(e) => checkNode(node, e.detail.checked, e.detail.indeterminate)}
144+
>
145+
{@html node.content}
146+
<svelte:fragment slot="lead">
147+
{@html node.lead}
148+
</svelte:fragment>
149+
<svelte:fragment slot="children">
150+
<RecursiveTreeViewItem
151+
nodes={node.children}
152+
bind:expandedNodes
153+
bind:disabledNodes
154+
bind:checkedNodes
155+
bind:indeterminateNodes
156+
bind:treeItems={children[i]}
157+
/>
158+
</svelte:fragment>
159+
</TreeViewItem>
160+
{/each}
161+
{/if}

packages/skeleton/src/lib/components/TreeView/TreeView.svelte

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,13 @@
22
import { setContext } from 'svelte';
33
44
// Types
5-
import type { CssClasses, TreeViewNode } from '../../index.js';
6-
import TreeViewDataDrivenItem from './TreeViewDataDrivenItem.svelte';
5+
import type { CssClasses } from '../../index.js';
76
87
// Props (parent)
98
/** Enable tree-view selection. */
109
export let selection = false;
1110
/** Enable selection of multiple items. */
1211
export let multiple = false;
13-
/**
14-
* Provide data-driven nodes.
15-
* @type {TreeViewNode[]}
16-
*/
17-
export let nodes: TreeViewNode[] = [];
1812
/** Provide classes to set the tree width. */
1913
export let width: CssClasses = 'w-full';
2014
/** Provide classes to set the vertical spacing between items. */
@@ -114,9 +108,5 @@
114108
aria-label={labelledby}
115109
aria-disabled={disabled}
116110
>
117-
{#if nodes && nodes.length > 0}
118-
<TreeViewDataDrivenItem bind:nodes on:change on:click on:toggle on:keydown on:keyup />
119-
{:else}
120-
<slot />
121-
{/if}
111+
<slot />
122112
</div>

0 commit comments

Comments
 (0)