openproject-primer_view_components 0.70.4 → 0.71.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/app/assets/javascripts/components/primer/alpha/segmented_control.d.ts +2 -2
  4. data/app/assets/javascripts/components/primer/open_project/filterable_tree_view.d.ts +29 -0
  5. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +11 -1
  6. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +5 -1
  7. data/app/assets/javascripts/components/primer/primer.d.ts +1 -0
  8. data/app/assets/javascripts/primer_view_components.js +1 -1
  9. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  10. data/app/assets/styles/primer_view_components.css +1 -1
  11. data/app/assets/styles/primer_view_components.css.map +1 -1
  12. data/app/components/primer/alpha/action_menu/sub_menu_item.rb +4 -0
  13. data/app/components/primer/alpha/segmented_control.d.ts +2 -2
  14. data/app/components/primer/alpha/segmented_control.js +12 -0
  15. data/app/components/primer/alpha/segmented_control.ts +16 -1
  16. data/app/components/primer/alpha/stack.css +1 -1
  17. data/app/components/primer/alpha/stack.css.json +5 -1
  18. data/app/components/primer/alpha/stack.css.map +1 -1
  19. data/app/components/primer/alpha/stack.pcss +13 -0
  20. data/app/components/primer/alpha/stack.rb +2 -1
  21. data/app/components/primer/open_project/filterable_tree_view/sub_tree.rb +39 -0
  22. data/app/components/primer/open_project/filterable_tree_view.d.ts +29 -0
  23. data/app/components/primer/open_project/filterable_tree_view.html.erb +28 -0
  24. data/app/components/primer/open_project/filterable_tree_view.js +409 -0
  25. data/app/components/primer/open_project/filterable_tree_view.rb +254 -0
  26. data/app/components/primer/open_project/filterable_tree_view.ts +492 -0
  27. data/app/components/primer/open_project/page_header.css +1 -1
  28. data/app/components/primer/open_project/page_header.css.json +1 -1
  29. data/app/components/primer/open_project/page_header.css.map +1 -1
  30. data/app/components/primer/open_project/page_header.pcss +4 -4
  31. data/app/components/primer/open_project/page_header.rb +6 -2
  32. data/app/components/primer/open_project/tree_view/node.rb +19 -3
  33. data/app/components/primer/open_project/tree_view/sub_tree_node.rb +14 -4
  34. data/app/components/primer/open_project/tree_view/tree_view.d.ts +11 -1
  35. data/app/components/primer/open_project/tree_view/tree_view.js +120 -20
  36. data/app/components/primer/open_project/tree_view/tree_view.ts +137 -18
  37. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +5 -1
  38. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +27 -4
  39. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +36 -5
  40. data/app/components/primer/open_project/tree_view.css +1 -1
  41. data/app/components/primer/open_project/tree_view.css.json +9 -0
  42. data/app/components/primer/open_project/tree_view.css.map +1 -1
  43. data/app/components/primer/open_project/tree_view.html.erb +4 -0
  44. data/app/components/primer/open_project/tree_view.pcss +48 -0
  45. data/app/components/primer/open_project/tree_view.rb +6 -1
  46. data/app/components/primer/primer.d.ts +1 -0
  47. data/app/components/primer/primer.js +1 -0
  48. data/app/components/primer/primer.ts +1 -0
  49. data/app/lib/primer/forms/base_component.rb +1 -1
  50. data/app/lib/primer/forms/dsl/text_field_input.rb +2 -0
  51. data/config/locales/en.yml +20 -0
  52. data/lib/primer/view_components/version.rb +2 -2
  53. data/previews/primer/alpha/action_menu_preview/sub_menus.html.erb +1 -0
  54. data/previews/primer/open_project/filterable_tree_view_preview/_custom_select_js.html.erb +62 -0
  55. data/previews/primer/open_project/filterable_tree_view_preview/custom_checkbox_text.html.erb +26 -0
  56. data/previews/primer/open_project/filterable_tree_view_preview/custom_no_results_text.html.erb +28 -0
  57. data/previews/primer/open_project/filterable_tree_view_preview/custom_segmented_control.html.erb +31 -0
  58. data/previews/primer/open_project/filterable_tree_view_preview/default.html.erb +26 -0
  59. data/previews/primer/open_project/filterable_tree_view_preview/form_input.html.erb +32 -0
  60. data/previews/primer/open_project/filterable_tree_view_preview/playground.html.erb +26 -0
  61. data/previews/primer/open_project/filterable_tree_view_preview.rb +125 -0
  62. data/previews/primer/open_project/tree_view_preview/buttons.html.erb +4 -4
  63. data/previews/primer/open_project/tree_view_preview/default.html.erb +4 -4
  64. data/previews/primer/open_project/tree_view_preview/leaf_node_playground.html.erb +1 -1
  65. data/previews/primer/open_project/tree_view_preview/links.html.erb +4 -4
  66. data/previews/primer/open_project/tree_view_preview.rb +18 -8
  67. data/static/arguments.json +89 -3
  68. data/static/audited_at.json +2 -0
  69. data/static/classes.json +3 -0
  70. data/static/constants.json +40 -1
  71. data/static/info_arch.json +220 -3
  72. data/static/previews.json +86 -0
  73. data/static/statuses.json +2 -0
  74. metadata +18 -2
@@ -1,10 +1,15 @@
1
- import { TreeViewSubTreeNodeElement } from './tree_view_sub_tree_node_element';
1
+ import { SelectStrategy, TreeViewSubTreeNodeElement } from './tree_view_sub_tree_node_element';
2
2
  import type { TreeViewNodeType, TreeViewCheckedValue, TreeViewNodeInfo } from '../../shared_events';
3
3
  export declare class TreeViewElement extends HTMLElement {
4
4
  #private;
5
+ formInputContainer: HTMLElement;
6
+ formInputPrototype: HTMLInputElement;
5
7
  connectedCallback(): void;
8
+ rootLeafNodes(): NodeListOf<HTMLElement>;
9
+ rootSubTreeNodes(): NodeListOf<TreeViewSubTreeNodeElement>;
6
10
  disconnectedCallback(): void;
7
11
  handleEvent(event: Event): void;
12
+ getFormInputValueForNode(node: Element): string | null;
8
13
  getNodePath(node: Element): string[];
9
14
  getNodeType(node: Element): TreeViewNodeType | null;
10
15
  markCurrentAtPath(path: string[]): void;
@@ -16,13 +21,18 @@ export declare class TreeViewElement extends HTMLElement {
16
21
  uncheckAtPath(path: string[]): void;
17
22
  toggleCheckedAtPath(path: string[]): void;
18
23
  checkedValueAtPath(path: string[]): TreeViewCheckedValue;
24
+ disabledValueAtPath(path: string[]): boolean;
19
25
  nodeAtPath(path: string[], selector?: string): Element | null;
20
26
  subTreeAtPath(path: string[]): TreeViewSubTreeNodeElement | null;
21
27
  leafAtPath(path: string[]): HTMLLIElement | null;
28
+ setNodeCheckedValue(node: Element, value: TreeViewCheckedValue): void;
22
29
  getNodeCheckedValue(node: Element): TreeViewCheckedValue;
30
+ getNodeDisabledValue(node: Element): boolean;
31
+ setNodeDisabledValue(node: Element, disabled: boolean): void;
23
32
  nodeHasCheckBox(node: Element): boolean;
24
33
  nodeHasNativeAction(node: Element): boolean;
25
34
  expandAncestorsForNode(node: HTMLElement): void;
35
+ changeSelectStrategy(newStrategy: SelectStrategy): void;
26
36
  infoFromNode(node: Element, newCheckedValue?: TreeViewCheckedValue): TreeViewNodeInfo | null;
27
37
  }
28
38
  declare global {
@@ -15,8 +15,8 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
15
15
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
16
16
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
17
17
  };
18
- var _TreeViewElement_instances, _TreeViewElement_abortController, _TreeViewElement_autoExpandFrom, _TreeViewElement_eventIsActivation, _TreeViewElement_nodeForEvent, _TreeViewElement_handleNodeEvent, _TreeViewElement_eventIsCheckboxToggle, _TreeViewElement_handleCheckboxToggle, _TreeViewElement_handleNodeActivated, _TreeViewElement_handleNodeFocused, _TreeViewElement_handleNodeKeyboardEvent, _TreeViewElement_setNodeCheckedValue;
19
- import { controller } from '@github/catalyst';
18
+ var _TreeViewElement_instances, _TreeViewElement_abortController, _TreeViewElement_autoExpandFrom, _TreeViewElement_eventIsActivation, _TreeViewElement_nodeForEvent, _TreeViewElement_handleNodeEvent, _TreeViewElement_eventIsCheckboxToggle, _TreeViewElement_handleCheckboxToggle, _TreeViewElement_handleNodeActivated, _TreeViewElement_handleNodeFocused, _TreeViewElement_handleNodeKeyboardEvent;
19
+ import { controller, target } from '@github/catalyst';
20
20
  import { useRovingTabIndex } from './tree_view_roving_tab_index';
21
21
  let TreeViewElement = class TreeViewElement extends HTMLElement {
22
22
  constructor() {
@@ -43,12 +43,53 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
43
43
  }
44
44
  }
45
45
  }).observe(this, { childList: true, subtree: true });
46
+ const updateInputsObserver = new MutationObserver(mutations => {
47
+ if (!this.formInputContainer)
48
+ return;
49
+ // There is another MutationObserver in TreeViewSubTreeNodeElement that manages checking/unchecking
50
+ // nodes based on the component's select strategy. These two observers can conflict and cause infinite
51
+ // looping, so we make sure something actually changed before computing inputs again.
52
+ const somethingChanged = mutations.some(m => {
53
+ if (!(m.target instanceof HTMLElement))
54
+ return false;
55
+ return m.target.getAttribute('aria-checked') !== m.oldValue;
56
+ });
57
+ if (!somethingChanged)
58
+ return;
59
+ const newInputs = [];
60
+ // eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback
61
+ for (const node of this.querySelectorAll('[role=treeitem][aria-checked=true]')) {
62
+ const newInput = this.formInputPrototype.cloneNode();
63
+ newInput.removeAttribute('data-target');
64
+ newInput.removeAttribute('form');
65
+ const payload = {
66
+ path: this.getNodePath(node),
67
+ };
68
+ const inputValue = this.getFormInputValueForNode(node);
69
+ if (inputValue)
70
+ payload.value = inputValue;
71
+ newInput.value = JSON.stringify(payload);
72
+ newInputs.push(newInput);
73
+ }
74
+ this.formInputContainer.replaceChildren(...newInputs);
75
+ });
76
+ updateInputsObserver.observe(this, {
77
+ childList: true,
78
+ subtree: true,
79
+ attributeFilter: ['aria-checked'],
80
+ });
46
81
  // eslint-disable-next-line github/no-then -- We don't want to wait for this to resolve, just get on with it
47
82
  customElements.whenDefined('tree-view-sub-tree-node').then(() => {
48
83
  // depends on TreeViewSubTreeNodeElement#eachAncestorSubTreeNode, which may not be defined yet
49
84
  __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_autoExpandFrom).call(this, this);
50
85
  });
51
86
  }
87
+ rootLeafNodes() {
88
+ return this.querySelectorAll(':scope > ul > li > .TreeViewItemContainer [role=treeitem]');
89
+ }
90
+ rootSubTreeNodes() {
91
+ return this.querySelectorAll(':scope > ul > tree-view-sub-tree-node');
92
+ }
52
93
  disconnectedCallback() {
53
94
  __classPrivateFieldGet(this, _TreeViewElement_abortController, "f").abort();
54
95
  }
@@ -58,6 +99,9 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
58
99
  __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeEvent).call(this, node, event);
59
100
  }
60
101
  }
102
+ getFormInputValueForNode(node) {
103
+ return node.getAttribute('data-value');
104
+ }
61
105
  getNodePath(node) {
62
106
  const rawPath = node.getAttribute('data-path');
63
107
  if (rawPath) {
@@ -101,13 +145,13 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
101
145
  const node = this.nodeAtPath(path);
102
146
  if (!node)
103
147
  return;
104
- __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'true');
148
+ this.setNodeCheckedValue(node, 'true');
105
149
  }
106
150
  uncheckAtPath(path) {
107
151
  const node = this.nodeAtPath(path);
108
152
  if (!node)
109
153
  return;
110
- __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'false');
154
+ this.setNodeCheckedValue(node, 'false');
111
155
  }
112
156
  toggleCheckedAtPath(path) {
113
157
  const node = this.nodeAtPath(path);
@@ -128,6 +172,12 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
128
172
  return 'false';
129
173
  return this.getNodeCheckedValue(node);
130
174
  }
175
+ disabledValueAtPath(path) {
176
+ const node = this.nodeAtPath(path);
177
+ if (!node)
178
+ return false;
179
+ return this.getNodeDisabledValue(node);
180
+ }
131
181
  nodeAtPath(path, selector) {
132
182
  const pathStr = JSON.stringify(path);
133
183
  return this.querySelector(`${selector || ''}[data-path="${CSS.escape(pathStr)}"]`);
@@ -141,9 +191,23 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
141
191
  leafAtPath(path) {
142
192
  return this.nodeAtPath(path, '[data-node-type=leaf]');
143
193
  }
194
+ setNodeCheckedValue(node, value) {
195
+ node.setAttribute('aria-checked', value.toString());
196
+ }
144
197
  getNodeCheckedValue(node) {
145
198
  return (node.getAttribute('aria-checked') || 'false');
146
199
  }
200
+ getNodeDisabledValue(node) {
201
+ return node.getAttribute('aria-disabled') === 'true';
202
+ }
203
+ setNodeDisabledValue(node, disabled) {
204
+ if (disabled) {
205
+ node.setAttribute('aria-disabled', 'true');
206
+ }
207
+ else {
208
+ node.removeAttribute('aria-disabled');
209
+ }
210
+ }
147
211
  nodeHasCheckBox(node) {
148
212
  return node.querySelector('.TreeViewItemCheckbox') !== null;
149
213
  }
@@ -160,6 +224,11 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
160
224
  }
161
225
  }
162
226
  }
227
+ changeSelectStrategy(newStrategy) {
228
+ for (const subTreeNode of this.querySelectorAll('tree-view-sub-tree-node')) {
229
+ subTreeNode.changeSelectStrategy(newStrategy);
230
+ }
231
+ }
163
232
  // PRIVATE API METHOD
164
233
  //
165
234
  // This would normally be marked private, but it's called by TreeViewSubTreeNodes
@@ -189,22 +258,22 @@ _TreeViewElement_eventIsActivation = function _TreeViewElement_eventIsActivation
189
258
  return event.type === 'click';
190
259
  };
191
260
  _TreeViewElement_nodeForEvent = function _TreeViewElement_nodeForEvent(event) {
192
- const target = event.target;
193
- const node = target.closest('[role=treeitem]');
261
+ const eventTarget = event.target;
262
+ const node = eventTarget.closest('[role=treeitem]');
194
263
  if (!node)
195
264
  return null;
196
- if (target.closest('.TreeViewItemToggle'))
265
+ if (eventTarget.closest('.TreeViewItemToggle'))
197
266
  return null;
198
- if (target.closest('.TreeViewItemLeadingAction'))
267
+ if (eventTarget.closest('.TreeViewItemLeadingAction'))
199
268
  return null;
200
269
  return node;
201
270
  };
202
271
  _TreeViewElement_handleNodeEvent = function _TreeViewElement_handleNodeEvent(node, event) {
203
272
  if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsCheckboxToggle).call(this, event, node)) {
204
- __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleCheckboxToggle).call(this, node);
273
+ __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleCheckboxToggle).call(this, event, node);
205
274
  }
206
275
  else if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsActivation).call(this, event)) {
207
- __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeActivated).call(this, node);
276
+ __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeActivated).call(this, event, node);
208
277
  }
209
278
  else if (event.type === 'focusin') {
210
279
  __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeFocused).call(this, node);
@@ -216,19 +285,43 @@ _TreeViewElement_handleNodeEvent = function _TreeViewElement_handleNodeEvent(nod
216
285
  _TreeViewElement_eventIsCheckboxToggle = function _TreeViewElement_eventIsCheckboxToggle(event, node) {
217
286
  return event.type === 'click' && this.nodeHasCheckBox(node);
218
287
  };
219
- _TreeViewElement_handleCheckboxToggle = function _TreeViewElement_handleCheckboxToggle(node) {
220
- // only handle checking of leaf nodes
288
+ _TreeViewElement_handleCheckboxToggle = function _TreeViewElement_handleCheckboxToggle(event, node) {
289
+ if (this.getNodeDisabledValue(node)) {
290
+ event.preventDefault();
291
+ return;
292
+ }
293
+ // only handle checking of leaf nodes, see TreeViewSubTreeNodeElement for the code that
294
+ // handles checking sub tree items.
221
295
  const type = this.getNodeType(node);
222
296
  if (type !== 'leaf')
223
297
  return;
298
+ const checkValue = this.getNodeCheckedValue(node);
299
+ const newCheckValue = checkValue === 'false' ? 'true' : 'false';
300
+ const nodeInfo = this.infoFromNode(node, newCheckValue);
301
+ const checkSuccess = this.dispatchEvent(new CustomEvent('treeViewBeforeNodeChecked', {
302
+ bubbles: true,
303
+ cancelable: true,
304
+ detail: [nodeInfo],
305
+ }));
306
+ if (!checkSuccess)
307
+ return;
224
308
  if (this.getNodeCheckedValue(node) === 'true') {
225
- __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'false');
309
+ this.setNodeCheckedValue(node, 'false');
226
310
  }
227
311
  else {
228
- __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'true');
312
+ this.setNodeCheckedValue(node, 'true');
229
313
  }
314
+ this.dispatchEvent(new CustomEvent('treeViewNodeChecked', {
315
+ bubbles: true,
316
+ cancelable: true,
317
+ detail: [nodeInfo],
318
+ }));
230
319
  };
231
- _TreeViewElement_handleNodeActivated = function _TreeViewElement_handleNodeActivated(node) {
320
+ _TreeViewElement_handleNodeActivated = function _TreeViewElement_handleNodeActivated(event, node) {
321
+ if (this.getNodeDisabledValue(node)) {
322
+ event.preventDefault();
323
+ return;
324
+ }
232
325
  // do not emit activation events for buttons and anchors, since it is assumed any activation
233
326
  // behavior for these element types is user- or browser-defined
234
327
  if (!(node instanceof HTMLDivElement))
@@ -262,13 +355,17 @@ _TreeViewElement_handleNodeKeyboardEvent = function _TreeViewElement_handleNodeK
262
355
  switch (event.key) {
263
356
  case ' ':
264
357
  case 'Enter':
358
+ if (this.getNodeDisabledValue(node)) {
359
+ event.preventDefault();
360
+ break;
361
+ }
265
362
  if (this.nodeHasCheckBox(node)) {
266
363
  event.preventDefault();
267
364
  if (this.getNodeCheckedValue(node) === 'true') {
268
- __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'false');
365
+ this.setNodeCheckedValue(node, 'false');
269
366
  }
270
367
  else {
271
- __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'true');
368
+ this.setNodeCheckedValue(node, 'true');
272
369
  }
273
370
  }
274
371
  else if (node instanceof HTMLAnchorElement) {
@@ -278,9 +375,12 @@ _TreeViewElement_handleNodeKeyboardEvent = function _TreeViewElement_handleNodeK
278
375
  break;
279
376
  }
280
377
  };
281
- _TreeViewElement_setNodeCheckedValue = function _TreeViewElement_setNodeCheckedValue(node, value) {
282
- node.setAttribute('aria-checked', value.toString());
283
- };
378
+ __decorate([
379
+ target
380
+ ], TreeViewElement.prototype, "formInputContainer", void 0);
381
+ __decorate([
382
+ target
383
+ ], TreeViewElement.prototype, "formInputPrototype", void 0);
284
384
  TreeViewElement = __decorate([
285
385
  controller
286
386
  ], TreeViewElement);
@@ -1,10 +1,13 @@
1
- import {controller} from '@github/catalyst'
2
- import {TreeViewSubTreeNodeElement} from './tree_view_sub_tree_node_element'
1
+ import {controller, target} from '@github/catalyst'
2
+ import {SelectStrategy, TreeViewSubTreeNodeElement} from './tree_view_sub_tree_node_element'
3
3
  import {useRovingTabIndex} from './tree_view_roving_tab_index'
4
4
  import type {TreeViewNodeType, TreeViewCheckedValue, TreeViewNodeInfo} from '../../shared_events'
5
5
 
6
6
  @controller
7
7
  export class TreeViewElement extends HTMLElement {
8
+ @target formInputContainer: HTMLElement
9
+ @target formInputPrototype: HTMLInputElement
10
+
8
11
  #abortController: AbortController
9
12
 
10
13
  connectedCallback() {
@@ -29,6 +32,47 @@ export class TreeViewElement extends HTMLElement {
29
32
  }
30
33
  }).observe(this, {childList: true, subtree: true})
31
34
 
35
+ const updateInputsObserver = new MutationObserver(mutations => {
36
+ if (!this.formInputContainer) return
37
+
38
+ // There is another MutationObserver in TreeViewSubTreeNodeElement that manages checking/unchecking
39
+ // nodes based on the component's select strategy. These two observers can conflict and cause infinite
40
+ // looping, so we make sure something actually changed before computing inputs again.
41
+ const somethingChanged = mutations.some(m => {
42
+ if (!(m.target instanceof HTMLElement)) return false
43
+ return m.target.getAttribute('aria-checked') !== m.oldValue
44
+ })
45
+
46
+ if (!somethingChanged) return
47
+
48
+ const newInputs = []
49
+
50
+ // eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback
51
+ for (const node of this.querySelectorAll('[role=treeitem][aria-checked=true]')) {
52
+ const newInput = this.formInputPrototype.cloneNode() as HTMLInputElement
53
+ newInput.removeAttribute('data-target')
54
+ newInput.removeAttribute('form')
55
+
56
+ const payload: {path: string[]; value?: string} = {
57
+ path: this.getNodePath(node),
58
+ }
59
+
60
+ const inputValue = this.getFormInputValueForNode(node)
61
+ if (inputValue) payload.value = inputValue
62
+
63
+ newInput.value = JSON.stringify(payload)
64
+ newInputs.push(newInput)
65
+ }
66
+
67
+ this.formInputContainer.replaceChildren(...newInputs)
68
+ })
69
+
70
+ updateInputsObserver.observe(this, {
71
+ childList: true,
72
+ subtree: true,
73
+ attributeFilter: ['aria-checked'],
74
+ })
75
+
32
76
  // eslint-disable-next-line github/no-then -- We don't want to wait for this to resolve, just get on with it
33
77
  customElements.whenDefined('tree-view-sub-tree-node').then(() => {
34
78
  // depends on TreeViewSubTreeNodeElement#eachAncestorSubTreeNode, which may not be defined yet
@@ -36,6 +80,14 @@ export class TreeViewElement extends HTMLElement {
36
80
  })
37
81
  }
38
82
 
83
+ rootLeafNodes(): NodeListOf<HTMLElement> {
84
+ return this.querySelectorAll(':scope > ul > li > .TreeViewItemContainer [role=treeitem]')
85
+ }
86
+
87
+ rootSubTreeNodes(): NodeListOf<TreeViewSubTreeNodeElement> {
88
+ return this.querySelectorAll(':scope > ul > tree-view-sub-tree-node')
89
+ }
90
+
39
91
  #autoExpandFrom(root: HTMLElement) {
40
92
  for (const element of root.querySelectorAll('[aria-expanded=true]')) {
41
93
  this.expandAncestorsForNode(element as HTMLElement)
@@ -59,21 +111,21 @@ export class TreeViewElement extends HTMLElement {
59
111
  }
60
112
 
61
113
  #nodeForEvent(event: Event): Element | null {
62
- const target = event.target as Element
63
- const node = target.closest('[role=treeitem]')
114
+ const eventTarget = event.target as Element
115
+ const node = eventTarget.closest('[role=treeitem]')
64
116
  if (!node) return null
65
117
 
66
- if (target.closest('.TreeViewItemToggle')) return null
67
- if (target.closest('.TreeViewItemLeadingAction')) return null
118
+ if (eventTarget.closest('.TreeViewItemToggle')) return null
119
+ if (eventTarget.closest('.TreeViewItemLeadingAction')) return null
68
120
 
69
121
  return node
70
122
  }
71
123
 
72
124
  #handleNodeEvent(node: Element, event: Event) {
73
125
  if (this.#eventIsCheckboxToggle(event, node)) {
74
- this.#handleCheckboxToggle(node)
126
+ this.#handleCheckboxToggle(event, node)
75
127
  } else if (this.#eventIsActivation(event)) {
76
- this.#handleNodeActivated(node)
128
+ this.#handleNodeActivated(event, node)
77
129
  } else if (event.type === 'focusin') {
78
130
  this.#handleNodeFocused(node)
79
131
  } else if (event instanceof KeyboardEvent) {
@@ -85,19 +137,52 @@ export class TreeViewElement extends HTMLElement {
85
137
  return event.type === 'click' && this.nodeHasCheckBox(node)
86
138
  }
87
139
 
88
- #handleCheckboxToggle(node: Element) {
89
- // only handle checking of leaf nodes
140
+ #handleCheckboxToggle(event: Event, node: Element) {
141
+ if (this.getNodeDisabledValue(node)) {
142
+ event.preventDefault()
143
+ return
144
+ }
145
+
146
+ // only handle checking of leaf nodes, see TreeViewSubTreeNodeElement for the code that
147
+ // handles checking sub tree items.
90
148
  const type = this.getNodeType(node)
91
149
  if (type !== 'leaf') return
92
150
 
151
+ const checkValue = this.getNodeCheckedValue(node)
152
+ const newCheckValue = checkValue === 'false' ? 'true' : 'false'
153
+ const nodeInfo = this.infoFromNode(node, newCheckValue)
154
+
155
+ const checkSuccess = this.dispatchEvent(
156
+ new CustomEvent('treeViewBeforeNodeChecked', {
157
+ bubbles: true,
158
+ cancelable: true,
159
+ detail: [nodeInfo],
160
+ }),
161
+ )
162
+
163
+ if (!checkSuccess) return
164
+
93
165
  if (this.getNodeCheckedValue(node) === 'true') {
94
- this.#setNodeCheckedValue(node, 'false')
166
+ this.setNodeCheckedValue(node, 'false')
95
167
  } else {
96
- this.#setNodeCheckedValue(node, 'true')
168
+ this.setNodeCheckedValue(node, 'true')
97
169
  }
170
+
171
+ this.dispatchEvent(
172
+ new CustomEvent('treeViewNodeChecked', {
173
+ bubbles: true,
174
+ cancelable: true,
175
+ detail: [nodeInfo],
176
+ }),
177
+ )
98
178
  }
99
179
 
100
- #handleNodeActivated(node: Element) {
180
+ #handleNodeActivated(event: Event, node: Element) {
181
+ if (this.getNodeDisabledValue(node)) {
182
+ event.preventDefault()
183
+ return
184
+ }
185
+
101
186
  // do not emit activation events for buttons and anchors, since it is assumed any activation
102
187
  // behavior for these element types is user- or browser-defined
103
188
  if (!(node instanceof HTMLDivElement)) return
@@ -141,13 +226,18 @@ export class TreeViewElement extends HTMLElement {
141
226
  switch (event.key) {
142
227
  case ' ':
143
228
  case 'Enter':
229
+ if (this.getNodeDisabledValue(node)) {
230
+ event.preventDefault()
231
+ break
232
+ }
233
+
144
234
  if (this.nodeHasCheckBox(node)) {
145
235
  event.preventDefault()
146
236
 
147
237
  if (this.getNodeCheckedValue(node) === 'true') {
148
- this.#setNodeCheckedValue(node, 'false')
238
+ this.setNodeCheckedValue(node, 'false')
149
239
  } else {
150
- this.#setNodeCheckedValue(node, 'true')
240
+ this.setNodeCheckedValue(node, 'true')
151
241
  }
152
242
  } else if (node instanceof HTMLAnchorElement) {
153
243
  // simulate click on space
@@ -158,6 +248,10 @@ export class TreeViewElement extends HTMLElement {
158
248
  }
159
249
  }
160
250
 
251
+ getFormInputValueForNode(node: Element): string | null {
252
+ return node.getAttribute('data-value')
253
+ }
254
+
161
255
  getNodePath(node: Element): string[] {
162
256
  const rawPath = node.getAttribute('data-path')
163
257
 
@@ -210,14 +304,14 @@ export class TreeViewElement extends HTMLElement {
210
304
  const node = this.nodeAtPath(path)
211
305
  if (!node) return
212
306
 
213
- this.#setNodeCheckedValue(node, 'true')
307
+ this.setNodeCheckedValue(node, 'true')
214
308
  }
215
309
 
216
310
  uncheckAtPath(path: string[]) {
217
311
  const node = this.nodeAtPath(path)
218
312
  if (!node) return
219
313
 
220
- this.#setNodeCheckedValue(node, 'false')
314
+ this.setNodeCheckedValue(node, 'false')
221
315
  }
222
316
 
223
317
  toggleCheckedAtPath(path: string[]) {
@@ -240,6 +334,13 @@ export class TreeViewElement extends HTMLElement {
240
334
  return this.getNodeCheckedValue(node)
241
335
  }
242
336
 
337
+ disabledValueAtPath(path: string[]): boolean {
338
+ const node = this.nodeAtPath(path)
339
+ if (!node) return false
340
+
341
+ return this.getNodeDisabledValue(node)
342
+ }
343
+
243
344
  nodeAtPath(path: string[], selector?: string): Element | null {
244
345
  const pathStr = JSON.stringify(path)
245
346
  return this.querySelector(`${selector || ''}[data-path="${CSS.escape(pathStr)}"]`)
@@ -256,7 +357,7 @@ export class TreeViewElement extends HTMLElement {
256
357
  return this.nodeAtPath(path, '[data-node-type=leaf]') as HTMLLIElement | null
257
358
  }
258
359
 
259
- #setNodeCheckedValue(node: Element, value: TreeViewCheckedValue) {
360
+ setNodeCheckedValue(node: Element, value: TreeViewCheckedValue) {
260
361
  node.setAttribute('aria-checked', value.toString())
261
362
  }
262
363
 
@@ -264,6 +365,18 @@ export class TreeViewElement extends HTMLElement {
264
365
  return (node.getAttribute('aria-checked') || 'false') as TreeViewCheckedValue
265
366
  }
266
367
 
368
+ getNodeDisabledValue(node: Element): boolean {
369
+ return node.getAttribute('aria-disabled') === 'true'
370
+ }
371
+
372
+ setNodeDisabledValue(node: Element, disabled: boolean) {
373
+ if (disabled) {
374
+ node.setAttribute('aria-disabled', 'true')
375
+ } else {
376
+ node.removeAttribute('aria-disabled')
377
+ }
378
+ }
379
+
267
380
  nodeHasCheckBox(node: Element): boolean {
268
381
  return node.querySelector('.TreeViewItemCheckbox') !== null
269
382
  }
@@ -283,6 +396,12 @@ export class TreeViewElement extends HTMLElement {
283
396
  }
284
397
  }
285
398
 
399
+ changeSelectStrategy(newStrategy: SelectStrategy) {
400
+ for (const subTreeNode of this.querySelectorAll<TreeViewSubTreeNodeElement>('tree-view-sub-tree-node')) {
401
+ subTreeNode.changeSelectStrategy(newStrategy)
402
+ }
403
+ }
404
+
286
405
  // PRIVATE API METHOD
287
406
  //
288
407
  // This would normally be marked private, but it's called by TreeViewSubTreeNodes
@@ -2,6 +2,7 @@ import { TreeViewIconPairElement } from './tree_view_icon_pair_element';
2
2
  import { TreeViewIncludeFragmentElement } from './tree_view_include_fragment_element';
3
3
  import { TreeViewElement } from './tree_view';
4
4
  type LoadingState = 'loading' | 'error' | 'success';
5
+ export type SelectStrategy = 'self' | 'descendants' | 'mixed_descendants';
5
6
  export declare class TreeViewSubTreeNodeElement extends HTMLElement {
6
7
  #private;
7
8
  node: HTMLElement;
@@ -19,7 +20,8 @@ export declare class TreeViewSubTreeNodeElement extends HTMLElement {
19
20
  set expanded(newValue: boolean);
20
21
  get loadingState(): LoadingState;
21
22
  set loadingState(newState: LoadingState);
22
- get selectStrategy(): string;
23
+ get selectStrategy(): SelectStrategy;
24
+ get level(): number;
23
25
  disconnectedCallback(): void;
24
26
  handleEvent(event: Event): void;
25
27
  expand(): void;
@@ -27,11 +29,13 @@ export declare class TreeViewSubTreeNodeElement extends HTMLElement {
27
29
  toggle(): void;
28
30
  get nodes(): NodeListOf<Element>;
29
31
  eachDirectDescendantNode(): Generator<Element>;
32
+ eachDirectDescendantSubTreeNode(): Generator<TreeViewSubTreeNodeElement>;
30
33
  eachDescendantNode(): Generator<Element>;
31
34
  eachAncestorSubTreeNode(): Generator<TreeViewSubTreeNodeElement>;
32
35
  get isEmpty(): boolean;
33
36
  get treeView(): TreeViewElement | null;
34
37
  toggleChecked(): void;
38
+ changeSelectStrategy(newStrategy: SelectStrategy): void;
35
39
  }
36
40
  declare global {
37
41
  interface Window {
@@ -48,7 +48,7 @@ let TreeViewSubTreeNodeElement = class TreeViewSubTreeNodeElement extends HTMLEl
48
48
  }, { signal });
49
49
  });
50
50
  const checkedMutationObserver = new MutationObserver(() => {
51
- if (this.selectStrategy !== 'descendants')
51
+ if (this.selectStrategy !== 'mixed_descendants')
52
52
  return;
53
53
  let checkType = 'unknown';
54
54
  for (const node of this.eachDirectDescendantNode()) {
@@ -95,7 +95,10 @@ let TreeViewSubTreeNodeElement = class TreeViewSubTreeNodeElement extends HTMLEl
95
95
  __classPrivateFieldGet(this, _TreeViewSubTreeNodeElement_instances, "m", _TreeViewSubTreeNodeElement_update).call(this);
96
96
  }
97
97
  get selectStrategy() {
98
- return this.node.getAttribute('data-select-strategy') || 'descendants';
98
+ return (this.node.getAttribute('data-select-strategy') || 'descendants');
99
+ }
100
+ get level() {
101
+ return parseInt(this.node.getAttribute('aria-level') || '0');
99
102
  }
100
103
  disconnectedCallback() {
101
104
  __classPrivateFieldGet(this, _TreeViewSubTreeNodeElement_abortController, "f").abort();
@@ -160,6 +163,11 @@ let TreeViewSubTreeNodeElement = class TreeViewSubTreeNodeElement extends HTMLEl
160
163
  yield subTree;
161
164
  }
162
165
  }
166
+ *eachDirectDescendantSubTreeNode() {
167
+ for (const subTree of this.subTree.querySelectorAll(':scope > tree-view-sub-tree-node')) {
168
+ yield subTree;
169
+ }
170
+ }
163
171
  *eachDescendantNode() {
164
172
  for (const node of this.subTree.querySelectorAll('[role=treeitem]')) {
165
173
  yield node;
@@ -182,13 +190,13 @@ let TreeViewSubTreeNodeElement = class TreeViewSubTreeNodeElement extends HTMLEl
182
190
  return this.closest('tree-view');
183
191
  }
184
192
  toggleChecked() {
185
- const checkValue = this.node.getAttribute('aria-checked') || 'false';
193
+ const checkValue = this.treeView?.getNodeCheckedValue(this.node) || 'false';
186
194
  const newCheckValue = checkValue === 'false' ? 'true' : 'false';
187
195
  const nodeInfos = [];
188
196
  const rootInfo = this.treeView?.infoFromNode(this.node, newCheckValue);
189
197
  if (rootInfo)
190
198
  nodeInfos.push(rootInfo);
191
- if (this.selectStrategy === 'descendants') {
199
+ if (this.selectStrategy === 'descendants' || this.selectStrategy === 'mixed_descendants') {
192
200
  for (const node of this.eachDescendantNode()) {
193
201
  const info = this.treeView?.infoFromNode(node, newCheckValue);
194
202
  if (info)
@@ -211,6 +219,9 @@ let TreeViewSubTreeNodeElement = class TreeViewSubTreeNodeElement extends HTMLEl
211
219
  detail: nodeInfos,
212
220
  }));
213
221
  }
222
+ changeSelectStrategy(newStrategy) {
223
+ this.node.setAttribute('data-select-strategy', newStrategy);
224
+ }
214
225
  };
215
226
  _TreeViewSubTreeNodeElement_expanded = new WeakMap();
216
227
  _TreeViewSubTreeNodeElement_loadingState = new WeakMap();
@@ -269,6 +280,10 @@ _TreeViewSubTreeNodeElement_handleKeyboardEvent = function _TreeViewSubTreeNodeE
269
280
  }
270
281
  switch (event.key) {
271
282
  case 'Enter':
283
+ if (this.treeView?.getNodeDisabledValue(node)) {
284
+ event.preventDefault();
285
+ break;
286
+ }
272
287
  // eslint-disable-next-line no-restricted-syntax
273
288
  event.stopPropagation();
274
289
  if (__classPrivateFieldGet(this, _TreeViewSubTreeNodeElement_instances, "a", _TreeViewSubTreeNodeElement_checkboxElement_get)) {
@@ -290,6 +305,10 @@ _TreeViewSubTreeNodeElement_handleKeyboardEvent = function _TreeViewSubTreeNodeE
290
305
  this.collapse();
291
306
  break;
292
307
  case ' ':
308
+ if (this.treeView?.getNodeDisabledValue(node)) {
309
+ event.preventDefault();
310
+ break;
311
+ }
293
312
  if (__classPrivateFieldGet(this, _TreeViewSubTreeNodeElement_instances, "a", _TreeViewSubTreeNodeElement_checkboxElement_get)) {
294
313
  // eslint-disable-next-line no-restricted-syntax
295
314
  event.stopPropagation();
@@ -309,6 +328,10 @@ _TreeViewSubTreeNodeElement_handleKeyboardEvent = function _TreeViewSubTreeNodeE
309
328
  }
310
329
  };
311
330
  _TreeViewSubTreeNodeElement_handleCheckboxEvent = function _TreeViewSubTreeNodeElement_handleCheckboxEvent(event) {
331
+ if (this.treeView?.getNodeDisabledValue(this.node)) {
332
+ event.preventDefault();
333
+ return;
334
+ }
312
335
  if (event.type !== 'click')
313
336
  return;
314
337
  this.toggleChecked();