tree-element 0.2.0 → 0.3.1

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 (123) hide show
  1. package/README.md +14 -6
  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 +23 -6
  57. package/lib/options.d.ts.map +1 -1
  58. package/lib/requestUrl.d.ts +0 -1
  59. package/lib/requestUrl.d.ts.map +1 -1
  60. package/lib/requestUrl.js +4 -22
  61. package/lib/requestUrl.js.map +1 -1
  62. package/lib/saveStateHandler.d.ts.map +1 -1
  63. package/lib/saveStateHandler.js +21 -41
  64. package/lib/saveStateHandler.js.map +1 -1
  65. package/lib/scrollHandler/containerScrollParent.d.ts.map +1 -1
  66. package/lib/scrollHandler/containerScrollParent.js +3 -8
  67. package/lib/scrollHandler/containerScrollParent.js.map +1 -1
  68. package/lib/scrollHandler/createScrollParent.d.ts.map +1 -1
  69. package/lib/scrollHandler/createScrollParent.js +4 -5
  70. package/lib/scrollHandler/createScrollParent.js.map +1 -1
  71. package/lib/scrollHandler/documentScrollParent.d.ts.map +1 -1
  72. package/lib/scrollHandler/documentScrollParent.js +3 -9
  73. package/lib/scrollHandler/documentScrollParent.js.map +1 -1
  74. package/lib/scrollHandler/scrollParent.d.ts.map +1 -1
  75. package/lib/scrollHandler/scrollParent.js +19 -17
  76. package/lib/scrollHandler/scrollParent.js.map +1 -1
  77. package/lib/selectNodeHandler.d.ts +3 -1
  78. package/lib/selectNodeHandler.d.ts.map +1 -1
  79. package/lib/selectNodeHandler.js +27 -37
  80. package/lib/selectNodeHandler.js.map +1 -1
  81. package/lib/setDefaultOptions.d.ts +3 -0
  82. package/lib/setDefaultOptions.d.ts.map +1 -1
  83. package/lib/setDefaultOptions.js +21 -20
  84. package/lib/setDefaultOptions.js.map +1 -1
  85. package/lib/util.d.ts.map +1 -1
  86. package/lib/util.js +1 -1
  87. package/lib/util.js.map +1 -1
  88. package/lib/version.d.ts +1 -1
  89. package/lib/version.js +1 -1
  90. package/lib/version.js.map +1 -1
  91. package/package.json +44 -35
  92. package/src/animation.ts +13 -11
  93. package/src/dataLoader.ts +11 -0
  94. package/src/dragAndDropHandler/dragElement.ts +2 -1
  95. package/src/dragAndDropHandler/index.ts +70 -75
  96. package/src/dragAndDropHandler/iterateVisibleNodes.ts +18 -16
  97. package/src/elementsRenderer.ts +76 -46
  98. package/src/events.ts +44 -11
  99. package/src/index.ts +210 -150
  100. package/src/keyHandler.ts +95 -41
  101. package/src/methodTypes.ts +3 -3
  102. package/src/mouseHandler.ts +10 -7
  103. package/src/node.ts +155 -148
  104. package/src/nodeElement/folderElement.ts +54 -63
  105. package/src/nodeElement/ghostDropHint.ts +4 -2
  106. package/src/nodeElement/index.ts +9 -15
  107. package/src/nodeUtils.ts +1 -1
  108. package/src/options.ts +23 -6
  109. package/src/requestUrl.ts +4 -25
  110. package/src/saveStateHandler.ts +29 -43
  111. package/src/scrollHandler/containerScrollParent.ts +3 -9
  112. package/src/scrollHandler/createScrollParent.ts +1 -2
  113. package/src/scrollHandler/documentScrollParent.ts +5 -11
  114. package/src/scrollHandler/scrollParent.ts +29 -25
  115. package/src/selectNodeHandler.ts +34 -38
  116. package/src/setDefaultOptions.ts +31 -19
  117. package/src/util.ts +1 -2
  118. package/src/version.ts +1 -1
  119. package/tree_element.css +87 -69
  120. package/tree_element.debug.js +717 -659
  121. package/tree_element.debug.js.map +1 -1
  122. package/tree_element.js +2 -2
  123. package/tree_element.js.map +1 -1
@@ -6,6 +6,8 @@ import type { IconElement, OnCreateLi } from "./options";
6
6
  import { getBoolString } from "./util";
7
7
 
8
8
  interface ElementsRendererParams {
9
+ ariaLabel?: string;
10
+ ariaLabelledBy?: string;
9
11
  autoEscape: boolean;
10
12
  buttonLeft: boolean;
11
13
  classNames: ClassNames;
@@ -36,6 +38,8 @@ interface ElementsRendererParams {
36
38
  export default class ElementsRenderer {
37
39
  public closedIconElement?: globalThis.Node;
38
40
  public openedIconElement?: globalThis.Node;
41
+ private ariaLabel?: string;
42
+ private ariaLabelledBy?: string;
39
43
  private autoEscape: boolean;
40
44
  private buttonLeft: boolean;
41
45
  private classNames: ClassNames;
@@ -55,21 +59,25 @@ export default class ElementsRenderer {
55
59
  private tabIndex?: number;
56
60
 
57
61
  constructor({
62
+ ariaLabel,
63
+ ariaLabelledBy,
58
64
  autoEscape,
59
65
  buttonLeft,
60
66
  classNames,
61
- closedIcon,
67
+ closedIcon = "-",
62
68
  dragAndDrop,
63
69
  getTree,
64
70
  htmlElement,
65
71
  isNodeSelected,
66
72
  onCreateLi,
67
- openedIcon,
73
+ openedIcon = "+",
68
74
  rtl,
69
75
  setNodeElement,
70
76
  showEmptyFolder,
71
77
  tabIndex,
72
78
  }: ElementsRendererParams) {
79
+ this.ariaLabel = ariaLabel;
80
+ this.ariaLabelledBy = ariaLabelledBy;
73
81
  this.autoEscape = autoEscape;
74
82
  this.buttonLeft = buttonLeft;
75
83
  this.classNames = classNames;
@@ -82,8 +90,8 @@ export default class ElementsRenderer {
82
90
  this.setNodeElement = setNodeElement;
83
91
  this.showEmptyFolder = showEmptyFolder;
84
92
  this.tabIndex = tabIndex;
85
- this.openedIconElement = this.createButtonElement(openedIcon ?? "+");
86
- this.closedIconElement = this.createButtonElement(closedIcon ?? "-");
93
+ this.openedIconElement = this.createButtonElement(openedIcon);
94
+ this.closedIconElement = this.createButtonElement(closedIcon);
87
95
 
88
96
  this.rootUlTemplate = this.createUlTemplate(true);
89
97
  this.groupUlTemplate = this.createUlTemplate(false);
@@ -104,16 +112,14 @@ export default class ElementsRenderer {
104
112
  * null when the folder is not rendered or has no children.
105
113
  */
106
114
  public renderChildren(node: Node): HTMLUListElement | null {
107
- if (!node.element || !node.hasChildren()) {
108
- return null;
109
- }
110
-
111
- return this.createDomElements(
112
- node.element,
113
- node.children,
114
- false,
115
- node.getLevel() + 1,
116
- );
115
+ return !node.element || !node.hasChildren()
116
+ ? null
117
+ : this.createDomElements(
118
+ node.element,
119
+ node.children,
120
+ false,
121
+ node.getLevel() + 1,
122
+ );
117
123
  }
118
124
 
119
125
  public renderFromNode(node: Node): void {
@@ -152,14 +158,12 @@ export default class ElementsRenderer {
152
158
  private createButtonElement(value: IconElement): globalThis.Node | undefined {
153
159
  if (typeof value === "string") {
154
160
  const template = document.createElement("template");
161
+ // eslint-disable-next-line no-unsanitized/property -- icon options are documented to accept html
155
162
  template.innerHTML = value;
156
163
 
157
164
  return template.content;
158
- } else if (value.nodeType) {
159
- return value;
160
- } else {
161
- return undefined;
162
165
  }
166
+ return value.nodeType ? value : undefined;
163
167
  }
164
168
 
165
169
  private createDomElements(
@@ -170,11 +174,11 @@ export default class ElementsRenderer {
170
174
  ): HTMLUListElement {
171
175
  const template = isRootNode ? this.rootUlTemplate : this.groupUlTemplate;
172
176
  const ul = template.cloneNode() as HTMLUListElement;
173
- element.appendChild(ul);
177
+ element.append(ul);
174
178
 
175
179
  for (const child of children) {
176
180
  const li = this.createLi(child, level);
177
- ul.appendChild(li);
181
+ ul.append(li);
178
182
 
179
183
  if (this.mustRenderChildren(child)) {
180
184
  this.createDomElements(li, child.children, false, level + 1);
@@ -198,11 +202,7 @@ export default class ElementsRenderer {
198
202
  li.classList.add(this.classNames.selected);
199
203
  }
200
204
 
201
- if (node.is_loading) {
202
- li.classList.add(this.classNames.loading);
203
- }
204
-
205
- // li > div > [button link], title span, [button link]
205
+ // li > div > [toggler], title span, [toggler]
206
206
  const div = li.firstChild as HTMLDivElement;
207
207
  const titleSpan = div.childNodes[
208
208
  this.buttonLeft ? 1 : 0
@@ -210,11 +210,16 @@ export default class ElementsRenderer {
210
210
 
211
211
  this.fillTitleSpan(titleSpan, node.name, isSelected, level);
212
212
 
213
+ if (node.is_loading) {
214
+ li.classList.add(this.classNames.loading);
215
+ titleSpan.setAttribute("aria-busy", "true");
216
+ }
217
+
213
218
  return li;
214
219
  }
215
220
 
216
221
  /* Template for a folder <li>:
217
- * li > div > [button link], title span, [button link]
222
+ * li > div > [toggler], title span, [toggler]
218
223
  * The toggler icon and the aria-expanded attribute depend on the open
219
224
  * state, so there is a template for each.
220
225
  */
@@ -228,29 +233,31 @@ export default class ElementsRenderer {
228
233
  const li = this.createLiTemplate(liClasses.join(" "));
229
234
  const div = li.firstChild as HTMLDivElement;
230
235
 
231
- // button link
232
- const buttonLink = document.createElement("a");
233
- buttonLink.className = this.getButtonClasses(isOpen);
236
+ // The toggler is hidden from screen readers: the icon is decoration, and
237
+ // aria-expanded on the treeitem has the open state.
238
+ const toggler = document.createElement("span");
239
+ toggler.className = this.getButtonClasses(isOpen);
240
+ toggler.setAttribute("aria-hidden", "true");
234
241
 
235
242
  const iconElement = isOpen
236
243
  ? this.openedIconElement
237
244
  : this.closedIconElement;
238
245
 
239
246
  if (iconElement) {
240
- buttonLink.appendChild(iconElement.cloneNode(true));
247
+ toggler.append(iconElement.cloneNode(true));
241
248
  }
242
249
 
243
250
  if (this.buttonLeft) {
244
- div.appendChild(buttonLink);
251
+ div.append(toggler);
245
252
  }
246
253
 
247
254
  // title span
248
255
  const titleSpan = this.createTitleSpanTemplate(true);
249
256
  titleSpan.setAttribute("aria-expanded", getBoolString(isOpen));
250
- div.appendChild(titleSpan);
257
+ div.append(titleSpan);
251
258
 
252
259
  if (!this.buttonLeft) {
253
- div.appendChild(buttonLink);
260
+ div.append(toggler);
254
261
  }
255
262
 
256
263
  return li;
@@ -279,7 +286,9 @@ export default class ElementsRenderer {
279
286
  return li;
280
287
  }
281
288
 
282
- /* Create the outer part of a <li> template: li > div */
289
+ /*
290
+ Create the outer part of a <li> template: li > div
291
+ */
283
292
  private createLiTemplate(liClasses: string): HTMLLIElement {
284
293
  const li = document.createElement("li");
285
294
  li.className = liClasses;
@@ -289,7 +298,7 @@ export default class ElementsRenderer {
289
298
  div.className = `${this.classNames.element} ${this.classNames.common}`;
290
299
  div.setAttribute("role", "none");
291
300
 
292
- li.appendChild(div);
301
+ li.append(div);
293
302
 
294
303
  return li;
295
304
  }
@@ -314,12 +323,14 @@ export default class ElementsRenderer {
314
323
  return li;
315
324
  }
316
325
 
317
- /* Template for a <li> without children: li > div > title span */
326
+ /*
327
+ Template for a <li> without children: li > div > title span
328
+ */
318
329
  private createNodeTemplate(): HTMLLIElement {
319
330
  const li = this.createLiTemplate(this.classNames.common);
320
331
  const div = li.firstChild as HTMLDivElement;
321
332
 
322
- div.appendChild(this.createTitleSpanTemplate(false));
333
+ div.append(this.createTitleSpanTemplate(false));
323
334
 
324
335
  return li;
325
336
  }
@@ -339,6 +350,11 @@ export default class ElementsRenderer {
339
350
  titleSpan.setAttribute("role", "treeitem");
340
351
  titleSpan.setAttribute("aria-selected", "false");
341
352
 
353
+ // Every node is focusable, but only the tab stop is in the tab order
354
+ if (this.tabIndex !== undefined) {
355
+ titleSpan.setAttribute("tabindex", "-1");
356
+ }
357
+
342
358
  return titleSpan;
343
359
  }
344
360
 
@@ -346,16 +362,16 @@ export default class ElementsRenderer {
346
362
  let classString;
347
363
  let role;
348
364
 
349
- if (!isRootNode) {
350
- classString = "";
351
- role = "group";
352
- } else {
365
+ if (isRootNode) {
353
366
  classString = this.classNames.tree;
354
367
  role = "tree";
355
368
 
356
369
  if (this.rtl) {
357
370
  classString += ` ${this.classNames.rtl}`;
358
371
  }
372
+ } else {
373
+ classString = "";
374
+ role = "group";
359
375
  }
360
376
 
361
377
  if (this.dragAndDrop) {
@@ -367,18 +383,29 @@ export default class ElementsRenderer {
367
383
 
368
384
  ul.setAttribute("role", role);
369
385
 
386
+ if (isRootNode) {
387
+ if (this.ariaLabel !== undefined) {
388
+ ul.setAttribute("aria-label", this.ariaLabel);
389
+ }
390
+
391
+ if (this.ariaLabelledBy !== undefined) {
392
+ ul.setAttribute("aria-labelledby", this.ariaLabelledBy);
393
+ }
394
+ }
395
+
370
396
  return ul;
371
397
  }
372
398
 
373
- /* Set the parts of a cloned title span that differ per node */
399
+ /*
400
+ Set the parts of a cloned title span that differ per node
401
+ */
374
402
  private fillTitleSpan(
375
403
  titleSpan: HTMLSpanElement,
376
404
  nodeName: string,
377
405
  isSelected: boolean,
378
406
  level: number,
379
407
  ): void {
380
- titleSpan.setAttribute("aria-label", nodeName);
381
- titleSpan.setAttribute("aria-level", `${level}`);
408
+ titleSpan.setAttribute("aria-level", String(level));
382
409
 
383
410
  if (isSelected) {
384
411
  titleSpan.setAttribute("aria-selected", "true");
@@ -386,13 +413,14 @@ export default class ElementsRenderer {
386
413
  const tabIndex = this.tabIndex;
387
414
 
388
415
  if (tabIndex !== undefined) {
389
- titleSpan.setAttribute("tabindex", `${tabIndex}`);
416
+ titleSpan.setAttribute("tabindex", String(tabIndex));
390
417
  }
391
418
  }
392
419
 
393
420
  if (this.autoEscape) {
394
421
  titleSpan.textContent = nodeName;
395
422
  } else {
423
+ // eslint-disable-next-line no-unsanitized/property -- the user opted out of escaping with autoEscape: false
396
424
  titleSpan.innerHTML = nodeName;
397
425
  }
398
426
  }
@@ -413,7 +441,9 @@ export default class ElementsRenderer {
413
441
  return classes.join(" ");
414
442
  }
415
443
 
416
- /* The children of a closed folder are rendered when it is opened */
444
+ /*
445
+ The children of a closed folder are rendered when it is opened
446
+ */
417
447
  private mustRenderChildren(node: Node): boolean {
418
448
  return node.hasChildren() && node.is_open === true;
419
449
  }
package/src/events.ts CHANGED
@@ -94,6 +94,23 @@ export interface TreeEvents {
94
94
  node: Node;
95
95
  originalEvent: MouseEvent;
96
96
  };
97
+ /**
98
+ * Dispatched when `Enter` is pressed on the selected node, or on the
99
+ * focused node when no node is selected. Use it for what a click on a
100
+ * link would do, like opening the edit page of the node:
101
+ *
102
+ * ```js
103
+ * element.addEventListener("tree.activate", (e) => {
104
+ * window.location.href = e.detail.node.url;
105
+ * });
106
+ * ```
107
+ *
108
+ * @group Keyboard
109
+ */
110
+ "tree.activate": {
111
+ node: Node;
112
+ originalEvent: KeyboardEvent;
113
+ };
97
114
  /**
98
115
  * Dispatched when a folder is opened, after the animation.
99
116
  *
@@ -124,15 +141,19 @@ export interface TreeEvents {
124
141
  * @group Selection
125
142
  */
126
143
  "tree.select": {
127
- /** The node that was selected before, if any. */
144
+ /**
145
+ The node that was selected before, if any.
146
+ */
128
147
  deselectedNode: Node | null;
129
- /** The node that is now selected. */
148
+ /**
149
+ The node that is now selected.
150
+ */
130
151
  node: Node;
131
152
  };
132
153
  /**
133
154
  * Dispatched when the selected node is deselected, which happens when it
134
- * is clicked again or passed to `selectNode` again. No `tree.select` is
135
- * dispatched for it.
155
+ * is clicked again or passed to `selectNode` again with
156
+ * `mustToggle: true`. No `tree.select` is dispatched for it.
136
157
  *
137
158
  * ```js
138
159
  * element.addEventListener("tree.deselect", (e) => {
@@ -142,7 +163,7 @@ export interface TreeEvents {
142
163
  *
143
164
  * Selecting another node does not dispatch it — that is a `tree.select`
144
165
  * with the previous node in `deselectedNode`. It is also not dispatched
145
- * with `mustToggle: false`, or by `selectNode(null)`,
166
+ * by `selectNode` without `mustToggle: true`, `selectNode(null)`,
146
167
  * `removeFromSelection`, `removeNode`, `loadData` and restoring a saved
147
168
  * state.
148
169
  *
@@ -170,7 +191,9 @@ export interface TreeEvents {
170
191
  * @group Drag and drop
171
192
  */
172
193
  "tree.move": {
173
- /** @hidden The fields are in the table above. */
194
+ /**
195
+ @hidden The fields are in the table above.
196
+ */
174
197
  moveInfo: MoveInfo;
175
198
  };
176
199
  /**
@@ -180,7 +203,9 @@ export interface TreeEvents {
180
203
  * @group Loading data
181
204
  */
182
205
  "tree.set_data": {
183
- /** The node whose children were replaced, if it was a subtree. */
206
+ /**
207
+ The node whose children were replaced, if it was a subtree.
208
+ */
184
209
  node?: Node;
185
210
  treeData?: NodeData[];
186
211
  };
@@ -191,7 +216,9 @@ export interface TreeEvents {
191
216
  */
192
217
  "tree.loading_data": {
193
218
  element: HTMLElement;
194
- /** The node whose children are loading, if it is a subtree. */
219
+ /**
220
+ The node whose children are loading, if it is a subtree.
221
+ */
195
222
  node?: Node;
196
223
  };
197
224
  /**
@@ -202,7 +229,9 @@ export interface TreeEvents {
202
229
  */
203
230
  "tree.loaded_data": {
204
231
  element: HTMLElement;
205
- /** The node whose children were loaded, if it was a subtree. */
232
+ /**
233
+ The node whose children were loaded, if it was a subtree.
234
+ */
206
235
  node?: Node;
207
236
  };
208
237
  /**
@@ -222,9 +251,13 @@ export interface TreeEvents {
222
251
  * @group Loading data
223
252
  */
224
253
  "tree.load_failed": {
225
- /** The error thrown by `fetch`, if the request failed with a network error. */
254
+ /**
255
+ The error thrown by `fetch`, if the request failed with a network error.
256
+ */
226
257
  error?: unknown;
227
- /** The response, if the request returned an error status. */
258
+ /**
259
+ The response, if the request returned an error status.
260
+ */
228
261
  response?: Response;
229
262
  };
230
263
  }