openproject-primer_view_components 0.64.0 → 0.65.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/app/assets/javascripts/components/primer/open_project/collapsible.d.ts +2 -0
  4. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +29 -0
  5. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_icon_pair_element.d.ts +15 -0
  6. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_include_fragment_element.d.ts +9 -0
  7. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_roving_tab_index.d.ts +3 -0
  8. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +38 -0
  9. data/app/assets/javascripts/components/primer/primer.d.ts +4 -0
  10. data/app/assets/javascripts/components/primer/shared_events.d.ts +15 -0
  11. data/app/assets/javascripts/primer_view_components.js +1 -1
  12. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  13. data/app/assets/styles/primer_view_components.css +1 -1
  14. data/app/assets/styles/primer_view_components.css.map +1 -1
  15. data/app/components/primer/alpha/select_panel.css +1 -1
  16. data/app/components/primer/alpha/select_panel.css.json +2 -2
  17. data/app/components/primer/alpha/select_panel.css.map +1 -1
  18. data/app/components/primer/alpha/select_panel.html.erb +1 -1
  19. data/app/components/primer/alpha/select_panel.pcss +5 -2
  20. data/app/components/primer/beta/spinner.html.erb +1 -1
  21. data/app/components/primer/beta/spinner.rb +2 -0
  22. data/app/components/primer/open_project/border_box/collapsible_header.css +1 -1
  23. data/app/components/primer/open_project/border_box/collapsible_header.css.json +2 -1
  24. data/app/components/primer/open_project/border_box/collapsible_header.css.map +1 -1
  25. data/app/components/primer/open_project/border_box/collapsible_header.html.erb +12 -1
  26. data/app/components/primer/open_project/border_box/collapsible_header.pcss +4 -0
  27. data/app/components/primer/open_project/border_box/collapsible_header.rb +16 -12
  28. data/app/components/primer/open_project/collapsible.d.ts +2 -0
  29. data/app/components/primer/open_project/collapsible.js +11 -0
  30. data/app/components/primer/open_project/collapsible.ts +10 -0
  31. data/app/components/primer/open_project/collapsible_section.html.erb +14 -2
  32. data/app/components/primer/open_project/collapsible_section.rb +5 -1
  33. data/app/components/primer/open_project/danger_dialog.html.erb +4 -0
  34. data/app/components/primer/open_project/feedback_dialog.html.erb +5 -0
  35. data/app/components/primer/open_project/file_tree_view/directory_node.html.erb +5 -0
  36. data/app/components/primer/open_project/file_tree_view/directory_node.rb +24 -0
  37. data/app/components/primer/open_project/file_tree_view/file_node.html.erb +2 -0
  38. data/app/components/primer/open_project/file_tree_view/file_node.rb +14 -0
  39. data/app/components/primer/open_project/file_tree_view.rb +15 -0
  40. data/app/components/primer/open_project/skeleton_box.css +1 -0
  41. data/app/components/primer/open_project/skeleton_box.css.json +6 -0
  42. data/app/components/primer/open_project/skeleton_box.css.map +1 -0
  43. data/app/components/primer/open_project/skeleton_box.html.erb +1 -0
  44. data/app/components/primer/open_project/skeleton_box.pcss +30 -0
  45. data/app/components/primer/open_project/skeleton_box.rb +27 -0
  46. data/app/components/primer/open_project/tree_view/icon.html.erb +1 -0
  47. data/app/components/primer/open_project/tree_view/icon.rb +22 -0
  48. data/app/components/primer/open_project/tree_view/icon_pair.html.erb +13 -0
  49. data/app/components/primer/open_project/tree_view/icon_pair.rb +42 -0
  50. data/app/components/primer/open_project/tree_view/leading_action.html.erb +3 -0
  51. data/app/components/primer/open_project/tree_view/leading_action.rb +18 -0
  52. data/app/components/primer/open_project/tree_view/leaf_node.html.erb +18 -0
  53. data/app/components/primer/open_project/tree_view/leaf_node.rb +96 -0
  54. data/app/components/primer/open_project/tree_view/loading_failure_message.html.erb +13 -0
  55. data/app/components/primer/open_project/tree_view/loading_failure_message.rb +31 -0
  56. data/app/components/primer/open_project/tree_view/node.html.erb +32 -0
  57. data/app/components/primer/open_project/tree_view/node.rb +155 -0
  58. data/app/components/primer/open_project/tree_view/skeleton_loader.html.erb +23 -0
  59. data/app/components/primer/open_project/tree_view/skeleton_loader.rb +36 -0
  60. data/app/components/primer/open_project/tree_view/spinner_loader.html.erb +20 -0
  61. data/app/components/primer/open_project/tree_view/spinner_loader.rb +33 -0
  62. data/app/components/primer/open_project/tree_view/sub_tree.html.erb +21 -0
  63. data/app/components/primer/open_project/tree_view/sub_tree.rb +106 -0
  64. data/app/components/primer/open_project/tree_view/sub_tree_container.html.erb +3 -0
  65. data/app/components/primer/open_project/tree_view/sub_tree_container.rb +39 -0
  66. data/app/components/primer/open_project/tree_view/sub_tree_node.html.erb +49 -0
  67. data/app/components/primer/open_project/tree_view/sub_tree_node.rb +172 -0
  68. data/app/components/primer/open_project/tree_view/tree_view.d.ts +29 -0
  69. data/app/components/primer/open_project/tree_view/tree_view.js +238 -0
  70. data/app/components/primer/open_project/tree_view/tree_view.ts +257 -0
  71. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.d.ts +15 -0
  72. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.js +62 -0
  73. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.ts +56 -0
  74. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.d.ts +9 -0
  75. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.js +29 -0
  76. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.ts +29 -0
  77. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.d.ts +3 -0
  78. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.js +126 -0
  79. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.ts +156 -0
  80. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +38 -0
  81. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +362 -0
  82. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +402 -0
  83. data/app/components/primer/open_project/tree_view/visual.html.erb +14 -0
  84. data/app/components/primer/open_project/tree_view/visual.rb +27 -0
  85. data/app/components/primer/open_project/tree_view.css +1 -0
  86. data/app/components/primer/open_project/tree_view.css.json +42 -0
  87. data/app/components/primer/open_project/tree_view.css.map +1 -0
  88. data/app/components/primer/open_project/tree_view.html.erb +7 -0
  89. data/app/components/primer/open_project/tree_view.pcss +319 -0
  90. data/app/components/primer/open_project/tree_view.rb +367 -0
  91. data/app/components/primer/primer.d.ts +4 -0
  92. data/app/components/primer/primer.js +4 -0
  93. data/app/components/primer/primer.pcss +2 -0
  94. data/app/components/primer/primer.ts +4 -0
  95. data/app/components/primer/shared_events.d.ts +15 -0
  96. data/app/components/primer/shared_events.ts +19 -0
  97. data/app/lib/primer/forms/acts_as_component.rb +1 -12
  98. data/lib/primer/view_components/version.rb +1 -1
  99. data/previews/primer/open_project/file_tree_view_preview/default.html.erb +16 -0
  100. data/previews/primer/open_project/file_tree_view_preview/playground.html.erb +4 -0
  101. data/previews/primer/open_project/file_tree_view_preview.rb +69 -0
  102. data/previews/primer/open_project/skeleton_box_preview.rb +20 -0
  103. data/previews/primer/open_project/tree_view_preview/default.html.erb +24 -0
  104. data/previews/primer/open_project/tree_view_preview/empty.html.erb +10 -0
  105. data/previews/primer/open_project/tree_view_preview/leaf_node_playground.html.erb +15 -0
  106. data/previews/primer/open_project/tree_view_preview/loading_failure.html.erb +36 -0
  107. data/previews/primer/open_project/tree_view_preview/loading_skeleton.html.erb +12 -0
  108. data/previews/primer/open_project/tree_view_preview/loading_spinner.html.erb +12 -0
  109. data/previews/primer/open_project/tree_view_preview/playground.html.erb +4 -0
  110. data/previews/primer/open_project/tree_view_preview.rb +139 -0
  111. data/static/arguments.json +400 -0
  112. data/static/audited_at.json +17 -0
  113. data/static/classes.json +18 -0
  114. data/static/constants.json +83 -0
  115. data/static/info_arch.json +1379 -0
  116. data/static/previews.json +167 -0
  117. data/static/statuses.json +17 -0
  118. metadata +75 -2
@@ -0,0 +1,15 @@
1
+ export declare class TreeViewIconPairElement extends HTMLElement {
2
+ #private;
3
+ expandedIcon: HTMLElement;
4
+ collapsedIcon: HTMLElement;
5
+ expanded: boolean;
6
+ connectedCallback(): void;
7
+ showExpanded(): void;
8
+ showCollapsed(): void;
9
+ toggle(): void;
10
+ }
11
+ declare global {
12
+ interface Window {
13
+ TreeViewIconPairElement: typeof TreeViewIconPairElement;
14
+ }
15
+ }
@@ -0,0 +1,62 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ 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");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _TreeViewIconPairElement_instances, _TreeViewIconPairElement_update;
13
+ import { controller, target } from '@github/catalyst';
14
+ import { observeMutationsUntilConditionMet } from '../../utils';
15
+ let TreeViewIconPairElement = class TreeViewIconPairElement extends HTMLElement {
16
+ constructor() {
17
+ super(...arguments);
18
+ _TreeViewIconPairElement_instances.add(this);
19
+ }
20
+ connectedCallback() {
21
+ observeMutationsUntilConditionMet(this, () => Boolean(this.collapsedIcon) && Boolean(this.expandedIcon), () => {
22
+ this.expanded = this.collapsedIcon.hidden;
23
+ });
24
+ }
25
+ showExpanded() {
26
+ this.expanded = true;
27
+ __classPrivateFieldGet(this, _TreeViewIconPairElement_instances, "m", _TreeViewIconPairElement_update).call(this);
28
+ }
29
+ showCollapsed() {
30
+ this.expanded = false;
31
+ __classPrivateFieldGet(this, _TreeViewIconPairElement_instances, "m", _TreeViewIconPairElement_update).call(this);
32
+ }
33
+ toggle() {
34
+ this.expanded = !this.expanded;
35
+ __classPrivateFieldGet(this, _TreeViewIconPairElement_instances, "m", _TreeViewIconPairElement_update).call(this);
36
+ }
37
+ };
38
+ _TreeViewIconPairElement_instances = new WeakSet();
39
+ _TreeViewIconPairElement_update = function _TreeViewIconPairElement_update() {
40
+ if (this.expanded) {
41
+ this.expandedIcon.hidden = false;
42
+ this.collapsedIcon.hidden = true;
43
+ }
44
+ else {
45
+ this.expandedIcon.hidden = true;
46
+ this.collapsedIcon.hidden = false;
47
+ }
48
+ };
49
+ __decorate([
50
+ target
51
+ ], TreeViewIconPairElement.prototype, "expandedIcon", void 0);
52
+ __decorate([
53
+ target
54
+ ], TreeViewIconPairElement.prototype, "collapsedIcon", void 0);
55
+ TreeViewIconPairElement = __decorate([
56
+ controller
57
+ ], TreeViewIconPairElement);
58
+ export { TreeViewIconPairElement };
59
+ if (!window.customElements.get('tree-view-icon-pair')) {
60
+ window.TreeViewIconPairElement = TreeViewIconPairElement;
61
+ window.customElements.define('tree-view-icon-pair', TreeViewIconPairElement);
62
+ }
@@ -0,0 +1,56 @@
1
+ import {controller, target} from '@github/catalyst'
2
+ import {observeMutationsUntilConditionMet} from '../../utils'
3
+
4
+ @controller
5
+ export class TreeViewIconPairElement extends HTMLElement {
6
+ @target expandedIcon: HTMLElement
7
+ @target collapsedIcon: HTMLElement
8
+
9
+ expanded: boolean
10
+
11
+ connectedCallback() {
12
+ observeMutationsUntilConditionMet(
13
+ this,
14
+ () => Boolean(this.collapsedIcon) && Boolean(this.expandedIcon),
15
+ () => {
16
+ this.expanded = this.collapsedIcon.hidden
17
+ },
18
+ )
19
+ }
20
+
21
+ showExpanded() {
22
+ this.expanded = true
23
+ this.#update()
24
+ }
25
+
26
+ showCollapsed() {
27
+ this.expanded = false
28
+ this.#update()
29
+ }
30
+
31
+ toggle() {
32
+ this.expanded = !this.expanded
33
+ this.#update()
34
+ }
35
+
36
+ #update() {
37
+ if (this.expanded) {
38
+ this.expandedIcon.hidden = false
39
+ this.collapsedIcon.hidden = true
40
+ } else {
41
+ this.expandedIcon.hidden = true
42
+ this.collapsedIcon.hidden = false
43
+ }
44
+ }
45
+ }
46
+
47
+ if (!window.customElements.get('tree-view-icon-pair')) {
48
+ window.TreeViewIconPairElement = TreeViewIconPairElement
49
+ window.customElements.define('tree-view-icon-pair', TreeViewIconPairElement)
50
+ }
51
+
52
+ declare global {
53
+ interface Window {
54
+ TreeViewIconPairElement: typeof TreeViewIconPairElement
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ import { IncludeFragmentElement } from '@github/include-fragment-element';
2
+ export declare class TreeViewIncludeFragmentElement extends IncludeFragmentElement {
3
+ request(): Request;
4
+ }
5
+ declare global {
6
+ interface Window {
7
+ TreeViewIncludeFragmentElement: typeof TreeViewIncludeFragmentElement;
8
+ }
9
+ }
@@ -0,0 +1,29 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { controller } from '@github/catalyst';
8
+ import { IncludeFragmentElement } from '@github/include-fragment-element';
9
+ let TreeViewIncludeFragmentElement = class TreeViewIncludeFragmentElement extends IncludeFragmentElement {
10
+ request() {
11
+ const originalRequest = super.request();
12
+ const url = new URL(originalRequest.url);
13
+ url.searchParams.set('path', this.getAttribute('data-path') || '');
14
+ return new Request(url, {
15
+ method: originalRequest.method,
16
+ headers: originalRequest.headers,
17
+ credentials: originalRequest.credentials,
18
+ });
19
+ }
20
+ };
21
+ TreeViewIncludeFragmentElement = __decorate([
22
+ controller
23
+ ], TreeViewIncludeFragmentElement);
24
+ export { TreeViewIncludeFragmentElement };
25
+ if (!window.customElements.get('tree-view-include-fragment')) {
26
+ window.TreeViewIncludeFragmentElement = TreeViewIncludeFragmentElement;
27
+ // eslint-disable-next-line custom-elements/extends-correct-class
28
+ window.customElements.define('tree-view-include-fragment', TreeViewIncludeFragmentElement);
29
+ }
@@ -0,0 +1,29 @@
1
+ import {controller} from '@github/catalyst'
2
+ import {IncludeFragmentElement} from '@github/include-fragment-element'
3
+
4
+ @controller
5
+ export class TreeViewIncludeFragmentElement extends IncludeFragmentElement {
6
+ request(): Request {
7
+ const originalRequest = super.request()
8
+ const url = new URL(originalRequest.url)
9
+ url.searchParams.set('path', this.getAttribute('data-path') || '')
10
+
11
+ return new Request(url, {
12
+ method: originalRequest.method,
13
+ headers: originalRequest.headers,
14
+ credentials: originalRequest.credentials,
15
+ })
16
+ }
17
+ }
18
+
19
+ if (!window.customElements.get('tree-view-include-fragment')) {
20
+ window.TreeViewIncludeFragmentElement = TreeViewIncludeFragmentElement
21
+ // eslint-disable-next-line custom-elements/extends-correct-class
22
+ window.customElements.define('tree-view-include-fragment', TreeViewIncludeFragmentElement)
23
+ }
24
+
25
+ declare global {
26
+ interface Window {
27
+ TreeViewIncludeFragmentElement: typeof TreeViewIncludeFragmentElement
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ import { TreeViewElement } from './tree_view';
2
+ export declare function useRovingTabIndex(containerEl: TreeViewElement): void;
3
+ export declare function getElementState(element: HTMLElement): 'open' | 'closed' | 'end';
@@ -0,0 +1,126 @@
1
+ import { FocusKeys, focusZone } from '@primer/behaviors';
2
+ // This code was adapted from the roving tab index implementation in primer/react, see:
3
+ // https://github.com/primer/react/blob/f9785343716435f43e3d82482b057a17bd345c25/packages/react/src/TreeView/useRovingTabIndex.ts
4
+ export function useRovingTabIndex(containerEl) {
5
+ // TODO: Initialize focus to the aria-current item if it exists
6
+ focusZone(containerEl, {
7
+ bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd | FocusKeys.Backspace,
8
+ getNextFocusable: (_direction, from, event) => {
9
+ if (!(from instanceof HTMLElement))
10
+ return;
11
+ // Skip elements within a modal dialog
12
+ // This need to be in a try/catch to avoid errors in
13
+ // non-supported browsers
14
+ try {
15
+ if (from.closest('dialog:modal')) {
16
+ return;
17
+ }
18
+ }
19
+ catch {
20
+ // Don't return
21
+ }
22
+ return getNextFocusableElement(from, event) ?? from;
23
+ },
24
+ focusInStrategy: () => {
25
+ // Don't try to execute the focusInStrategy if focus is coming from a click.
26
+ // The clicked row will receive focus correctly by default.
27
+ // If a chevron is clicked, setting the focus through the focuszone will prevent its toggle.
28
+ // if (mouseDownRef.current) {
29
+ // return undefined
30
+ // }
31
+ let currentItem = containerEl.querySelector('[aria-current]');
32
+ currentItem = currentItem?.checkVisibility() ? currentItem : null;
33
+ const firstItem = containerEl.querySelector('[role="treeitem"]');
34
+ // Focus the aria-current item if it exists
35
+ if (currentItem instanceof HTMLElement) {
36
+ return currentItem;
37
+ }
38
+ // Otherwise, focus the activeElement if it's a treeitem
39
+ if (document.activeElement instanceof HTMLElement &&
40
+ containerEl.contains(document.activeElement) &&
41
+ document.activeElement.getAttribute('role') === 'treeitem') {
42
+ return document.activeElement;
43
+ }
44
+ // Otherwise, focus the first treeitem
45
+ return firstItem instanceof HTMLElement ? firstItem : undefined;
46
+ },
47
+ });
48
+ }
49
+ // DOM utilities used for focus management
50
+ function getNextFocusableElement(activeElement, event) {
51
+ const elementState = getElementState(activeElement);
52
+ // Reference: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboard-interaction-24
53
+ switch (`${elementState} ${event.key}`) {
54
+ case 'open ArrowRight':
55
+ // Focus first child node
56
+ return getFirstChildElement(activeElement);
57
+ case 'open ArrowLeft':
58
+ // Close node; don't change focus
59
+ return;
60
+ case 'closed ArrowRight':
61
+ // Open node; don't change focus
62
+ return;
63
+ case 'closed ArrowLeft':
64
+ // Focus parent element
65
+ return getParentElement(activeElement);
66
+ case 'end ArrowRight':
67
+ // Do nothing
68
+ return;
69
+ case 'end ArrowLeft':
70
+ // Focus parent element
71
+ return getParentElement(activeElement);
72
+ }
73
+ // ArrowUp and ArrowDown behavior is the same regardless of element state
74
+ switch (event.key) {
75
+ case 'ArrowUp':
76
+ // Focus previous visible element
77
+ return getVisibleElement(activeElement, 'previous');
78
+ case 'ArrowDown':
79
+ // Focus next visible element
80
+ return getVisibleElement(activeElement, 'next');
81
+ case 'Backspace':
82
+ return getParentElement(activeElement);
83
+ }
84
+ }
85
+ export function getElementState(element) {
86
+ if (element.getAttribute('role') !== 'treeitem') {
87
+ throw new Error('Element is not a treeitem');
88
+ }
89
+ switch (element.getAttribute('aria-expanded')) {
90
+ case 'true':
91
+ return 'open';
92
+ case 'false':
93
+ return 'closed';
94
+ default:
95
+ return 'end';
96
+ }
97
+ }
98
+ function getVisibleElement(element, direction) {
99
+ const root = element.closest('[role=tree]');
100
+ if (!root)
101
+ return;
102
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, node => {
103
+ if (!(node instanceof HTMLElement))
104
+ return NodeFilter.FILTER_SKIP;
105
+ return node.getAttribute('role') === 'treeitem' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
106
+ });
107
+ let current = walker.firstChild();
108
+ while (current !== element) {
109
+ current = walker.nextNode();
110
+ }
111
+ let next = direction === 'next' ? walker.nextNode() : walker.previousNode();
112
+ // If next element is nested inside a collapsed subtree, continue iterating
113
+ while (next instanceof HTMLElement && next.parentElement?.closest('[role=treeitem][aria-expanded=false]')) {
114
+ next = direction === 'next' ? walker.nextNode() : walker.previousNode();
115
+ }
116
+ return next instanceof HTMLElement ? next : undefined;
117
+ }
118
+ function getFirstChildElement(element) {
119
+ const firstChild = element.querySelector('[role=treeitem]');
120
+ return firstChild instanceof HTMLElement ? firstChild : undefined;
121
+ }
122
+ function getParentElement(element) {
123
+ const group = element.closest('[role=group]');
124
+ const parent = group?.closest('[role=treeitem]');
125
+ return parent instanceof HTMLElement ? parent : undefined;
126
+ }
@@ -0,0 +1,156 @@
1
+ import {TreeViewElement} from './tree_view'
2
+ import {FocusKeys, focusZone} from '@primer/behaviors'
3
+
4
+ // This code was adapted from the roving tab index implementation in primer/react, see:
5
+ // https://github.com/primer/react/blob/f9785343716435f43e3d82482b057a17bd345c25/packages/react/src/TreeView/useRovingTabIndex.ts
6
+ export function useRovingTabIndex(containerEl: TreeViewElement) {
7
+ // TODO: Initialize focus to the aria-current item if it exists
8
+ focusZone(containerEl, {
9
+ bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd | FocusKeys.Backspace,
10
+ getNextFocusable: (_direction, from, event) => {
11
+ if (!(from instanceof HTMLElement)) return
12
+
13
+ // Skip elements within a modal dialog
14
+ // This need to be in a try/catch to avoid errors in
15
+ // non-supported browsers
16
+ try {
17
+ if (from.closest('dialog:modal')) {
18
+ return
19
+ }
20
+ } catch {
21
+ // Don't return
22
+ }
23
+
24
+ return getNextFocusableElement(from, event) ?? from
25
+ },
26
+ focusInStrategy: () => {
27
+ // Don't try to execute the focusInStrategy if focus is coming from a click.
28
+ // The clicked row will receive focus correctly by default.
29
+ // If a chevron is clicked, setting the focus through the focuszone will prevent its toggle.
30
+ // if (mouseDownRef.current) {
31
+ // return undefined
32
+ // }
33
+
34
+ let currentItem = containerEl.querySelector('[aria-current]')
35
+ currentItem = currentItem?.checkVisibility() ? currentItem : null
36
+
37
+ const firstItem = containerEl.querySelector('[role="treeitem"]')
38
+
39
+ // Focus the aria-current item if it exists
40
+ if (currentItem instanceof HTMLElement) {
41
+ return currentItem
42
+ }
43
+
44
+ // Otherwise, focus the activeElement if it's a treeitem
45
+ if (
46
+ document.activeElement instanceof HTMLElement &&
47
+ containerEl.contains(document.activeElement) &&
48
+ document.activeElement.getAttribute('role') === 'treeitem'
49
+ ) {
50
+ return document.activeElement
51
+ }
52
+
53
+ // Otherwise, focus the first treeitem
54
+ return firstItem instanceof HTMLElement ? firstItem : undefined
55
+ },
56
+ })
57
+ }
58
+
59
+ // DOM utilities used for focus management
60
+
61
+ function getNextFocusableElement(activeElement: HTMLElement, event: KeyboardEvent): HTMLElement | undefined {
62
+ const elementState = getElementState(activeElement)
63
+
64
+ // Reference: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboard-interaction-24
65
+ switch (`${elementState} ${event.key}`) {
66
+ case 'open ArrowRight':
67
+ // Focus first child node
68
+ return getFirstChildElement(activeElement)
69
+
70
+ case 'open ArrowLeft':
71
+ // Close node; don't change focus
72
+ return
73
+
74
+ case 'closed ArrowRight':
75
+ // Open node; don't change focus
76
+ return
77
+
78
+ case 'closed ArrowLeft':
79
+ // Focus parent element
80
+ return getParentElement(activeElement)
81
+
82
+ case 'end ArrowRight':
83
+ // Do nothing
84
+ return
85
+
86
+ case 'end ArrowLeft':
87
+ // Focus parent element
88
+ return getParentElement(activeElement)
89
+ }
90
+
91
+ // ArrowUp and ArrowDown behavior is the same regardless of element state
92
+ switch (event.key) {
93
+ case 'ArrowUp':
94
+ // Focus previous visible element
95
+ return getVisibleElement(activeElement, 'previous')
96
+
97
+ case 'ArrowDown':
98
+ // Focus next visible element
99
+ return getVisibleElement(activeElement, 'next')
100
+
101
+ case 'Backspace':
102
+ return getParentElement(activeElement)
103
+ }
104
+ }
105
+
106
+ export function getElementState(element: HTMLElement): 'open' | 'closed' | 'end' {
107
+ if (element.getAttribute('role') !== 'treeitem') {
108
+ throw new Error('Element is not a treeitem')
109
+ }
110
+
111
+ switch (element.getAttribute('aria-expanded')) {
112
+ case 'true':
113
+ return 'open'
114
+ case 'false':
115
+ return 'closed'
116
+ default:
117
+ return 'end'
118
+ }
119
+ }
120
+
121
+ function getVisibleElement(element: HTMLElement, direction: 'next' | 'previous'): HTMLElement | undefined {
122
+ const root = element.closest('[role=tree]')
123
+
124
+ if (!root) return
125
+
126
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, node => {
127
+ if (!(node instanceof HTMLElement)) return NodeFilter.FILTER_SKIP
128
+ return node.getAttribute('role') === 'treeitem' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
129
+ })
130
+
131
+ let current = walker.firstChild()
132
+
133
+ while (current !== element) {
134
+ current = walker.nextNode()
135
+ }
136
+
137
+ let next = direction === 'next' ? walker.nextNode() : walker.previousNode()
138
+
139
+ // If next element is nested inside a collapsed subtree, continue iterating
140
+ while (next instanceof HTMLElement && next.parentElement?.closest('[role=treeitem][aria-expanded=false]')) {
141
+ next = direction === 'next' ? walker.nextNode() : walker.previousNode()
142
+ }
143
+
144
+ return next instanceof HTMLElement ? next : undefined
145
+ }
146
+
147
+ function getFirstChildElement(element: HTMLElement): HTMLElement | undefined {
148
+ const firstChild = element.querySelector('[role=treeitem]')
149
+ return firstChild instanceof HTMLElement ? firstChild : undefined
150
+ }
151
+
152
+ function getParentElement(element: HTMLElement): HTMLElement | undefined {
153
+ const group = element.closest('[role=group]')
154
+ const parent = group?.closest('[role=treeitem]')
155
+ return parent instanceof HTMLElement ? parent : undefined
156
+ }
@@ -0,0 +1,38 @@
1
+ import { TreeViewIconPairElement } from './tree_view_icon_pair_element';
2
+ import { TreeViewIncludeFragmentElement } from './tree_view_include_fragment_element';
3
+ import { TreeViewElement } from './tree_view';
4
+ type LoadingState = 'loading' | 'error' | 'success';
5
+ export declare class TreeViewSubTreeNodeElement extends HTMLElement {
6
+ #private;
7
+ node: HTMLElement;
8
+ subTree: HTMLElement;
9
+ iconPair: TreeViewIconPairElement;
10
+ toggleButton: HTMLElement;
11
+ expandedToggleIcon: HTMLElement;
12
+ collapsedToggleIcon: HTMLElement;
13
+ includeFragment: TreeViewIncludeFragmentElement;
14
+ loadingIndicator: HTMLElement;
15
+ loadingFailureMessage: HTMLElement;
16
+ retryButton: HTMLButtonElement;
17
+ expanded: boolean;
18
+ loadingState: LoadingState;
19
+ connectedCallback(): void;
20
+ get selectStrategy(): string;
21
+ disconnectedCallback(): void;
22
+ handleEvent(event: Event): void;
23
+ expand(): void;
24
+ collapse(): void;
25
+ toggle(): void;
26
+ get nodes(): NodeListOf<Element>;
27
+ eachDirectDescendantNode(): Generator<Element>;
28
+ eachDescendantNode(): Generator<Element>;
29
+ get isEmpty(): boolean;
30
+ get treeView(): TreeViewElement | null;
31
+ toggleChecked(): void;
32
+ }
33
+ declare global {
34
+ interface Window {
35
+ TreeViewSubTreeNodeElement: typeof TreeViewSubTreeNodeElement;
36
+ }
37
+ }
38
+ export {};