tree-element 0.3.2 → 0.4.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.
Files changed (59) hide show
  1. package/README.md +1 -0
  2. package/lib/classNames.js +4 -4
  3. package/lib/classNames.js.map +1 -1
  4. package/lib/dataLoader.d.ts +0 -1
  5. package/lib/dataLoader.d.ts.map +1 -1
  6. package/lib/dataLoader.js +6 -6
  7. package/lib/dataLoader.js.map +1 -1
  8. package/lib/elementsRenderer.d.ts +4 -2
  9. package/lib/elementsRenderer.d.ts.map +1 -1
  10. package/lib/elementsRenderer.js +35 -21
  11. package/lib/elementsRenderer.js.map +1 -1
  12. package/lib/index.d.ts +10 -4
  13. package/lib/index.d.ts.map +1 -1
  14. package/lib/index.js +75 -18
  15. package/lib/index.js.map +1 -1
  16. package/lib/keyHandler.d.ts +14 -3
  17. package/lib/keyHandler.d.ts.map +1 -1
  18. package/lib/keyHandler.js +61 -32
  19. package/lib/keyHandler.js.map +1 -1
  20. package/lib/methodTypes.d.ts +3 -0
  21. package/lib/methodTypes.d.ts.map +1 -1
  22. package/lib/nodeElement/folderElement.d.ts +1 -1
  23. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  24. package/lib/nodeElement/folderElement.js +4 -4
  25. package/lib/nodeElement/folderElement.js.map +1 -1
  26. package/lib/nodeElement/index.d.ts +3 -2
  27. package/lib/nodeElement/index.d.ts.map +1 -1
  28. package/lib/nodeElement/index.js +15 -12
  29. package/lib/nodeElement/index.js.map +1 -1
  30. package/lib/options.d.ts +13 -0
  31. package/lib/options.d.ts.map +1 -1
  32. package/lib/selectNodeHandler.d.ts +3 -0
  33. package/lib/selectNodeHandler.d.ts.map +1 -1
  34. package/lib/selectNodeHandler.js +54 -22
  35. package/lib/selectNodeHandler.js.map +1 -1
  36. package/lib/setDefaultOptions.d.ts.map +1 -1
  37. package/lib/setDefaultOptions.js +1 -0
  38. package/lib/setDefaultOptions.js.map +1 -1
  39. package/lib/version.d.ts +1 -1
  40. package/lib/version.js +1 -1
  41. package/lib/version.js.map +1 -1
  42. package/package.json +19 -19
  43. package/src/classNames.ts +4 -4
  44. package/src/dataLoader.ts +6 -9
  45. package/src/elementsRenderer.ts +42 -24
  46. package/src/index.ts +83 -24
  47. package/src/keyHandler.ts +84 -34
  48. package/src/methodTypes.ts +6 -0
  49. package/src/nodeElement/folderElement.ts +4 -4
  50. package/src/nodeElement/index.ts +20 -17
  51. package/src/options.ts +13 -0
  52. package/src/selectNodeHandler.ts +58 -25
  53. package/src/setDefaultOptions.ts +1 -0
  54. package/src/version.ts +1 -1
  55. package/tree_element.css +16 -0
  56. package/tree_element.debug.js +256 -120
  57. package/tree_element.debug.js.map +1 -1
  58. package/tree_element.js +2 -2
  59. package/tree_element.js.map +1 -1
package/src/index.ts CHANGED
@@ -47,6 +47,8 @@ interface TreeElementParams extends Partial<TreeElementOptions> {
47
47
  }
48
48
 
49
49
  export default class TreeElement {
50
+ // With multiSelect: the node that is the tab stop, see updateTabStop
51
+ private activeNode: Node | null;
50
52
  private classNames: ClassNames;
51
53
  private dataLoader: DataLoader;
52
54
  private dndHandler: DragAndDropHandler;
@@ -82,6 +84,7 @@ export default class TreeElement {
82
84
  this.tree = new Node({}, true);
83
85
  this.nodeMap = new WeakMap();
84
86
  this.fallbackTabStop = null;
87
+ this.activeNode = null;
85
88
 
86
89
  const {
87
90
  ariaLabel,
@@ -92,6 +95,7 @@ export default class TreeElement {
92
95
  dataFilter,
93
96
  dragAndDrop,
94
97
  keyboardSupport,
98
+ multiSelect,
95
99
  onCanMove,
96
100
  onCanMoveTo,
97
101
  onCreateLi,
@@ -153,6 +157,19 @@ export default class TreeElement {
153
157
  const selectNode =
154
158
  selectNodeHandler.selectSingleNode.bind(selectNodeHandler);
155
159
 
160
+ const toggleNodeInSelection =
161
+ selectNodeHandler.toggleNodeInSelection.bind(selectNodeHandler);
162
+
163
+ // With multiSelect, a click adds a node to the selection or removes it
164
+ const onClickTitle = (node: Node) => {
165
+ if (this.options.multiSelect) {
166
+ toggleNodeInSelection(node, false);
167
+ this.focusNode(node);
168
+ } else {
169
+ selectNode(node);
170
+ }
171
+ };
172
+
156
173
  const getMouseDelay = () => this.options.startDndDelay ?? 0;
157
174
 
158
175
  const dataLoader = new DataLoader({
@@ -205,12 +222,16 @@ export default class TreeElement {
205
222
 
206
223
  const keyHandler = new KeyHandler({
207
224
  closeNode,
225
+ focusNode: this.focusNode.bind(this),
226
+ getActiveNode: this.getActiveNode.bind(this),
208
227
  getFocusedNode,
209
228
  getSelectedNode,
210
229
  getTree,
211
230
  keyboardSupport,
231
+ multiSelect,
212
232
  openNode,
213
233
  selectNode,
234
+ toggleNodeInSelection,
214
235
  triggerEvent,
215
236
  });
216
237
 
@@ -225,6 +246,7 @@ export default class TreeElement {
225
246
  getTree,
226
247
  htmlElement: treeElement,
227
248
  isNodeSelected,
249
+ multiSelect,
228
250
  onCreateLi,
229
251
  openedIcon,
230
252
  rtl,
@@ -245,7 +267,7 @@ export default class TreeElement {
245
267
  getMouseDelay,
246
268
  getNode,
247
269
  onClickButton: this.toggle.bind(this),
248
- onClickTitle: selectNode,
270
+ onClickTitle,
249
271
  onMouseCapture,
250
272
  onMouseDrag,
251
273
  onMouseStart,
@@ -263,6 +285,10 @@ export default class TreeElement {
263
285
  this.scrollHandler = scrollHandler;
264
286
  this.selectNodeHandler = selectNodeHandler;
265
287
 
288
+ if (multiSelect) {
289
+ treeElement.addEventListener("focusin", this.handleFocusIn);
290
+ }
291
+
266
292
  this.initData();
267
293
  }
268
294
 
@@ -388,6 +414,7 @@ export default class TreeElement {
388
414
  this.dataLoader.deinit();
389
415
  this.keyHandler.deinit();
390
416
  this.mouseHandler.deinit();
417
+ this.htmlElement.removeEventListener("focusin", this.handleFocusIn);
391
418
 
392
419
  this.tree = new Node({}, true);
393
420
  }
@@ -584,7 +611,8 @@ export default class TreeElement {
584
611
  }
585
612
 
586
613
  /**
587
- * Selects the next visible node, like the down arrow key.
614
+ * Selects the next visible node, like the down arrow key. With
615
+ * `multiSelect` it moves the focus instead.
588
616
  *
589
617
  * @group Selection
590
618
  */
@@ -609,7 +637,8 @@ export default class TreeElement {
609
637
  }
610
638
 
611
639
  /**
612
- * Selects the previous visible node, like the up arrow key.
640
+ * Selects the previous visible node, like the up arrow key. With
641
+ * `multiSelect` it moves the focus instead.
613
642
  *
614
643
  * @group Selection
615
644
  */
@@ -754,8 +783,8 @@ export default class TreeElement {
754
783
  */
755
784
  public selectNode(node: Node | null, options?: SelectNodeOptions): void {
756
785
  if (!node) {
757
- // Called with empty node -> deselect current node
758
- this.deselectCurrentNode();
786
+ // Called with empty node -> deselect all nodes
787
+ this.deselectAllNodes();
759
788
  this.saveStateHandler.saveState();
760
789
  return;
761
790
  }
@@ -913,6 +942,7 @@ export default class TreeElement {
913
942
  classNames,
914
943
  closedIconElement,
915
944
  getScrollLeft,
945
+ multiSelect: this.options.multiSelect,
916
946
  node,
917
947
  openedIconElement,
918
948
  renderChildren,
@@ -933,6 +963,7 @@ export default class TreeElement {
933
963
  return new NodeElement({
934
964
  classNames,
935
965
  getScrollLeft,
966
+ multiSelect: this.options.multiSelect,
936
967
  node,
937
968
  tabIndex,
938
969
  treeElement,
@@ -968,9 +999,8 @@ export default class TreeElement {
968
999
  return requestUrl;
969
1000
  }
970
1001
 
971
- private deselectCurrentNode(): void {
972
- const node = this.getSelectedNode();
973
- if (node) {
1002
+ private deselectAllNodes(): void {
1003
+ for (const node of this.getSelectedNodes()) {
974
1004
  this.removeFromSelection(node);
975
1005
  }
976
1006
  }
@@ -984,6 +1014,23 @@ export default class TreeElement {
984
1014
  }
985
1015
  }
986
1016
 
1017
+ // The treeitem of the first node, or null when the tree is empty
1018
+ // Move the focus to a node, without selecting it
1019
+ private focusNode(node: Node): void {
1020
+ this.activeNode = node;
1021
+ this.updateTabStop();
1022
+ node.element?.focus();
1023
+ }
1024
+
1025
+ // With multiSelect: the active node, if it is still rendered in the tree
1026
+ private getActiveNode(): Node | null {
1027
+ const element = this.activeNode?.element;
1028
+
1029
+ return element && this.htmlElement.contains(element)
1030
+ ? this.activeNode
1031
+ : null;
1032
+ }
1033
+
987
1034
  // Get the maximum level for auto open
988
1035
  private getAutoOpenMaxLevel(): number {
989
1036
  if (this.options.autoOpen === true) {
@@ -1000,15 +1047,8 @@ export default class TreeElement {
1000
1047
  return 0;
1001
1048
  }
1002
1049
 
1003
- // The title span of the first node, or null when the tree is empty
1004
- private getFirstTitleSpan(): HTMLElement | null {
1005
- const firstElement = this.tree.children[0]?.element;
1006
-
1007
- return firstElement
1008
- ? firstElement.querySelector<HTMLElement>(
1009
- `:scope > .${this.classNames.element} > span.${this.classNames.title}`,
1010
- )
1011
- : null;
1050
+ private getFirstTreeItem(): HTMLElement | null {
1051
+ return this.tree.children[0]?.element ?? null;
1012
1052
  }
1013
1053
 
1014
1054
  // The node of the HTML element that has the focus, if it is in this tree
@@ -1022,7 +1062,7 @@ export default class TreeElement {
1022
1062
 
1023
1063
  // The keyboard must still work for input elements.
1024
1064
  const tagName = activeElement.tagName;
1025
- if (tagName !== "A" && tagName !== "SPAN") {
1065
+ if (tagName !== "A" && tagName !== "LI" && tagName !== "SPAN") {
1026
1066
  return null;
1027
1067
  }
1028
1068
 
@@ -1045,6 +1085,18 @@ export default class TreeElement {
1045
1085
  return this.saveStateHandler.getNodeIdToBeSelected();
1046
1086
  }
1047
1087
 
1088
+ // With multiSelect, the focused node becomes the tab stop
1089
+ private handleFocusIn = (): void => {
1090
+ const node = this.getFocusedNode();
1091
+
1092
+ if (!node || node === this.activeNode) {
1093
+ return;
1094
+ }
1095
+
1096
+ this.activeNode = node;
1097
+ this.updateTabStop();
1098
+ };
1099
+
1048
1100
  private hasRenderedSelectedNode(): boolean {
1049
1101
  return this.getSelectedNodes().some((node) => Boolean(node.element));
1050
1102
  }
@@ -1147,7 +1199,6 @@ export default class TreeElement {
1147
1199
 
1148
1200
  private mouseStart(positionInfo: PositionInfo): boolean {
1149
1201
  /* istanbul ignore if */
1150
- // eslint-disable-next-line unicorn/prefer-ternary
1151
1202
  if (!this.options.dragAndDrop) {
1152
1203
  return false;
1153
1204
  }
@@ -1283,19 +1334,27 @@ export default class TreeElement {
1283
1334
  /* Keyboard users reach the tree through the tab stop. The selected node is
1284
1335
  * the tab stop, see NodeElement.select. Without a selected node, the first
1285
1336
  * node is the tab stop, so the tree can always be reached.
1337
+ * With multiSelect the tab stop is the active node: the node that was
1338
+ * focused last. Without one, it is the first node.
1286
1339
  */
1287
1340
  private updateTabStop(): void {
1288
1341
  const tabIndex = this.options.tabIndex;
1342
+ const multiSelect = this.options.multiSelect;
1289
1343
  const previousTabStop = this.fallbackTabStop;
1290
- const tabStop =
1291
- tabIndex == null || this.hasRenderedSelectedNode()
1292
- ? null
1293
- : this.getFirstTitleSpan();
1344
+ let tabStop: HTMLElement | null;
1345
+
1346
+ if (tabIndex == null) {
1347
+ tabStop = null;
1348
+ } else if (multiSelect) {
1349
+ tabStop = this.getActiveNode()?.element ?? this.getFirstTreeItem();
1350
+ } else {
1351
+ tabStop = this.hasRenderedSelectedNode() ? null : this.getFirstTreeItem();
1352
+ }
1294
1353
 
1295
1354
  if (
1296
1355
  previousTabStop &&
1297
1356
  previousTabStop !== tabStop &&
1298
- previousTabStop.getAttribute("aria-selected") !== "true"
1357
+ (multiSelect || previousTabStop.getAttribute("aria-selected") !== "true")
1299
1358
  ) {
1300
1359
  previousTabStop.setAttribute("tabindex", "-1");
1301
1360
  }
package/src/keyHandler.ts CHANGED
@@ -1,53 +1,77 @@
1
1
  import type {
2
2
  CloseNode,
3
+ FocusNode,
4
+ GetActiveNode,
3
5
  GetFocusedNode,
4
6
  GetSelectedNode,
5
7
  GetTree,
6
8
  OpenNode,
7
9
  SelectNode,
10
+ ToggleNodeInSelection,
8
11
  TriggerEvent,
9
12
  } from "./methodTypes";
10
13
  import type { Node } from "./node";
11
14
 
12
15
  interface KeyHandlerParams {
13
16
  closeNode: CloseNode;
17
+ focusNode: FocusNode;
18
+ getActiveNode: GetActiveNode;
14
19
  getFocusedNode: GetFocusedNode;
15
20
  getSelectedNode: GetSelectedNode;
16
21
  getTree: GetTree;
17
22
  keyboardSupport: boolean;
23
+ multiSelect: boolean;
18
24
  openNode: OpenNode;
19
25
  selectNode: SelectNode;
26
+ toggleNodeInSelection: ToggleNodeInSelection;
20
27
  triggerEvent: TriggerEvent;
21
28
  }
22
29
 
30
+ /* The keys move from the selected node and select the node they move to.
31
+ * With multiSelect they move from the focused node and only move the focus;
32
+ * Space adds the focused node to the selection or removes it.
33
+ */
34
+
23
35
  export default class KeyHandler {
24
36
  private closeNode: CloseNode;
37
+ private focusNode: FocusNode;
38
+ private getActiveNode: GetActiveNode;
25
39
  private getFocusedNode: GetFocusedNode;
26
40
  private getSelectedNode: GetSelectedNode;
27
41
  private getTree: GetTree;
28
42
 
29
43
  private keyboardSupport: boolean;
44
+ private multiSelect: boolean;
30
45
  private openNode: OpenNode;
31
46
  private originalSelectNode: SelectNode;
47
+ private toggleNodeInSelection: ToggleNodeInSelection;
32
48
  private triggerEvent: TriggerEvent;
33
49
 
34
50
  constructor({
35
51
  closeNode,
52
+ focusNode,
53
+ getActiveNode,
36
54
  getFocusedNode,
37
55
  getSelectedNode,
38
56
  getTree,
39
57
  keyboardSupport,
58
+ multiSelect,
40
59
  openNode,
41
60
  selectNode,
61
+ toggleNodeInSelection,
42
62
  triggerEvent,
43
63
  }: KeyHandlerParams) {
44
64
  this.closeNode = closeNode;
65
+ this.focusNode = focusNode;
66
+ this.getActiveNode = getActiveNode;
45
67
  this.getFocusedNode = getFocusedNode;
46
68
  this.getSelectedNode = getSelectedNode;
47
69
  this.getTree = getTree;
48
70
  this.keyboardSupport = keyboardSupport;
71
+ this.multiSelect = multiSelect;
49
72
  this.openNode = openNode;
50
73
  this.originalSelectNode = selectNode;
74
+ this.toggleNodeInSelection = toggleNodeInSelection;
51
75
  this.triggerEvent = triggerEvent;
52
76
 
53
77
  if (keyboardSupport) {
@@ -62,11 +86,11 @@ export default class KeyHandler {
62
86
  }
63
87
 
64
88
  public moveDown(): boolean {
65
- return this.selectNode(this.getSelectedNode()?.getNextVisibleNode());
89
+ return this.goToNode(this.getCurrentNode()?.getNextVisibleNode());
66
90
  }
67
91
 
68
92
  public moveUp(): boolean {
69
- return this.selectNode(this.getSelectedNode()?.getPreviousVisibleNode());
93
+ return this.goToNode(this.getCurrentNode()?.getPreviousVisibleNode());
70
94
  }
71
95
 
72
96
  // Activate the node, for example to open its edit page.
@@ -75,6 +99,28 @@ export default class KeyHandler {
75
99
  return true;
76
100
  }
77
101
 
102
+ // No node is selected. These keys select the focused node, which is
103
+ // usually the first node, see TreeElement.updateTabStop.
104
+ private getCurrentNode(): Node | null {
105
+ return this.multiSelect ? this.getActiveNode() : this.getSelectedNode();
106
+ }
107
+
108
+ // Move to the node: select it, or with multiSelect focus it.
109
+ // Result: there was a node to move to.
110
+ private goToNode(node?: Node | null): boolean {
111
+ if (!node) {
112
+ return false;
113
+ }
114
+
115
+ if (this.multiSelect) {
116
+ this.focusNode(node);
117
+ } else {
118
+ this.originalSelectNode(node);
119
+ }
120
+
121
+ return true;
122
+ }
123
+
78
124
  private handleArrowKey(key: string): boolean {
79
125
  switch (key) {
80
126
  case "ArrowDown": {
@@ -99,6 +145,12 @@ export default class KeyHandler {
99
145
  }
100
146
  }
101
147
 
148
+ private handleKey(key: string, focusedNode: Node): boolean {
149
+ return this.getCurrentNode()
150
+ ? this.handleArrowKey(key)
151
+ : this.handleKeyWithoutSelection(key, focusedNode);
152
+ }
153
+
102
154
  private handleKeyDown = (e: KeyboardEvent): void => {
103
155
  const focusedNode = this.getFocusedNode();
104
156
 
@@ -108,29 +160,37 @@ export default class KeyHandler {
108
160
 
109
161
  let isKeyHandled: boolean;
110
162
 
111
- const selectedNode = this.getSelectedNode();
112
-
113
163
  switch (e.key) {
164
+ case " ": {
165
+ isKeyHandled = this.multiSelect
166
+ ? this.toggle(focusedNode)
167
+ : this.handleKey(e.key, focusedNode);
168
+ break;
169
+ }
170
+
114
171
  case "End": {
115
- isKeyHandled = this.selectNode(this.getTree()?.getLastChild());
172
+ isKeyHandled = this.goToNode(this.getTree()?.getLastChild());
116
173
  break;
117
174
  }
118
175
 
119
176
  case "Enter": {
120
177
  // Without a selected node, activate the focused node
121
- isKeyHandled = this.activate(selectedNode ?? focusedNode, e);
178
+ isKeyHandled = this.activate(
179
+ this.multiSelect
180
+ ? focusedNode
181
+ : (this.getSelectedNode() ?? focusedNode),
182
+ e,
183
+ );
122
184
  break;
123
185
  }
124
186
 
125
187
  case "Home": {
126
- isKeyHandled = this.selectNode(this.getTree()?.children[0]);
188
+ isKeyHandled = this.goToNode(this.getTree()?.children[0]);
127
189
  break;
128
190
  }
129
191
 
130
192
  default: {
131
- isKeyHandled = selectedNode
132
- ? this.handleArrowKey(e.key)
133
- : this.handleKeyWithoutSelection(e.key, focusedNode);
193
+ isKeyHandled = this.handleKey(e.key, focusedNode);
134
194
  }
135
195
  }
136
196
 
@@ -139,14 +199,12 @@ export default class KeyHandler {
139
199
  }
140
200
  };
141
201
 
142
- // No node is selected. These keys select the focused node, which is
143
- // usually the first node, see TreeElement.updateTabStop.
144
202
  private handleKeyWithoutSelection(key: string, focusedNode: Node): boolean {
145
203
  switch (key) {
146
204
  case " ":
147
205
  case "ArrowDown":
148
206
  case "ArrowRight": {
149
- return this.selectNode(focusedNode);
207
+ return this.goToNode(focusedNode);
150
208
  }
151
209
 
152
210
  default: {
@@ -156,48 +214,40 @@ export default class KeyHandler {
156
214
  }
157
215
 
158
216
  private moveLeft(): boolean {
159
- const selectedNode = this.getSelectedNode();
217
+ const currentNode = this.getCurrentNode();
160
218
 
161
219
  /* istanbul ignore if */
162
- if (!selectedNode) {
220
+ if (!currentNode) {
163
221
  return false;
164
222
  }
165
223
 
166
- if (selectedNode.isFolder() && selectedNode.is_open) {
224
+ if (currentNode.isFolder() && currentNode.is_open) {
167
225
  // Left on an open node closes the node
168
- void this.closeNode(selectedNode);
226
+ void this.closeNode(currentNode);
169
227
  return true;
170
228
  }
171
229
  // Left on a closed or end node moves focus to the node's parent
172
- return this.selectNode(selectedNode.getParent());
230
+ return this.goToNode(currentNode.getParent());
173
231
  }
174
232
 
175
233
  private moveRight(): boolean {
176
- const selectedNode = this.getSelectedNode();
234
+ const currentNode = this.getCurrentNode();
177
235
 
178
- if (selectedNode?.isFolder()) {
236
+ if (currentNode?.isFolder()) {
179
237
  // folder node
180
- if (selectedNode.is_open) {
238
+ if (currentNode.is_open) {
181
239
  // Right moves to the first child of an open node
182
- return this.selectNode(selectedNode.getNextVisibleNode());
240
+ return this.goToNode(currentNode.getNextVisibleNode());
183
241
  }
184
242
  // Right expands a closed node
185
- void this.openNode(selectedNode);
243
+ void this.openNode(currentNode);
186
244
  return true;
187
245
  }
188
246
  return false;
189
247
  }
190
248
 
191
- /* Select the node.
192
- * Don't do anything if the node is null.
193
- * Result: a different node was selected.
194
- */
195
- private selectNode(node?: Node | null): boolean {
196
- if (node) {
197
- this.originalSelectNode(node);
198
-
199
- return true;
200
- }
201
- return false;
249
+ private toggle(node: Node): boolean {
250
+ this.toggleNodeInSelection(node);
251
+ return true;
202
252
  }
203
253
  }
@@ -6,6 +6,10 @@ export type AddToSelection = (node: Node) => void;
6
6
 
7
7
  export type CloseNode = (node: Node) => Promise<void>;
8
8
 
9
+ export type FocusNode = (node: Node) => void;
10
+
11
+ export type GetActiveNode = () => Node | null;
12
+
9
13
  export type GetFocusedNode = () => Node | null;
10
14
 
11
15
  export type GetNodeById = (nodeId: NodeId) => Node | null;
@@ -34,6 +38,8 @@ export type RemoveFromSelection = (node: Node) => void;
34
38
 
35
39
  export type SelectNode = (node: Node) => void;
36
40
 
41
+ export type ToggleNodeInSelection = (node: Node) => void;
42
+
37
43
  // Trigger an event. Return if the event is processed (true) or cancelled (false).
38
44
  export type TriggerEvent = <Name extends TreeEventName>(
39
45
  eventName: Name,
@@ -25,6 +25,7 @@ class FolderElement extends NodeElement {
25
25
  classNames,
26
26
  closedIconElement,
27
27
  getScrollLeft,
28
+ multiSelect,
28
29
  node,
29
30
  openedIconElement,
30
31
  renderChildren,
@@ -35,6 +36,7 @@ class FolderElement extends NodeElement {
35
36
  super({
36
37
  classNames,
37
38
  getScrollLeft,
39
+ multiSelect,
38
40
  node,
39
41
  tabIndex,
40
42
  treeElement,
@@ -84,8 +86,7 @@ class FolderElement extends NodeElement {
84
86
 
85
87
  this.element.classList.add(this.classNames.closed);
86
88
 
87
- const titleSpan = this.getTitleSpan();
88
- titleSpan.setAttribute("aria-expanded", "false");
89
+ this.element.setAttribute("aria-expanded", "false");
89
90
 
90
91
  this.triggerEvent("tree.close", {
91
92
  node: this.node,
@@ -132,8 +133,7 @@ class FolderElement extends NodeElement {
132
133
 
133
134
  this.element.classList.remove(this.classNames.closed);
134
135
 
135
- const titleSpan = this.getTitleSpan();
136
- titleSpan.setAttribute("aria-expanded", "true");
136
+ this.element.setAttribute("aria-expanded", "true");
137
137
 
138
138
  this.triggerEvent("tree.open", {
139
139
  node: this.node,
@@ -9,6 +9,10 @@ import GhostDropHint from "./ghostDropHint";
9
9
  export interface NodeElementParams {
10
10
  classNames: ClassNames;
11
11
  getScrollLeft: GetScrollLeft;
12
+ /* With multiSelect the focus moves independently of the selection, so
13
+ * selecting doesn't change the tabindex or the focus.
14
+ */
15
+ multiSelect?: boolean;
12
16
  node: Node;
13
17
  tabIndex?: number;
14
18
  treeElement: HTMLElement;
@@ -19,18 +23,21 @@ class NodeElement {
19
23
  public node: Node;
20
24
  protected classNames: ClassNames;
21
25
  private getScrollLeft: GetScrollLeft;
26
+ private multiSelect: boolean;
22
27
  private tabIndex?: number;
23
28
  private treeElement: HTMLElement;
24
29
 
25
30
  constructor({
26
31
  classNames,
27
32
  getScrollLeft,
33
+ multiSelect = false,
28
34
  node,
29
35
  tabIndex,
30
36
  treeElement,
31
37
  }: NodeElementParams) {
32
38
  this.classNames = classNames;
33
39
  this.getScrollLeft = getScrollLeft;
40
+ this.multiSelect = multiSelect;
34
41
  this.node = node;
35
42
  this.tabIndex = tabIndex;
36
43
  this.treeElement = treeElement;
@@ -52,16 +59,20 @@ class NodeElement {
52
59
 
53
60
  this.element.classList.remove(this.classNames.selected);
54
61
 
55
- const titleSpan = this.getTitleSpan();
62
+ // Only a multiselect tree marks the nodes that are not selected
63
+ if (this.multiSelect) {
64
+ this.element.setAttribute("aria-selected", "false");
65
+ return;
66
+ }
67
+
68
+ this.element.removeAttribute("aria-selected");
56
69
 
57
70
  // Check for null or undefined
58
71
  if (this.tabIndex != null) {
59
- titleSpan.setAttribute("tabindex", "-1");
72
+ this.element.setAttribute("tabindex", "-1");
60
73
  }
61
74
 
62
- titleSpan.setAttribute("aria-selected", "false");
63
-
64
- titleSpan.blur();
75
+ this.element.blur();
65
76
  }
66
77
 
67
78
  public select(mustSetFocus: boolean): void {
@@ -70,28 +81,20 @@ class NodeElement {
70
81
  }
71
82
 
72
83
  this.element.classList.add(this.classNames.selected);
84
+ this.element.setAttribute("aria-selected", "true");
73
85
 
74
- const titleSpan = this.getTitleSpan();
75
86
  const tabIndex = this.tabIndex;
76
87
 
77
88
  // Check for null or undefined
78
- if (tabIndex != null) {
79
- titleSpan.setAttribute("tabindex", tabIndex.toString());
89
+ if (tabIndex != null && !this.multiSelect) {
90
+ this.element.setAttribute("tabindex", tabIndex.toString());
80
91
  }
81
92
 
82
- titleSpan.setAttribute("aria-selected", "true");
83
-
84
93
  if (mustSetFocus) {
85
- titleSpan.focus();
94
+ this.element.focus();
86
95
  }
87
96
  }
88
97
 
89
- protected getTitleSpan(): HTMLSpanElement {
90
- return this.element.querySelector(
91
- `:scope > .${this.classNames.element} > span.${this.classNames.title}`,
92
- ) as HTMLSpanElement;
93
- }
94
-
95
98
  protected getUl(): HTMLUListElement | null {
96
99
  return this.element.querySelector(":scope > ul");
97
100
  }
package/src/options.ts CHANGED
@@ -151,6 +151,19 @@ export interface TreeElementOptions {
151
151
  * @group Selection
152
152
  */
153
153
  keyboardSupport: boolean;
154
+ /**
155
+ * Let the user select multiple nodes: a click adds a node to the
156
+ * selection, or removes it when it is selected. The arrow keys move the
157
+ * focus instead of the selection, and `Space` adds the focused node or
158
+ * removes it. See [Selection](/guide/selection#multiple-nodes).
159
+ *
160
+ * Set it in the constructor; changing it with `setOption` is not
161
+ * supported.
162
+ *
163
+ * @defaultValue `false`
164
+ * @group Selection
165
+ */
166
+ multiSelect: boolean;
154
167
  /**
155
168
  * The class used to create nodes. Subclass [`Node`](/reference/node) to
156
169
  * add your own methods. The `Node` class is not exposed on the global