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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/app/assets/javascripts/components/primer/open_project/collapsible.d.ts +2 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +29 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_icon_pair_element.d.ts +15 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_include_fragment_element.d.ts +9 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_roving_tab_index.d.ts +3 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +38 -0
- data/app/assets/javascripts/components/primer/primer.d.ts +4 -0
- data/app/assets/javascripts/components/primer/shared_events.d.ts +15 -0
- data/app/assets/javascripts/primer_view_components.js +1 -1
- data/app/assets/javascripts/primer_view_components.js.map +1 -1
- data/app/assets/styles/primer_view_components.css +1 -1
- data/app/assets/styles/primer_view_components.css.map +1 -1
- data/app/components/primer/alpha/select_panel.css +1 -1
- data/app/components/primer/alpha/select_panel.css.json +2 -2
- data/app/components/primer/alpha/select_panel.css.map +1 -1
- data/app/components/primer/alpha/select_panel.html.erb +1 -1
- data/app/components/primer/alpha/select_panel.pcss +5 -2
- data/app/components/primer/beta/spinner.html.erb +1 -1
- data/app/components/primer/beta/spinner.rb +2 -0
- data/app/components/primer/open_project/border_box/collapsible_header.css +1 -1
- data/app/components/primer/open_project/border_box/collapsible_header.css.json +2 -1
- data/app/components/primer/open_project/border_box/collapsible_header.css.map +1 -1
- data/app/components/primer/open_project/border_box/collapsible_header.html.erb +12 -1
- data/app/components/primer/open_project/border_box/collapsible_header.pcss +4 -0
- data/app/components/primer/open_project/border_box/collapsible_header.rb +16 -12
- data/app/components/primer/open_project/collapsible.d.ts +2 -0
- data/app/components/primer/open_project/collapsible.js +11 -0
- data/app/components/primer/open_project/collapsible.ts +10 -0
- data/app/components/primer/open_project/collapsible_section.html.erb +14 -2
- data/app/components/primer/open_project/collapsible_section.rb +5 -1
- data/app/components/primer/open_project/danger_dialog.html.erb +4 -0
- data/app/components/primer/open_project/feedback_dialog.html.erb +5 -0
- data/app/components/primer/open_project/file_tree_view/directory_node.html.erb +5 -0
- data/app/components/primer/open_project/file_tree_view/directory_node.rb +24 -0
- data/app/components/primer/open_project/file_tree_view/file_node.html.erb +2 -0
- data/app/components/primer/open_project/file_tree_view/file_node.rb +14 -0
- data/app/components/primer/open_project/file_tree_view.rb +15 -0
- data/app/components/primer/open_project/skeleton_box.css +1 -0
- data/app/components/primer/open_project/skeleton_box.css.json +6 -0
- data/app/components/primer/open_project/skeleton_box.css.map +1 -0
- data/app/components/primer/open_project/skeleton_box.html.erb +1 -0
- data/app/components/primer/open_project/skeleton_box.pcss +30 -0
- data/app/components/primer/open_project/skeleton_box.rb +27 -0
- data/app/components/primer/open_project/tree_view/icon.html.erb +1 -0
- data/app/components/primer/open_project/tree_view/icon.rb +22 -0
- data/app/components/primer/open_project/tree_view/icon_pair.html.erb +13 -0
- data/app/components/primer/open_project/tree_view/icon_pair.rb +42 -0
- data/app/components/primer/open_project/tree_view/leading_action.html.erb +3 -0
- data/app/components/primer/open_project/tree_view/leading_action.rb +18 -0
- data/app/components/primer/open_project/tree_view/leaf_node.html.erb +18 -0
- data/app/components/primer/open_project/tree_view/leaf_node.rb +96 -0
- data/app/components/primer/open_project/tree_view/loading_failure_message.html.erb +13 -0
- data/app/components/primer/open_project/tree_view/loading_failure_message.rb +31 -0
- data/app/components/primer/open_project/tree_view/node.html.erb +32 -0
- data/app/components/primer/open_project/tree_view/node.rb +155 -0
- data/app/components/primer/open_project/tree_view/skeleton_loader.html.erb +23 -0
- data/app/components/primer/open_project/tree_view/skeleton_loader.rb +36 -0
- data/app/components/primer/open_project/tree_view/spinner_loader.html.erb +20 -0
- data/app/components/primer/open_project/tree_view/spinner_loader.rb +33 -0
- data/app/components/primer/open_project/tree_view/sub_tree.html.erb +21 -0
- data/app/components/primer/open_project/tree_view/sub_tree.rb +106 -0
- data/app/components/primer/open_project/tree_view/sub_tree_container.html.erb +3 -0
- data/app/components/primer/open_project/tree_view/sub_tree_container.rb +39 -0
- data/app/components/primer/open_project/tree_view/sub_tree_node.html.erb +49 -0
- data/app/components/primer/open_project/tree_view/sub_tree_node.rb +172 -0
- data/app/components/primer/open_project/tree_view/tree_view.d.ts +29 -0
- data/app/components/primer/open_project/tree_view/tree_view.js +238 -0
- data/app/components/primer/open_project/tree_view/tree_view.ts +257 -0
- data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.d.ts +15 -0
- data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.js +62 -0
- data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.ts +56 -0
- data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.d.ts +9 -0
- data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.js +29 -0
- data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.ts +29 -0
- data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.d.ts +3 -0
- data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.js +126 -0
- data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.ts +156 -0
- data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +38 -0
- data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +362 -0
- data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +402 -0
- data/app/components/primer/open_project/tree_view/visual.html.erb +14 -0
- data/app/components/primer/open_project/tree_view/visual.rb +27 -0
- data/app/components/primer/open_project/tree_view.css +1 -0
- data/app/components/primer/open_project/tree_view.css.json +42 -0
- data/app/components/primer/open_project/tree_view.css.map +1 -0
- data/app/components/primer/open_project/tree_view.html.erb +7 -0
- data/app/components/primer/open_project/tree_view.pcss +319 -0
- data/app/components/primer/open_project/tree_view.rb +367 -0
- data/app/components/primer/primer.d.ts +4 -0
- data/app/components/primer/primer.js +4 -0
- data/app/components/primer/primer.pcss +2 -0
- data/app/components/primer/primer.ts +4 -0
- data/app/components/primer/shared_events.d.ts +15 -0
- data/app/components/primer/shared_events.ts +19 -0
- data/app/lib/primer/forms/acts_as_component.rb +1 -12
- data/lib/primer/view_components/version.rb +1 -1
- data/previews/primer/open_project/file_tree_view_preview/default.html.erb +16 -0
- data/previews/primer/open_project/file_tree_view_preview/playground.html.erb +4 -0
- data/previews/primer/open_project/file_tree_view_preview.rb +69 -0
- data/previews/primer/open_project/skeleton_box_preview.rb +20 -0
- data/previews/primer/open_project/tree_view_preview/default.html.erb +24 -0
- data/previews/primer/open_project/tree_view_preview/empty.html.erb +10 -0
- data/previews/primer/open_project/tree_view_preview/leaf_node_playground.html.erb +15 -0
- data/previews/primer/open_project/tree_view_preview/loading_failure.html.erb +36 -0
- data/previews/primer/open_project/tree_view_preview/loading_skeleton.html.erb +12 -0
- data/previews/primer/open_project/tree_view_preview/loading_spinner.html.erb +12 -0
- data/previews/primer/open_project/tree_view_preview/playground.html.erb +4 -0
- data/previews/primer/open_project/tree_view_preview.rb +139 -0
- data/static/arguments.json +400 -0
- data/static/audited_at.json +17 -0
- data/static/classes.json +18 -0
- data/static/constants.json +83 -0
- data/static/info_arch.json +1379 -0
- data/static/previews.json +167 -0
- data/static/statuses.json +17 -0
- 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,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 {};
|