tree-element 0.2.0 → 0.3.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 (121) hide show
  1. package/README.md +12 -4
  2. package/lib/animation.d.ts +2 -2
  3. package/lib/animation.d.ts.map +1 -1
  4. package/lib/animation.js +13 -9
  5. package/lib/animation.js.map +1 -1
  6. package/lib/dataLoader.d.ts +1 -0
  7. package/lib/dataLoader.d.ts.map +1 -1
  8. package/lib/dataLoader.js +8 -0
  9. package/lib/dataLoader.js.map +1 -1
  10. package/lib/dragAndDropHandler/dragElement.d.ts.map +1 -1
  11. package/lib/dragAndDropHandler/dragElement.js +2 -1
  12. package/lib/dragAndDropHandler/dragElement.js.map +1 -1
  13. package/lib/dragAndDropHandler/index.d.ts.map +1 -1
  14. package/lib/dragAndDropHandler/index.js +56 -65
  15. package/lib/dragAndDropHandler/index.js.map +1 -1
  16. package/lib/dragAndDropHandler/iterateVisibleNodes.d.ts.map +1 -1
  17. package/lib/dragAndDropHandler/iterateVisibleNodes.js +16 -15
  18. package/lib/dragAndDropHandler/iterateVisibleNodes.js.map +1 -1
  19. package/lib/elementsRenderer.d.ts +5 -1
  20. package/lib/elementsRenderer.d.ts.map +1 -1
  21. package/lib/elementsRenderer.js +62 -39
  22. package/lib/elementsRenderer.js.map +1 -1
  23. package/lib/events.d.ts +44 -11
  24. package/lib/events.d.ts.map +1 -1
  25. package/lib/index.d.ts +31 -8
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +162 -122
  28. package/lib/index.js.map +1 -1
  29. package/lib/keyHandler.d.ts +11 -5
  30. package/lib/keyHandler.d.ts.map +1 -1
  31. package/lib/keyHandler.js +87 -33
  32. package/lib/keyHandler.js.map +1 -1
  33. package/lib/methodTypes.d.ts +2 -2
  34. package/lib/methodTypes.d.ts.map +1 -1
  35. package/lib/mouseHandler.d.ts +1 -1
  36. package/lib/mouseHandler.d.ts.map +1 -1
  37. package/lib/mouseHandler.js +10 -7
  38. package/lib/mouseHandler.js.map +1 -1
  39. package/lib/node.d.ts +45 -15
  40. package/lib/node.d.ts.map +1 -1
  41. package/lib/node.js +136 -139
  42. package/lib/node.js.map +1 -1
  43. package/lib/nodeElement/folderElement.d.ts +1 -1
  44. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  45. package/lib/nodeElement/folderElement.js +46 -57
  46. package/lib/nodeElement/folderElement.js.map +1 -1
  47. package/lib/nodeElement/ghostDropHint.d.ts.map +1 -1
  48. package/lib/nodeElement/ghostDropHint.js +8 -4
  49. package/lib/nodeElement/ghostDropHint.js.map +1 -1
  50. package/lib/nodeElement/index.d.ts.map +1 -1
  51. package/lib/nodeElement/index.js +6 -6
  52. package/lib/nodeElement/index.js.map +1 -1
  53. package/lib/nodeUtils.d.ts.map +1 -1
  54. package/lib/nodeUtils.js +1 -1
  55. package/lib/nodeUtils.js.map +1 -1
  56. package/lib/options.d.ts +19 -3
  57. package/lib/options.d.ts.map +1 -1
  58. package/lib/requestUrl.d.ts.map +1 -1
  59. package/lib/requestUrl.js +2 -11
  60. package/lib/requestUrl.js.map +1 -1
  61. package/lib/saveStateHandler.d.ts.map +1 -1
  62. package/lib/saveStateHandler.js +21 -41
  63. package/lib/saveStateHandler.js.map +1 -1
  64. package/lib/scrollHandler/containerScrollParent.d.ts.map +1 -1
  65. package/lib/scrollHandler/containerScrollParent.js +3 -8
  66. package/lib/scrollHandler/containerScrollParent.js.map +1 -1
  67. package/lib/scrollHandler/createScrollParent.d.ts.map +1 -1
  68. package/lib/scrollHandler/createScrollParent.js +4 -5
  69. package/lib/scrollHandler/createScrollParent.js.map +1 -1
  70. package/lib/scrollHandler/documentScrollParent.d.ts.map +1 -1
  71. package/lib/scrollHandler/documentScrollParent.js +3 -9
  72. package/lib/scrollHandler/documentScrollParent.js.map +1 -1
  73. package/lib/scrollHandler/scrollParent.d.ts.map +1 -1
  74. package/lib/scrollHandler/scrollParent.js +19 -17
  75. package/lib/scrollHandler/scrollParent.js.map +1 -1
  76. package/lib/selectNodeHandler.d.ts +3 -1
  77. package/lib/selectNodeHandler.d.ts.map +1 -1
  78. package/lib/selectNodeHandler.js +27 -37
  79. package/lib/selectNodeHandler.js.map +1 -1
  80. package/lib/setDefaultOptions.d.ts.map +1 -1
  81. package/lib/setDefaultOptions.js +11 -10
  82. package/lib/setDefaultOptions.js.map +1 -1
  83. package/lib/util.d.ts.map +1 -1
  84. package/lib/util.js +1 -1
  85. package/lib/util.js.map +1 -1
  86. package/lib/version.d.ts +1 -1
  87. package/lib/version.js +1 -1
  88. package/lib/version.js.map +1 -1
  89. package/package.json +42 -33
  90. package/src/animation.ts +13 -11
  91. package/src/dataLoader.ts +11 -0
  92. package/src/dragAndDropHandler/dragElement.ts +2 -1
  93. package/src/dragAndDropHandler/index.ts +70 -75
  94. package/src/dragAndDropHandler/iterateVisibleNodes.ts +18 -16
  95. package/src/elementsRenderer.ts +76 -46
  96. package/src/events.ts +44 -11
  97. package/src/index.ts +210 -150
  98. package/src/keyHandler.ts +95 -41
  99. package/src/methodTypes.ts +3 -3
  100. package/src/mouseHandler.ts +10 -7
  101. package/src/node.ts +155 -148
  102. package/src/nodeElement/folderElement.ts +54 -63
  103. package/src/nodeElement/ghostDropHint.ts +4 -2
  104. package/src/nodeElement/index.ts +9 -15
  105. package/src/nodeUtils.ts +1 -1
  106. package/src/options.ts +19 -3
  107. package/src/requestUrl.ts +4 -11
  108. package/src/saveStateHandler.ts +29 -43
  109. package/src/scrollHandler/containerScrollParent.ts +3 -9
  110. package/src/scrollHandler/createScrollParent.ts +1 -2
  111. package/src/scrollHandler/documentScrollParent.ts +5 -11
  112. package/src/scrollHandler/scrollParent.ts +29 -25
  113. package/src/selectNodeHandler.ts +34 -38
  114. package/src/setDefaultOptions.ts +19 -10
  115. package/src/util.ts +1 -2
  116. package/src/version.ts +1 -1
  117. package/tree_element.css +87 -69
  118. package/tree_element.debug.js +706 -639
  119. package/tree_element.debug.js.map +1 -1
  120. package/tree_element.js +2 -2
  121. package/tree_element.js.map +1 -1
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ClassNames } from "./classNames";
2
- import type { MoveInfo, TreeEvent, TreeEventName, TreeEvents } from "./events";
2
+ import type { TreeEventName, TreeEvents } from "./events";
3
3
  import type { PositionInfo } from "./mouseUtils";
4
4
  import type { NodeData, NodeId, Position } from "./node";
5
5
  import type { TreeElementOptions } from "./options";
@@ -28,19 +28,6 @@ import __version__ from "./version";
28
28
  // the submodules directly, because those are not exposed in package.json.
29
29
  // Type only, so that the iife build keeps exposing the TreeElement class itself
30
30
  // as its global, instead of an object of named exports.
31
- export type {
32
- MoveInfo,
33
- Node,
34
- NodeData,
35
- NodeId,
36
- Position,
37
- SavedState,
38
- SelectNodeOptions,
39
- TreeElementOptions,
40
- TreeEvent,
41
- TreeEventName,
42
- TreeEvents,
43
- };
44
31
 
45
32
  export type TriggerEventProvider = (
46
33
  element: HTMLElement,
@@ -49,7 +36,9 @@ export type TriggerEventProvider = (
49
36
  ) => boolean;
50
37
 
51
38
  interface TreeElementParams extends Partial<TreeElementOptions> {
52
- /** The element the tree is rendered into. Its content is replaced. */
39
+ /**
40
+ The element the tree is rendered into. Its content is replaced.
41
+ */
53
42
  htmlElement: HTMLElement;
54
43
  /** Replaces how events are dispatched. It must return whether the event
55
44
  * was not cancelled. This exists for tests and for integrating with
@@ -61,6 +50,7 @@ export default class TreeElement {
61
50
  private classNames: ClassNames;
62
51
  private dataLoader: DataLoader;
63
52
  private dndHandler: DragAndDropHandler;
53
+ private fallbackTabStop: HTMLElement | null;
64
54
  private htmlElement: HTMLElement;
65
55
  private isInitialized: boolean;
66
56
  private keyHandler: KeyHandler;
@@ -74,7 +64,9 @@ export default class TreeElement {
74
64
  private tree: Node;
75
65
  private triggerEventProvider: TriggerEventProvider;
76
66
 
77
- /** @hidden */
67
+ /**
68
+ @hidden
69
+ */
78
70
  constructor({
79
71
  htmlElement,
80
72
  overrideTriggerEventProvider,
@@ -89,8 +81,11 @@ export default class TreeElement {
89
81
  this.isInitialized = false;
90
82
  this.tree = new Node({}, true);
91
83
  this.nodeMap = new WeakMap();
84
+ this.fallbackTabStop = null;
92
85
 
93
86
  const {
87
+ ariaLabel,
88
+ ariaLabelledBy,
94
89
  autoEscape,
95
90
  buttonLeft,
96
91
  closedIcon,
@@ -118,10 +113,10 @@ export default class TreeElement {
118
113
  const closeNode = this.closeNode.bind(this);
119
114
  const getNodeElement = this.getNodeElement.bind(this);
120
115
  const getNodeElementForNode = this.getNodeElementForNode.bind(this);
116
+ const getFocusedNode = this.getFocusedNode.bind(this);
121
117
  const getNodeById = this.getNodeById.bind(this);
122
118
  const getSelectedNode = this.getSelectedNode.bind(this);
123
119
  const getTree = this.getTree.bind(this);
124
- const isFocusOnTree = this.isFocusOnTree.bind(this);
125
120
  const loadData = this.loadData.bind(this);
126
121
  const openNode = this.openNode.bind(this);
127
122
  const openParents = this.openParents.bind(this);
@@ -130,6 +125,7 @@ export default class TreeElement {
130
125
  const setNodeElement = this.setNodeElement.bind(this);
131
126
  const treeElement = this.htmlElement;
132
127
  const triggerEvent = this.triggerEvent.bind(this);
128
+ const updateTabStop = this.updateTabStop.bind(this);
133
129
 
134
130
  const saveState = () => {
135
131
  saveStateHandler.saveState();
@@ -143,6 +139,7 @@ export default class TreeElement {
143
139
  openParents,
144
140
  saveState,
145
141
  triggerEvent,
142
+ updateTabStop,
146
143
  });
147
144
 
148
145
  const addToSelection =
@@ -208,14 +205,18 @@ export default class TreeElement {
208
205
 
209
206
  const keyHandler = new KeyHandler({
210
207
  closeNode,
208
+ getFocusedNode,
211
209
  getSelectedNode,
212
- isFocusOnTree,
210
+ getTree,
213
211
  keyboardSupport,
214
212
  openNode,
215
213
  selectNode,
214
+ triggerEvent,
216
215
  });
217
216
 
218
217
  const renderer = new ElementsRenderer({
218
+ ariaLabel,
219
+ ariaLabelledBy,
219
220
  autoEscape,
220
221
  buttonLeft,
221
222
  classNames,
@@ -320,12 +321,13 @@ export default class TreeElement {
320
321
  * @param mustSetFocus - Move the focus to the node. Default `true`.
321
322
  * @group Selection
322
323
  */
323
- public addToSelection(node: Node, mustSetFocus?: boolean) {
324
+ public addToSelection(node: Node, mustSetFocus = true) {
324
325
  this.selectNodeHandler.addToSelection(node);
325
326
  this.openParents(node);
326
327
 
327
- this.getNodeElementForNode(node).select(mustSetFocus ?? true);
328
+ this.getNodeElementForNode(node).select(mustSetFocus);
328
329
 
330
+ this.updateTabStop();
329
331
  this.saveState();
330
332
  }
331
333
 
@@ -349,20 +351,29 @@ export default class TreeElement {
349
351
  }
350
352
 
351
353
  /**
352
- * Closes a folder.
354
+ * Closes a folder. Await the promise when you need to know the closing
355
+ * animation has finished.
356
+ *
357
+ * @example
358
+ * ```js
359
+ * await tree.closeNode(node);
360
+ * console.log("closed");
361
+ * ```
353
362
  *
354
363
  * @param slide - Override the `slide` option for this call.
355
364
  * @group Opening and closing
356
365
  */
357
- public closeNode(node: Node, slide?: boolean): void {
358
- if (node.isFolder() || node.isEmptyFolder) {
359
- this.createFolderElement(node).close(
360
- slide ?? this.options.slide,
361
- this.options.animationSpeed,
362
- );
363
-
364
- this.saveState();
366
+ public async closeNode(node: Node, slide?: boolean): Promise<void> {
367
+ if (!(node.isFolder() || node.isEmptyFolder)) {
368
+ return;
365
369
  }
370
+
371
+ await this.createFolderElement(node).close(
372
+ slide ?? this.options.slide,
373
+ this.options.animationSpeed,
374
+ );
375
+
376
+ this.saveState();
366
377
  }
367
378
 
368
379
  /**
@@ -391,11 +402,7 @@ export default class TreeElement {
391
402
  `li.${this.classNames.common}`,
392
403
  );
393
404
 
394
- if (liElement) {
395
- return this.nodeMap.get(liElement) ?? null;
396
- } else {
397
- return null;
398
- }
405
+ return liElement ? (this.nodeMap.get(liElement) ?? null) : null;
399
406
  }
400
407
 
401
408
  /**
@@ -704,6 +711,7 @@ export default class TreeElement {
704
711
  this.selectNodeHandler.removeFromSelection(node);
705
712
 
706
713
  this.getNodeElementForNode(node).deselect();
714
+ this.updateTabStop();
707
715
  this.saveState();
708
716
  }
709
717
 
@@ -752,7 +760,11 @@ export default class TreeElement {
752
760
  return;
753
761
  }
754
762
 
755
- this.selectNodeHandler.selectSingleNode(node, options);
763
+ // Unlike a click, selecting the selected node again keeps it selected
764
+ this.selectNodeHandler.selectSingleNode(node, {
765
+ mustToggle: false,
766
+ ...options,
767
+ });
756
768
  }
757
769
 
758
770
  /**
@@ -778,18 +790,20 @@ export default class TreeElement {
778
790
  }
779
791
 
780
792
  /**
781
- * Closes an open node and opens a closed one.
793
+ * Closes an open node and opens a closed one. Await the promise when you
794
+ * need to know the node is really open or closed, like with `openNode` and
795
+ * `closeNode`.
782
796
  *
783
797
  * @param slide - Override the `slide` option for this call.
784
798
  * @group Opening and closing
785
799
  */
786
- public toggle(node: Node, slide: boolean | null = null) {
800
+ public async toggle(node: Node, slide: boolean | null = null): Promise<void> {
787
801
  const mustSlide = slide ?? this.options.slide;
788
802
 
789
803
  if (node.is_open) {
790
- this.closeNode(node, mustSlide);
804
+ await this.closeNode(node, mustSlide);
791
805
  } else {
792
- void this.openNode(node, mustSlide);
806
+ await this.openNode(node, mustSlide);
793
807
  }
794
808
  }
795
809
 
@@ -835,7 +849,7 @@ export default class TreeElement {
835
849
  if (
836
850
  typeof data === "object" &&
837
851
  data.children &&
838
- data.children instanceof Array
852
+ Array.isArray(data.children)
839
853
  ) {
840
854
  node.removeChildren();
841
855
 
@@ -847,6 +861,34 @@ export default class TreeElement {
847
861
  this.refreshElements(node);
848
862
  }
849
863
 
864
+ // Open the nodes up to the autoOpen level. A folder that is loaded on
865
+ // demand is fetched first, and its children are opened when the data has
866
+ // arrived.
867
+ private async autoOpenNodesOnDemand(): Promise<void> {
868
+ const maxLevel = this.getAutoOpenMaxLevel();
869
+
870
+ const openNodes = async (): Promise<void> => {
871
+ const loading: Promise<void>[] = [];
872
+
873
+ this.tree.iterate((node: Node, level: number) => {
874
+ if (node.load_on_demand) {
875
+ if (!node.is_loading) {
876
+ loading.push(this.openNode(node, false).then(openNodes));
877
+ }
878
+
879
+ return false;
880
+ }
881
+ void this.openNode(node, false);
882
+
883
+ return level !== maxLevel;
884
+ });
885
+
886
+ await Promise.all(loading);
887
+ };
888
+
889
+ await openNodes();
890
+ }
891
+
850
892
  private createFolderElement(node: Node) {
851
893
  const classNames = this.classNames;
852
894
  const closedIconElement = this.renderer.closedIconElement;
@@ -854,11 +896,19 @@ export default class TreeElement {
854
896
  this.scrollHandler,
855
897
  );
856
898
  const openedIconElement = this.renderer.openedIconElement;
857
- const renderChildren = this.renderer.renderChildren.bind(this.renderer);
858
899
  const tabIndex = this.options.tabIndex;
859
900
  const treeElement = this.htmlElement;
860
901
  const triggerEvent = this.triggerEvent.bind(this);
861
902
 
903
+ // A selected node in a closed folder is rendered when the folder is
904
+ // opened, which can be after it was selected. It becomes the tab stop
905
+ // then, so the fallback tab stop must go.
906
+ const renderChildren = (folder: Node) => {
907
+ const ul = this.renderer.renderChildren(folder);
908
+ this.updateTabStop();
909
+ return ul;
910
+ };
911
+
862
912
  return new FolderElement({
863
913
  classNames,
864
914
  closedIconElement,
@@ -896,13 +946,7 @@ export default class TreeElement {
896
946
  private createRequestUrl(node?: Node): null | RequestUrl {
897
947
  const dataUrl = this.options.dataUrl;
898
948
 
899
- let url;
900
-
901
- if (typeof dataUrl === "function") {
902
- url = dataUrl(node);
903
- } else {
904
- url = dataUrl;
905
- }
949
+ const url = typeof dataUrl === "function" ? dataUrl(node) : dataUrl;
906
950
 
907
951
  if (!url) {
908
952
  return null;
@@ -944,9 +988,11 @@ export default class TreeElement {
944
988
  private getAutoOpenMaxLevel(): number {
945
989
  if (this.options.autoOpen === true) {
946
990
  return -1;
947
- } else if (typeof this.options.autoOpen === "number") {
991
+ }
992
+ if (typeof this.options.autoOpen === "number") {
948
993
  return this.options.autoOpen;
949
- } else if (typeof this.options.autoOpen === "string") {
994
+ }
995
+ if (typeof this.options.autoOpen === "string") {
950
996
  return parseInt(this.options.autoOpen, 10);
951
997
  }
952
998
 
@@ -954,27 +1000,55 @@ export default class TreeElement {
954
1000
  return 0;
955
1001
  }
956
1002
 
957
- private getNodeElement(element: HTMLElement): NodeElement | null {
958
- const node = this.getNode(element);
959
- if (node) {
960
- return this.getNodeElementForNode(node);
961
- } else {
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;
1012
+ }
1013
+
1014
+ // The node of the HTML element that has the focus, if it is in this tree
1015
+ private getFocusedNode(): Node | null {
1016
+ const activeElement = document.activeElement;
1017
+
1018
+ /* istanbul ignore if */
1019
+ if (!activeElement) {
1020
+ return null;
1021
+ }
1022
+
1023
+ // The keyboard must still work for input elements.
1024
+ const tagName = activeElement.tagName;
1025
+ if (tagName !== "A" && tagName !== "SPAN") {
962
1026
  return null;
963
1027
  }
1028
+
1029
+ const node = this.getNode(activeElement as HTMLElement);
1030
+ return node?.tree === this.tree ? node : null;
1031
+ }
1032
+
1033
+ private getNodeElement(element: HTMLElement): NodeElement | null {
1034
+ const node = this.getNode(element);
1035
+ return node ? this.getNodeElementForNode(node) : null;
964
1036
  }
965
1037
 
966
1038
  private getNodeElementForNode(node: Node): NodeElement {
967
- if (node.isFolder()) {
968
- return this.createFolderElement(node);
969
- } else {
970
- return this.createNodeElement(node);
971
- }
1039
+ return node.isFolder()
1040
+ ? this.createFolderElement(node)
1041
+ : this.createNodeElement(node);
972
1042
  }
973
1043
 
974
1044
  private getNodeIdToBeSelected(): NodeId | null {
975
1045
  return this.saveStateHandler.getNodeIdToBeSelected();
976
1046
  }
977
1047
 
1048
+ private hasRenderedSelectedNode(): boolean {
1049
+ return this.getSelectedNodes().some((node) => Boolean(node.element));
1050
+ }
1051
+
978
1052
  private initData(): void {
979
1053
  if (this.options.data) {
980
1054
  this.loadData(this.options.data);
@@ -991,10 +1065,12 @@ export default class TreeElement {
991
1065
 
992
1066
  private initTree(data: NodeData[]): void {
993
1067
  const doInit = (): void => {
994
- if (!this.isInitialized) {
995
- this.isInitialized = true;
996
- this.triggerEvent("tree.init");
1068
+ if (this.isInitialized) {
1069
+ return;
997
1070
  }
1071
+
1072
+ this.isInitialized = true;
1073
+ this.triggerEvent("tree.init");
998
1074
  };
999
1075
 
1000
1076
  this.tree = new this.options.nodeClass(null, true, this.options.nodeClass);
@@ -1017,31 +1093,15 @@ export default class TreeElement {
1017
1093
 
1018
1094
  // Does an HTML element of the tree have the focus?
1019
1095
  private isFocusOnTree(): boolean {
1020
- const activeElement = document.activeElement;
1021
-
1022
- /* istanbul ignore if */
1023
- if (!activeElement) {
1024
- return false;
1025
- }
1026
-
1027
- // The keyboard must still work for input elements.
1028
- const tagName = activeElement.tagName;
1029
- if (tagName !== "A" && tagName !== "SPAN") {
1030
- return false;
1031
- }
1032
-
1033
- const node = this.getNode(activeElement as HTMLElement);
1034
- return node?.tree === this.tree;
1096
+ return this.getFocusedNode() !== null;
1035
1097
  }
1036
1098
 
1037
1099
  private isSelectedNodeInSubtree(subtree: Node): boolean {
1038
1100
  const selectedNode = this.getSelectedNode();
1039
1101
 
1040
- if (!selectedNode) {
1041
- return false;
1042
- } else {
1043
- return subtree === selectedNode || subtree.isParentOf(selectedNode);
1044
- }
1102
+ return selectedNode
1103
+ ? subtree === selectedNode || subtree.isParentOf(selectedNode)
1104
+ : false;
1045
1105
  }
1046
1106
 
1047
1107
  private async loadFolderOnDemand(node: Node, slide: boolean): Promise<void> {
@@ -1049,6 +1109,13 @@ export default class TreeElement {
1049
1109
 
1050
1110
  await this.loadDataFromUrl(undefined, node);
1051
1111
 
1112
+ // Loading failed, or there is no url. Opening the node now would load it
1113
+ // again, without end. Leave it closed so the user can try again.
1114
+ if (node.load_on_demand) {
1115
+ node.is_loading = false;
1116
+ return;
1117
+ }
1118
+
1052
1119
  await this.openNode(node, slide);
1053
1120
  }
1054
1121
 
@@ -1062,11 +1129,9 @@ export default class TreeElement {
1062
1129
  }
1063
1130
 
1064
1131
  private mouseCapture(positionInfo: PositionInfo): boolean | null {
1065
- if (!this.options.dragAndDrop) {
1066
- return false;
1067
- }
1068
-
1069
- return this.dndHandler.mouseCapture(positionInfo);
1132
+ return this.options.dragAndDrop
1133
+ ? this.dndHandler.mouseCapture(positionInfo)
1134
+ : false;
1070
1135
  }
1071
1136
 
1072
1137
  private mouseDrag(positionInfo: PositionInfo): boolean {
@@ -1082,6 +1147,7 @@ export default class TreeElement {
1082
1147
 
1083
1148
  private mouseStart(positionInfo: PositionInfo): boolean {
1084
1149
  /* istanbul ignore if */
1150
+ // eslint-disable-next-line unicorn/prefer-ternary
1085
1151
  if (!this.options.dragAndDrop) {
1086
1152
  return false;
1087
1153
  }
@@ -1123,6 +1189,8 @@ export default class TreeElement {
1123
1189
  this.selectCurrentNode(mustSetFocus);
1124
1190
  }
1125
1191
 
1192
+ this.updateTabStop();
1193
+
1126
1194
  this.triggerEvent("tree.refresh");
1127
1195
  }
1128
1196
 
@@ -1132,10 +1200,12 @@ export default class TreeElement {
1132
1200
 
1133
1201
  private selectCurrentNode(mustSetFocus: boolean): void {
1134
1202
  const node = this.getSelectedNode();
1135
- if (node) {
1136
- const nodeElement = this.getNodeElementForNode(node);
1137
- nodeElement.select(mustSetFocus);
1203
+ if (!node) {
1204
+ return;
1138
1205
  }
1206
+
1207
+ const nodeElement = this.getNodeElementForNode(node);
1208
+ nodeElement.select(mustSetFocus);
1139
1209
  }
1140
1210
 
1141
1211
  // Set initial state, either by restoring the state or auto-opening nodes
@@ -1145,14 +1215,13 @@ export default class TreeElement {
1145
1215
  // result: is state restored, must load on demand?
1146
1216
  const state = this.saveStateHandler.getStateFromStorage();
1147
1217
 
1148
- if (!state) {
1149
- return [false, false];
1150
- } else {
1218
+ if (state) {
1151
1219
  const mustLoadOnDemand = this.saveStateHandler.setInitialState(state);
1152
1220
 
1153
1221
  // return true: the state is restored
1154
1222
  return [true, mustLoadOnDemand];
1155
1223
  }
1224
+ return [false, false];
1156
1225
  };
1157
1226
 
1158
1227
  const autoOpenNodes = (): boolean => {
@@ -1168,12 +1237,12 @@ export default class TreeElement {
1168
1237
  if (node.load_on_demand) {
1169
1238
  mustLoadOnDemand = true;
1170
1239
  return false;
1171
- } else if (!node.hasChildren()) {
1172
- return false;
1173
- } else {
1240
+ }
1241
+ if (node.hasChildren()) {
1174
1242
  node.is_open = true;
1175
1243
  return level !== maxLevel;
1176
1244
  }
1245
+ return false;
1177
1246
  });
1178
1247
 
1179
1248
  return mustLoadOnDemand;
@@ -1190,60 +1259,13 @@ export default class TreeElement {
1190
1259
 
1191
1260
  // Set the initial state for nodes that are loaded on demand
1192
1261
  private async setInitialStateOnDemand(): Promise<void> {
1193
- return new Promise((resolve) => {
1194
- const restoreState = (): boolean => {
1195
- const state = this.saveStateHandler.getStateFromStorage();
1196
-
1197
- if (!state) {
1198
- return false;
1199
- } else {
1200
- void this.saveStateHandler.setInitialStateOnDemand(state).then(() => {
1201
- resolve();
1202
- });
1203
-
1204
- return true;
1205
- }
1206
- };
1207
-
1208
- const autoOpenNodes = (): void => {
1209
- const maxLevel = this.getAutoOpenMaxLevel();
1210
- let loadingCount = 0;
1211
-
1212
- const loadAndOpenNode = (node: Node): void => {
1213
- loadingCount += 1;
1214
- void this.openNode(node, false).then(() => {
1215
- loadingCount -= 1;
1216
- openNodes();
1217
- });
1218
- };
1219
-
1220
- const openNodes = (): void => {
1221
- this.tree.iterate((node: Node, level: number) => {
1222
- if (node.load_on_demand) {
1223
- if (!node.is_loading) {
1224
- loadAndOpenNode(node);
1225
- }
1226
-
1227
- return false;
1228
- } else {
1229
- void this.openNode(node, false);
1230
-
1231
- return level !== maxLevel;
1232
- }
1233
- });
1234
-
1235
- if (loadingCount === 0) {
1236
- resolve();
1237
- }
1238
- };
1239
-
1240
- openNodes();
1241
- };
1262
+ const state = this.saveStateHandler.getStateFromStorage();
1242
1263
 
1243
- if (!restoreState()) {
1244
- autoOpenNodes();
1245
- }
1246
- });
1264
+ if (state) {
1265
+ await this.saveStateHandler.setInitialStateOnDemand(state);
1266
+ } else {
1267
+ await this.autoOpenNodesOnDemand();
1268
+ }
1247
1269
  }
1248
1270
 
1249
1271
  // Set this HTML element to this node in the node map.
@@ -1257,4 +1279,42 @@ export default class TreeElement {
1257
1279
  ): boolean {
1258
1280
  return this.triggerEventProvider(this.htmlElement, eventName, values);
1259
1281
  }
1282
+
1283
+ /* Keyboard users reach the tree through the tab stop. The selected node is
1284
+ * the tab stop, see NodeElement.select. Without a selected node, the first
1285
+ * node is the tab stop, so the tree can always be reached.
1286
+ */
1287
+ private updateTabStop(): void {
1288
+ const tabIndex = this.options.tabIndex;
1289
+ const previousTabStop = this.fallbackTabStop;
1290
+ const tabStop =
1291
+ tabIndex == null || this.hasRenderedSelectedNode()
1292
+ ? null
1293
+ : this.getFirstTitleSpan();
1294
+
1295
+ if (
1296
+ previousTabStop &&
1297
+ previousTabStop !== tabStop &&
1298
+ previousTabStop.getAttribute("aria-selected") !== "true"
1299
+ ) {
1300
+ previousTabStop.setAttribute("tabindex", "-1");
1301
+ }
1302
+
1303
+ if (tabStop && tabIndex != null) {
1304
+ tabStop.setAttribute("tabindex", String(tabIndex));
1305
+ }
1306
+
1307
+ this.fallbackTabStop = tabStop;
1308
+ }
1260
1309
  }
1310
+
1311
+ export {
1312
+ type MoveInfo,
1313
+ type TreeEvent,
1314
+ type TreeEventName,
1315
+ type TreeEvents,
1316
+ } from "./events";
1317
+ export { type Node, type NodeData, type NodeId, type Position } from "./node";
1318
+ export { type TreeElementOptions } from "./options";
1319
+ export { type SavedState } from "./saveStateHandler";
1320
+ export { type SelectNodeOptions } from "./selectNodeHandler";