@streamscloud/kit 0.18.0 → 0.19.0

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.
@@ -10,7 +10,7 @@ import IconCheckmarkCircle from '@fluentui/svg-icons/icons/checkmark_circle_20_f
10
10
  import IconChevronRight from '@fluentui/svg-icons/icons/chevron_right_20_regular.svg?raw';
11
11
  import IconMoreVertical from '@fluentui/svg-icons/icons/more_vertical_20_regular.svg?raw';
12
12
  import { slide } from 'svelte/transition';
13
- let { node, nav, hideExpandIcon = false, level = 0, showSelectedCheckmark } = $props();
13
+ let { node, keyboard, hideExpandIcon = false, level = 0, showSelectedCheckmark } = $props();
14
14
  let actionsOpened = $state(false);
15
15
  let dbClickTimeout = undefined;
16
16
  let dragExpandTimeout = undefined;
@@ -65,13 +65,8 @@ const handleDrop = (e) => {
65
65
  DraggingNodeState.draggingNode = null;
66
66
  DraggingNodeState.dragOverNode = null;
67
67
  };
68
- const handlePointerDown = () => {
69
- nav.setActive(node);
70
- nav.keyboardActive = false;
71
- };
72
68
  const handleSelectNode = (e) => {
73
69
  e.stopPropagation();
74
- nav.setActive(node);
75
70
  clearTimeout(dbClickTimeout);
76
71
  if (node.expandOnDoubleClick) {
77
72
  dbClickTimeout = window.setTimeout(() => {
@@ -84,7 +79,6 @@ const handleSelectNode = (e) => {
84
79
  };
85
80
  const handleExpandNode = (e) => {
86
81
  e.stopPropagation();
87
- nav.setActive(node);
88
82
  if (node.expanded) {
89
83
  node.collapse();
90
84
  }
@@ -116,9 +110,7 @@ const handleMouseLeave = () => {
116
110
  <div
117
111
  class="tree-node"
118
112
  style={`--_tree-node--level: ${level}`}
119
- id={nav.itemId(node)}
120
113
  role="treeitem"
121
- tabindex="-1"
122
114
  aria-level={level + 1}
123
115
  aria-selected={node.selected}
124
116
  aria-expanded={children.length ? node.expanded : undefined}>
@@ -126,7 +118,6 @@ const handleMouseLeave = () => {
126
118
  <div
127
119
  class="tree-node__leaf"
128
120
  class:tree-node__leaf--active={node.selected}
129
- class:tree-node__leaf--focused={nav.hasFocus && nav.keyboardActive && nav.activeId === node.id}
130
121
  class:tree-node__leaf--draggable={node.dragConfig}
131
122
  class:tree-node__leaf--expandable={children.length}
132
123
  class:tree-node__leaf--drag-over={isDraggedOver}
@@ -147,7 +138,6 @@ const handleMouseLeave = () => {
147
138
  class:tree-node__expand-button--active={node.selected || node.involvedIntoActivePath}
148
139
  tabindex="-1"
149
140
  aria-hidden="true"
150
- onpointerdown={handlePointerDown}
151
141
  onclick={handleExpandNode}>
152
142
  <span class="tree-node__expand-icon" class:tree-node__expand-icon--expanded={node.expanded}>
153
143
  <Icon src={IconChevronRight} />
@@ -155,8 +145,15 @@ const handleMouseLeave = () => {
155
145
  </button>
156
146
  {/if}
157
147
 
158
- <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
159
- <div class="tree-node__anchor" onpointerdown={handlePointerDown} onclick={handleSelectNode} ondblclick={handleExpandOnDoubleClick}>
148
+ <div
149
+ class="tree-node__anchor"
150
+ id={keyboard.anchorId(node)}
151
+ role="button"
152
+ tabindex={keyboard.currentId === node.id ? 0 : -1}
153
+ onclick={handleSelectNode}
154
+ ondblclick={handleExpandOnDoubleClick}
155
+ onfocusin={() => keyboard.setFocused(node.id)}
156
+ onkeydown={(e) => keyboard.handleKeydown(node, e)}>
160
157
  {#if !node.hideIcon}
161
158
  <div class="tree-node__icon" class:tree-node__icon--active={node.selected}>
162
159
  {#if node.icon}
@@ -201,7 +198,7 @@ const handleMouseLeave = () => {
201
198
  {#if node.expanded && children.length}
202
199
  <div transition:slide={{ duration: transitionDuration.base }} class="tree-node__children" role="group">
203
200
  {#each children as child (child.id)}
204
- <Self node={child} nav={nav} level={level + 1} hideExpandIcon={hideChildExpandIcon} showSelectedCheckmark={showSelectedCheckmark} />
201
+ <Self node={child} keyboard={keyboard} level={level + 1} hideExpandIcon={hideChildExpandIcon} showSelectedCheckmark={showSelectedCheckmark} />
205
202
  {/each}
206
203
  </div>
207
204
  {/if}
@@ -240,7 +237,10 @@ const handleMouseLeave = () => {
240
237
  color: var(--sc-kit--color--text--primary);
241
238
  background: var(--sc-kit--color--bg--active);
242
239
  }
243
- .tree-node__leaf--focused {
240
+ .tree-node__leaf:focus-within {
241
+ --_tree-node--action-opacity: 1;
242
+ }
243
+ .tree-node__leaf:has(.tree-node__anchor:focus-visible) {
244
244
  outline: 2px solid var(--sc-kit--color--border--focus);
245
245
  outline-offset: -2px;
246
246
  }
@@ -285,6 +285,7 @@ const handleMouseLeave = () => {
285
285
  gap: var(--sc-kit--space--2);
286
286
  min-width: 0;
287
287
  padding: var(--_tree-node--padding-block) 0;
288
+ outline: none;
288
289
  }
289
290
  .tree-node__icon {
290
291
  --sc-kit--icon--size: var(--_tree-node--icon-size);
@@ -1,8 +1,8 @@
1
- import type { TreeNavigation } from './tree-navigation.svelte';
1
+ import type { TreeKeyboard } from './tree-keyboard.svelte';
2
2
  import { type TreeNode } from './tree-node.svelte';
3
3
  type Props = {
4
4
  node: TreeNode;
5
- nav: TreeNavigation;
5
+ keyboard: TreeKeyboard;
6
6
  hideExpandIcon?: boolean;
7
7
  level?: number;
8
8
  showSelectedCheckmark?: boolean;
@@ -1,42 +1,21 @@
1
1
  <script lang="ts">import NodeView from './cmp.tree-node-view.svelte';
2
- import { TreeNavigation } from './tree-navigation.svelte';
3
- let { nodes, showSelectedCheckmark, children } = $props();
2
+ import { createTreeKeyboard } from './tree-keyboard.svelte';
3
+ let { nodes, showSelectedCheckmark, header } = $props();
4
4
  const nodesComputed = $derived(nodes ? (Array.isArray(nodes) ? nodes : [nodes]) : []);
5
5
  const hideExpandIcon = $derived(nodesComputed.every((f) => !f.children.length));
6
- const nav = new TreeNavigation(() => nodesComputed);
7
- const handleFocusIn = (e) => {
8
- nav.focus();
9
- nav.keyboardActive = e.currentTarget.matches(':focus-visible');
10
- };
11
- const handleKeydown = (e) => {
12
- // Only the container itself drives tree navigation; a focused descendant (e.g. the actions popover) keeps its own keys.
13
- if (e.target !== e.currentTarget) {
14
- return;
6
+ $effect(() => {
7
+ for (const root of nodesComputed) {
8
+ root.setForest(() => nodesComputed);
15
9
  }
16
- nav.handleKeydown(e);
17
- // ArrowLeft collapses a subtree; if focus sat on a treeitem inside it, the unmount would drop focus
18
- // to <body> and arrow keys would die. Re-grab the container before the unmount so the single
19
- // aria-activedescendant focus owner stays put.
20
- e.currentTarget.focus();
21
- };
10
+ });
11
+ const keyboard = createTreeKeyboard(() => nodesComputed);
22
12
  </script>
23
13
 
24
14
  <div class="tree">
25
- {@render children?.()}
26
- <div
27
- class="tree__nodes"
28
- role="tree"
29
- tabindex="0"
30
- aria-activedescendant={nav.activeId ? `tree-item-${nav.activeId}` : undefined}
31
- onfocusin={handleFocusIn}
32
- onfocusout={(e) => {
33
- if (!(e.relatedTarget instanceof Node) || !e.currentTarget.contains(e.relatedTarget)) {
34
- nav.blur();
35
- }
36
- }}
37
- onkeydown={handleKeydown}>
15
+ {@render header?.()}
16
+ <div class="tree__nodes" role="tree">
38
17
  {#each nodesComputed as node (node.id)}
39
- <NodeView node={node} nav={nav} hideExpandIcon={hideExpandIcon} showSelectedCheckmark={showSelectedCheckmark} />
18
+ <NodeView node={node} keyboard={keyboard} hideExpandIcon={hideExpandIcon} showSelectedCheckmark={showSelectedCheckmark} />
40
19
  {/each}
41
20
  </div>
42
21
  </div>
@@ -48,9 +27,9 @@ holds its own expanded / selected state, children, optional icon / count / actio
48
27
  drag config (reorder or move-to-folder). Build the model with `new TreeNode({ … })` and pass the
49
28
  root(s) as `nodes`.
50
29
 
51
- Exposes an ARIA `tree` / `treeitem` / `group` structure with `aria-activedescendant` focus. Keyboard:
52
- ↑/↓ move between visible items, →/← expand-or-enter / collapse-or-leave, Home/End jump to first/last,
53
- Enter/Space select.
30
+ Click selects a node selection is the single source of truth. Keyboard: the current row is the tab
31
+ stop (native focus + `:focus-visible`), ↑/↓ move between visible rows, →/← expand-or-enter / collapse-or-leave,
32
+ Home/End jump to first/last, Enter/Space select.
54
33
 
55
34
  ### CSS Custom Properties
56
35
  | Property | Description | Default |
@@ -67,7 +46,4 @@ Enter/Space select.
67
46
  }
68
47
  .tree :global([contenteditable]) {
69
48
  user-select: text;
70
- }
71
- .tree__nodes:focus-visible {
72
- outline: none;
73
49
  }</style>
@@ -6,7 +6,7 @@ type Props = {
6
6
  /** Show a success checkmark on the selected node when it has no actions. */
7
7
  showSelectedCheckmark?: boolean;
8
8
  /** Optional content rendered above the nodes (e.g. a header). */
9
- children?: Snippet;
9
+ header?: Snippet;
10
10
  };
11
11
  /**
12
12
  * Tree — hierarchical, collapsible node list driven by `TreeNode` model instances. Each `TreeNode`
@@ -14,9 +14,9 @@ type Props = {
14
14
  * drag config (reorder or move-to-folder). Build the model with `new TreeNode({ … })` and pass the
15
15
  * root(s) as `nodes`.
16
16
  *
17
- * Exposes an ARIA `tree` / `treeitem` / `group` structure with `aria-activedescendant` focus. Keyboard:
18
- * ↑/↓ move between visible items, →/← expand-or-enter / collapse-or-leave, Home/End jump to first/last,
19
- * Enter/Space select.
17
+ * Click selects a node selection is the single source of truth. Keyboard: the current row is the tab
18
+ * stop (native focus + `:focus-visible`), ↑/↓ move between visible rows, →/← expand-or-enter / collapse-or-leave,
19
+ * Home/End jump to first/last, Enter/Space select.
20
20
  *
21
21
  * ### CSS Custom Properties
22
22
  * | Property | Description | Default |
@@ -0,0 +1,9 @@
1
+ import type { TreeNode } from './tree-node.svelte';
2
+ /** Native-focus roving keyboard navigation for the tree (single tab stop + arrows / Home / End / Enter). */
3
+ export declare const createTreeKeyboard: (getRoots: () => readonly TreeNode[]) => {
4
+ anchorId: (node: TreeNode) => string;
5
+ readonly currentId: string | null;
6
+ setFocused: (id: string) => void;
7
+ handleKeydown: (node: TreeNode, e: KeyboardEvent) => void;
8
+ };
9
+ export type TreeKeyboard = ReturnType<typeof createTreeKeyboard>;
@@ -0,0 +1,107 @@
1
+ import { randomNanoid } from '../../core/utils';
2
+ import { tick } from 'svelte';
3
+ /** Native-focus roving keyboard navigation for the tree (single tab stop + arrows / Home / End / Enter). */
4
+ export const createTreeKeyboard = (getRoots) => {
5
+ const treeId = randomNanoid(6);
6
+ const anchorDomId = (node) => `tree-${treeId}-anchor-${node.id}`;
7
+ let focusedId = $state(null);
8
+ const visible = () => {
9
+ const out = [];
10
+ const walk = (list) => {
11
+ for (const node of list) {
12
+ out.push(node);
13
+ if (node.expanded && node.children.length) {
14
+ walk(node.children);
15
+ }
16
+ }
17
+ };
18
+ walk(getRoots());
19
+ return out;
20
+ };
21
+ const currentId = $derived.by(() => {
22
+ const rows = visible();
23
+ if (focusedId && rows.some((n) => n.id === focusedId)) {
24
+ return focusedId;
25
+ }
26
+ return (rows.find((n) => n.selected) ?? rows[0])?.id ?? null;
27
+ });
28
+ const focusNode = (node) => {
29
+ focusedId = node.id;
30
+ document.getElementById(anchorDomId(node))?.focus();
31
+ };
32
+ const focusAfterTick = async (node) => {
33
+ await tick();
34
+ focusNode(node);
35
+ };
36
+ const step = (node, delta) => {
37
+ const rows = visible();
38
+ const index = rows.findIndex((n) => n.id === node.id);
39
+ if (index < 0) {
40
+ return;
41
+ }
42
+ const next = rows[Math.min(Math.max(index + delta, 0), rows.length - 1)];
43
+ if (next.id !== node.id) {
44
+ focusNode(next);
45
+ }
46
+ };
47
+ return {
48
+ anchorId: anchorDomId,
49
+ get currentId() {
50
+ return currentId;
51
+ },
52
+ setFocused: (id) => {
53
+ focusedId = id;
54
+ },
55
+ handleKeydown: (node, e) => {
56
+ switch (e.key) {
57
+ case 'ArrowDown':
58
+ e.preventDefault();
59
+ step(node, 1);
60
+ break;
61
+ case 'ArrowUp':
62
+ e.preventDefault();
63
+ step(node, -1);
64
+ break;
65
+ case 'ArrowRight':
66
+ e.preventDefault();
67
+ if (node.children.length && !node.expanded) {
68
+ node.expand();
69
+ }
70
+ else if (node.children.length) {
71
+ void focusAfterTick(node.children[0]);
72
+ }
73
+ break;
74
+ case 'ArrowLeft':
75
+ e.preventDefault();
76
+ if (node.expanded && node.children.length) {
77
+ node.collapse();
78
+ }
79
+ else if (node.parent) {
80
+ focusNode(node.parent);
81
+ }
82
+ break;
83
+ case 'Home': {
84
+ e.preventDefault();
85
+ const rows = visible();
86
+ if (rows.length) {
87
+ focusNode(rows[0]);
88
+ }
89
+ break;
90
+ }
91
+ case 'End': {
92
+ e.preventDefault();
93
+ const rows = visible();
94
+ if (rows.length) {
95
+ focusNode(rows[rows.length - 1]);
96
+ }
97
+ break;
98
+ }
99
+ case 'Enter':
100
+ case ' ':
101
+ e.preventDefault();
102
+ node.select({ explicitly: true });
103
+ break;
104
+ }
105
+ }
106
+ };
107
+ };
@@ -45,6 +45,7 @@ export declare class TreeNode {
45
45
  private _nonSelectable;
46
46
  private _children;
47
47
  private _parent?;
48
+ private _forestRoots?;
48
49
  private _dirty;
49
50
  constructor(init: TreeNodeInitializer);
50
51
  /** Whether this node has unsaved changes; set via `setDirty` and propagated up to ancestors. */
@@ -62,6 +63,8 @@ export declare class TreeNode {
62
63
  select(options?: {
63
64
  explicitly?: boolean;
64
65
  }): void;
66
+ /** Register the full set of forest roots so single-select clears across sibling roots, not just this node's own root. Set by the `Tree` component. */
67
+ setForest(getRoots: () => readonly TreeNode[]): void;
65
68
  setChildren(value: TreeNode[]): void;
66
69
  addChild(child: TreeNode): void;
67
70
  insertChildAt(index: number, child: TreeNode): void;
@@ -74,8 +77,9 @@ export declare class TreeNode {
74
77
  flatten(): TreeNode[];
75
78
  canMoveToParent(targetParent: TreeNode): boolean;
76
79
  moveToFolder(targetFolder: TreeNode): boolean;
77
- private refreshParentRefs;
80
+ private _refreshParentRefs;
78
81
  private _select;
79
- private unselectNodeAndChildren;
80
- private buildChainToCurrentNode;
82
+ private _firstSelectable;
83
+ private _unselectNodeAndChildren;
84
+ private _buildChainToCurrentNode;
81
85
  }
@@ -20,6 +20,7 @@ export class TreeNode {
20
20
  _nonSelectable;
21
21
  _children = $state([]);
22
22
  _parent;
23
+ _forestRoots;
23
24
  _dirty = $state(false);
24
25
  constructor(init) {
25
26
  this.id = init.id || this.id;
@@ -76,9 +77,13 @@ export class TreeNode {
76
77
  select(options) {
77
78
  this._select(options?.explicitly ?? false);
78
79
  }
80
+ /** Register the full set of forest roots so single-select clears across sibling roots, not just this node's own root. Set by the `Tree` component. */
81
+ setForest(getRoots) {
82
+ this._forestRoots = getRoots;
83
+ }
79
84
  setChildren(value) {
80
85
  this._children = value;
81
- this.refreshParentRefs(value);
86
+ this._refreshParentRefs(value);
82
87
  }
83
88
  addChild(child) {
84
89
  child._parent = this;
@@ -105,7 +110,7 @@ export class TreeNode {
105
110
  }
106
111
  }
107
112
  notifyHover(mouseOver) {
108
- const selectionPath = this.buildChainToCurrentNode(this);
113
+ const selectionPath = this._buildChainToCurrentNode(this);
109
114
  for (const item of selectionPath) {
110
115
  if (item._onNodeHover) {
111
116
  if (mouseOver) {
@@ -128,7 +133,7 @@ export class TreeNode {
128
133
  this._expanded = true;
129
134
  }
130
135
  unselect() {
131
- this.unselectNodeAndChildren(this);
136
+ this._unselectNodeAndChildren(this);
132
137
  }
133
138
  flatten() {
134
139
  const result = [];
@@ -168,26 +173,25 @@ export class TreeNode {
168
173
  this._dragConfig.onNodeMovedToFolder({ movedNode: this, targetFolder });
169
174
  return true;
170
175
  }
171
- refreshParentRefs(children) {
176
+ _refreshParentRefs(children) {
172
177
  for (const child of children) {
173
178
  child._parent = this;
174
179
  if (child._children.length) {
175
- child.refreshParentRefs(child._children);
180
+ child._refreshParentRefs(child._children);
176
181
  }
177
182
  }
178
183
  }
179
184
  _select(selectedExplicitly) {
180
185
  if (this._nonSelectable) {
181
- if (!this._children.length) {
182
- return;
183
- }
184
- return this._children[0]._select(selectedExplicitly);
186
+ this._firstSelectable()?._select(selectedExplicitly);
187
+ return;
185
188
  }
186
- // generate selection path (all nodes involved into current path)
187
- const selectionPath = this.buildChainToCurrentNode(this);
188
- // unselect previously selected values
189
+ const selectionPath = this._buildChainToCurrentNode(this);
189
190
  const root = selectionPath[selectionPath.length - 1];
190
- this.unselectNodeAndChildren(root);
191
+ const roots = root._forestRoots?.() ?? [root];
192
+ for (const r of roots) {
193
+ this._unselectNodeAndChildren(r);
194
+ }
191
195
  // process selection path
192
196
  this._selected = true;
193
197
  if (selectionPath.length > 1) {
@@ -209,7 +213,19 @@ export class TreeNode {
209
213
  }
210
214
  }
211
215
  }
212
- unselectNodeAndChildren(node) {
216
+ _firstSelectable() {
217
+ if (!this._nonSelectable) {
218
+ return this;
219
+ }
220
+ for (const child of this._children) {
221
+ const found = child._firstSelectable();
222
+ if (found) {
223
+ return found;
224
+ }
225
+ }
226
+ return null;
227
+ }
228
+ _unselectNodeAndChildren(node) {
213
229
  node._involvedIntoActivePath = false;
214
230
  node._selected = false;
215
231
  const children = node._children;
@@ -217,10 +233,10 @@ export class TreeNode {
217
233
  return;
218
234
  }
219
235
  children.forEach((f) => {
220
- this.unselectNodeAndChildren(f);
236
+ this._unselectNodeAndChildren(f);
221
237
  });
222
238
  }
223
- buildChainToCurrentNode(node) {
239
+ _buildChainToCurrentNode(node) {
224
240
  const chain = [];
225
241
  let current = node;
226
242
  while (current) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,16 +0,0 @@
1
- import type { TreeNode } from './tree-node.svelte';
2
- export declare class TreeNavigation {
3
- activeId: string | null;
4
- hasFocus: boolean;
5
- /** True only while the active node was reached via keyboard — gates the focus ring (mouse selection shows background only). */
6
- keyboardActive: boolean;
7
- private _getRoots;
8
- constructor(getRoots: () => readonly TreeNode[]);
9
- itemId(node: TreeNode): string;
10
- focus(): void;
11
- blur(): void;
12
- setActive(node: TreeNode): void;
13
- ensureActive(): void;
14
- handleKeydown(e: KeyboardEvent): void;
15
- private _visibleNodes;
16
- }
@@ -1,102 +0,0 @@
1
- export class TreeNavigation {
2
- activeId = $state(null);
3
- hasFocus = $state(false);
4
- /** True only while the active node was reached via keyboard — gates the focus ring (mouse selection shows background only). */
5
- keyboardActive = $state(false);
6
- _getRoots;
7
- constructor(getRoots) {
8
- this._getRoots = getRoots;
9
- }
10
- itemId(node) {
11
- return `tree-item-${node.id}`;
12
- }
13
- focus() {
14
- this.hasFocus = true;
15
- this.ensureActive();
16
- }
17
- blur() {
18
- this.hasFocus = false;
19
- }
20
- setActive(node) {
21
- this.activeId = node.id;
22
- }
23
- ensureActive() {
24
- const visible = this._visibleNodes();
25
- if (this.activeId !== null && visible.some((n) => n.id === this.activeId)) {
26
- return;
27
- }
28
- const selected = visible.find((n) => n.selected);
29
- this.activeId = (selected ?? visible[0])?.id ?? null;
30
- }
31
- handleKeydown(e) {
32
- const visible = this._visibleNodes();
33
- if (!visible.length) {
34
- return;
35
- }
36
- this.keyboardActive = true;
37
- const index = visible.findIndex((n) => n.id === this.activeId);
38
- const current = index < 0 ? null : visible[index];
39
- switch (e.key) {
40
- case 'ArrowDown':
41
- e.preventDefault();
42
- this.activeId = visible[index < 0 ? 0 : Math.min(index + 1, visible.length - 1)].id;
43
- break;
44
- case 'ArrowUp':
45
- e.preventDefault();
46
- this.activeId = visible[index <= 0 ? 0 : index - 1].id;
47
- break;
48
- case 'ArrowRight':
49
- e.preventDefault();
50
- if (!current) {
51
- this.activeId = visible[0].id;
52
- }
53
- else if (current.children.length) {
54
- if (current.expanded) {
55
- this.activeId = current.children[0].id;
56
- }
57
- else {
58
- current.expand();
59
- }
60
- }
61
- break;
62
- case 'ArrowLeft':
63
- e.preventDefault();
64
- if (!current) {
65
- this.activeId = visible[0].id;
66
- }
67
- else if (current.expanded && current.children.length) {
68
- current.collapse();
69
- }
70
- else if (current.parent && visible.includes(current.parent)) {
71
- this.activeId = current.parent.id;
72
- }
73
- break;
74
- case 'Home':
75
- e.preventDefault();
76
- this.activeId = visible[0].id;
77
- break;
78
- case 'End':
79
- e.preventDefault();
80
- this.activeId = visible[visible.length - 1].id;
81
- break;
82
- case 'Enter':
83
- case ' ':
84
- e.preventDefault();
85
- current?.select({ explicitly: true });
86
- break;
87
- }
88
- }
89
- _visibleNodes() {
90
- const result = [];
91
- const walk = (nodes) => {
92
- for (const node of nodes) {
93
- result.push(node);
94
- if (node.expanded && node.children.length) {
95
- walk(node.children);
96
- }
97
- }
98
- };
99
- walk(this._getRoots());
100
- return result;
101
- }
102
- }