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,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Primer
|
4
|
+
module OpenProject
|
5
|
+
class TreeView
|
6
|
+
# A `TreeView` sub-tree node.
|
7
|
+
#
|
8
|
+
# This component is part of the <%= link_to_component(Primer::OpenProject::TreeView) %> component and should
|
9
|
+
# not be used directly.
|
10
|
+
class SubTreeNode < Primer::Component
|
11
|
+
DEFAULT_SELECT_STRATEGY = :descendants
|
12
|
+
SELECT_STRATEGIES = [
|
13
|
+
:self,
|
14
|
+
DEFAULT_SELECT_STRATEGY
|
15
|
+
]
|
16
|
+
|
17
|
+
# @!parse
|
18
|
+
# # Adds a leading visual icon rendered to the left of the node's label.
|
19
|
+
# #
|
20
|
+
# # @param label [String] A label describing the visual, displayed only to screen readers.
|
21
|
+
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::Icon) %>.
|
22
|
+
# def with_leading_visual_icon(label: nil, **system_arguments, &block)
|
23
|
+
# end
|
24
|
+
|
25
|
+
# @!parse
|
26
|
+
# # Adds a pair of leading visual icon rendered to the left of the node's label.
|
27
|
+
# #
|
28
|
+
# # @param label [String] A label describing the visual, displayed only to screen readers.
|
29
|
+
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::IconPair) %>.
|
30
|
+
# def with_leading_visual_icons(label: nil, **system_arguments, &block)
|
31
|
+
# end
|
32
|
+
|
33
|
+
renders_one :leading_visual, types: {
|
34
|
+
icon: lambda { |label: nil, **system_arguments|
|
35
|
+
merge_system_arguments!(
|
36
|
+
aria: { describedby: leading_visual_label_id }
|
37
|
+
)
|
38
|
+
|
39
|
+
Visual.new(
|
40
|
+
id: leading_visual_label_id,
|
41
|
+
label: label,
|
42
|
+
visual: Icon.new(**system_arguments)
|
43
|
+
)
|
44
|
+
},
|
45
|
+
|
46
|
+
icons: lambda { |label: nil, **system_arguments|
|
47
|
+
merge_system_arguments!(
|
48
|
+
aria: { describedby: leading_visual_label_id }
|
49
|
+
)
|
50
|
+
|
51
|
+
system_arguments[:data] = merge_data(
|
52
|
+
system_arguments,
|
53
|
+
{ data: { target: "tree-view-sub-tree-node.iconPair" } }
|
54
|
+
)
|
55
|
+
|
56
|
+
Visual.new(
|
57
|
+
id: leading_visual_label_id,
|
58
|
+
label: label,
|
59
|
+
visual: IconPair.new(
|
60
|
+
**system_arguments,
|
61
|
+
expanded: @sub_tree.expanded?,
|
62
|
+
)
|
63
|
+
)
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
# @!parse
|
68
|
+
# # Adds a leading action rendered to the left of the node's label and any leading visuals or checkboxes.
|
69
|
+
# #
|
70
|
+
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::IconButton) %>.
|
71
|
+
# def with_leading_action_button(**system_arguments, &block)
|
72
|
+
# end
|
73
|
+
|
74
|
+
renders_one :leading_action, types: {
|
75
|
+
button: lambda { |**system_arguments|
|
76
|
+
LeadingAction.new(
|
77
|
+
action: Primer::Beta::IconButton.new(
|
78
|
+
scheme: :invisible,
|
79
|
+
**system_arguments
|
80
|
+
)
|
81
|
+
)
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
# @!parse
|
86
|
+
# # Adds a trailing visual icon rendered to the right of the node's label.
|
87
|
+
# #
|
88
|
+
# # @param label [String] A label describing the visual, displayed only to screen readers.
|
89
|
+
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::Icon) %>.
|
90
|
+
# def with_trailing_visual_icon(label: nil, **system_arguments, &block)
|
91
|
+
# end
|
92
|
+
|
93
|
+
renders_one :trailing_visual, types: {
|
94
|
+
icon: lambda { |**system_arguments|
|
95
|
+
label = system_arguments.delete(:label)
|
96
|
+
|
97
|
+
Visual.new(
|
98
|
+
id: nil,
|
99
|
+
visual: Icon.new(**system_arguments),
|
100
|
+
label: label
|
101
|
+
)
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
delegate :with_leaf, :with_sub_tree, :with_loading_spinner, :with_loading_skeleton, to: :@sub_tree
|
106
|
+
delegate :current?, :merge_system_arguments!, to: :@node
|
107
|
+
|
108
|
+
# @param label [String] The node's label, i.e. it's textual content.
|
109
|
+
# @param path [Array<String>] The node's "path," i.e. this node's label and the labels of all its ancestors. This node should be reachable by traversing the tree following this path.
|
110
|
+
# @param expanded [Boolean] Whether or not this sub-tree should be rendered expanded.
|
111
|
+
# @param select_strategy [Symbol] What should happen when this sub-tree node is checked. <%= one_of(Primer::OpenProject::TreeView::SubTreeNode::SELECT_STRATEGIES) %>
|
112
|
+
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::Node) %>.
|
113
|
+
def initialize(label:, path:, expanded: false, select_strategy: DEFAULT_SELECT_STRATEGY, **system_arguments)
|
114
|
+
@label = label
|
115
|
+
@system_arguments = system_arguments
|
116
|
+
@select_strategy = fetch_or_fallback(SELECT_STRATEGIES, select_strategy, DEFAULT_SELECT_STRATEGY)
|
117
|
+
|
118
|
+
@system_arguments[:aria] = merge_aria(
|
119
|
+
@system_arguments,
|
120
|
+
{ aria: { expanded: expanded } }
|
121
|
+
)
|
122
|
+
|
123
|
+
@system_arguments[:data] = merge_data(
|
124
|
+
@system_arguments, {
|
125
|
+
data: {
|
126
|
+
target: "tree-view-sub-tree-node.node",
|
127
|
+
"node-type": "sub-tree"
|
128
|
+
}
|
129
|
+
}
|
130
|
+
)
|
131
|
+
|
132
|
+
sub_tree_arguments = @system_arguments.delete(:sub_tree_arguments) || {}
|
133
|
+
|
134
|
+
@sub_tree = SubTree.new(
|
135
|
+
expanded: expanded,
|
136
|
+
path: path,
|
137
|
+
**sub_tree_arguments
|
138
|
+
)
|
139
|
+
|
140
|
+
@node = Primer::OpenProject::TreeView::Node.new(**@system_arguments, path: @sub_tree.path)
|
141
|
+
|
142
|
+
return if @node.select_variant == :none
|
143
|
+
|
144
|
+
@node.merge_system_arguments!(
|
145
|
+
data: {
|
146
|
+
"select-strategy": @select_strategy
|
147
|
+
}
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
def render_in(*args, &block)
|
152
|
+
super.tap do
|
153
|
+
# check this _after_ rendering so @sub_tree's slots are defined
|
154
|
+
if @node.select_variant != :none && @sub_tree.defer?
|
155
|
+
raise ArgumentError, "TreeView does not currently support select variants for sub-trees loaded asynchronously."
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def base_id
|
163
|
+
@base_id ||= self.class.generate_id
|
164
|
+
end
|
165
|
+
|
166
|
+
def leading_visual_label_id
|
167
|
+
@leading_visual_id ||= "#{base_id}-leading-visual-label"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { TreeViewSubTreeNodeElement } from './tree_view_sub_tree_node_element';
|
2
|
+
import type { TreeViewNodeType, TreeViewCheckedValue, TreeViewNodeInfo } from '../../shared_events';
|
3
|
+
export declare class TreeViewElement extends HTMLElement {
|
4
|
+
#private;
|
5
|
+
connectedCallback(): void;
|
6
|
+
disconnectedCallback(): void;
|
7
|
+
handleEvent(event: Event): void;
|
8
|
+
getNodePath(node: Element): string[];
|
9
|
+
getNodeType(node: Element): TreeViewNodeType | null;
|
10
|
+
markCurrentAtPath(path: string[]): void;
|
11
|
+
get currentNode(): HTMLLIElement | null;
|
12
|
+
expandAtPath(path: string[]): void;
|
13
|
+
collapseAtPath(path: string[]): void;
|
14
|
+
toggleAtPath(path: string[]): void;
|
15
|
+
checkAtPath(path: string[]): void;
|
16
|
+
uncheckAtPath(path: string[]): void;
|
17
|
+
toggleCheckedAtPath(path: string[]): void;
|
18
|
+
checkedValueAtPath(path: string[]): TreeViewCheckedValue;
|
19
|
+
nodeAtPath(path: string[], selector?: string): Element | null;
|
20
|
+
subTreeAtPath(path: string[]): TreeViewSubTreeNodeElement | null;
|
21
|
+
leafAtPath(path: string[]): HTMLLIElement | null;
|
22
|
+
getNodeCheckedValue(node: Element): TreeViewCheckedValue;
|
23
|
+
infoFromNode(node: Element, newCheckedValue?: TreeViewCheckedValue): TreeViewNodeInfo | null;
|
24
|
+
}
|
25
|
+
declare global {
|
26
|
+
interface Window {
|
27
|
+
TreeViewElement: typeof TreeViewElement;
|
28
|
+
}
|
29
|
+
}
|
@@ -0,0 +1,238 @@
|
|
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 __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
8
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
9
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
10
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
11
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
12
|
+
};
|
13
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
14
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
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
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
17
|
+
};
|
18
|
+
var _TreeViewElement_instances, _TreeViewElement_abortController, _TreeViewElement_eventIsActivation, _TreeViewElement_nodeForEvent, _TreeViewElement_handleNodeEvent, _TreeViewElement_eventIsCheckboxToggle, _TreeViewElement_handleCheckboxToggle, _TreeViewElement_handleNodeActivated, _TreeViewElement_handleNodeFocused, _TreeViewElement_handleNodeKeyboardEvent, _TreeViewElement_setNodeCheckedValue;
|
19
|
+
import { controller } from '@github/catalyst';
|
20
|
+
import { useRovingTabIndex } from './tree_view_roving_tab_index';
|
21
|
+
let TreeViewElement = class TreeViewElement extends HTMLElement {
|
22
|
+
constructor() {
|
23
|
+
super(...arguments);
|
24
|
+
_TreeViewElement_instances.add(this);
|
25
|
+
_TreeViewElement_abortController.set(this, void 0);
|
26
|
+
}
|
27
|
+
connectedCallback() {
|
28
|
+
const { signal } = (__classPrivateFieldSet(this, _TreeViewElement_abortController, new AbortController(), "f"));
|
29
|
+
this.addEventListener('click', this, { signal });
|
30
|
+
this.addEventListener('focusin', this, { signal });
|
31
|
+
this.addEventListener('keydown', this, { signal });
|
32
|
+
useRovingTabIndex(this);
|
33
|
+
}
|
34
|
+
disconnectedCallback() {
|
35
|
+
__classPrivateFieldGet(this, _TreeViewElement_abortController, "f").abort();
|
36
|
+
}
|
37
|
+
handleEvent(event) {
|
38
|
+
const node = __classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_nodeForEvent).call(this, event);
|
39
|
+
if (node) {
|
40
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeEvent).call(this, node, event);
|
41
|
+
}
|
42
|
+
}
|
43
|
+
getNodePath(node) {
|
44
|
+
const rawPath = node.getAttribute('data-path');
|
45
|
+
if (rawPath) {
|
46
|
+
return JSON.parse(rawPath);
|
47
|
+
}
|
48
|
+
return [];
|
49
|
+
}
|
50
|
+
getNodeType(node) {
|
51
|
+
return node.getAttribute('data-node-type');
|
52
|
+
}
|
53
|
+
markCurrentAtPath(path) {
|
54
|
+
const pathStr = JSON.stringify(path);
|
55
|
+
const nodeToMark = this.querySelector(`[data-path="${CSS.escape(pathStr)}"`);
|
56
|
+
if (!nodeToMark)
|
57
|
+
return;
|
58
|
+
this.currentNode?.setAttribute('aria-current', 'false');
|
59
|
+
nodeToMark.setAttribute('aria-current', 'true');
|
60
|
+
}
|
61
|
+
get currentNode() {
|
62
|
+
return this.querySelector('[aria-current=true]');
|
63
|
+
}
|
64
|
+
expandAtPath(path) {
|
65
|
+
const node = this.subTreeAtPath(path);
|
66
|
+
if (!node)
|
67
|
+
return;
|
68
|
+
node.expand();
|
69
|
+
}
|
70
|
+
collapseAtPath(path) {
|
71
|
+
const node = this.subTreeAtPath(path);
|
72
|
+
if (!node)
|
73
|
+
return;
|
74
|
+
node.collapse();
|
75
|
+
}
|
76
|
+
toggleAtPath(path) {
|
77
|
+
const node = this.subTreeAtPath(path);
|
78
|
+
if (!node)
|
79
|
+
return;
|
80
|
+
node.toggle();
|
81
|
+
}
|
82
|
+
checkAtPath(path) {
|
83
|
+
const node = this.nodeAtPath(path);
|
84
|
+
if (!node)
|
85
|
+
return;
|
86
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'true');
|
87
|
+
}
|
88
|
+
uncheckAtPath(path) {
|
89
|
+
const node = this.nodeAtPath(path);
|
90
|
+
if (!node)
|
91
|
+
return;
|
92
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'false');
|
93
|
+
}
|
94
|
+
toggleCheckedAtPath(path) {
|
95
|
+
const node = this.nodeAtPath(path);
|
96
|
+
if (!node)
|
97
|
+
return;
|
98
|
+
if (this.getNodeType(node) === 'leaf') {
|
99
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
100
|
+
this.uncheckAtPath(path);
|
101
|
+
}
|
102
|
+
else {
|
103
|
+
this.checkAtPath(path);
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
checkedValueAtPath(path) {
|
108
|
+
const node = this.nodeAtPath(path);
|
109
|
+
if (!node)
|
110
|
+
return 'false';
|
111
|
+
return this.getNodeCheckedValue(node);
|
112
|
+
}
|
113
|
+
nodeAtPath(path, selector) {
|
114
|
+
const pathStr = JSON.stringify(path);
|
115
|
+
return this.querySelector(`${selector || ''}[data-path="${CSS.escape(pathStr)}"]`);
|
116
|
+
}
|
117
|
+
subTreeAtPath(path) {
|
118
|
+
const node = this.nodeAtPath(path, '[data-node-type=sub-tree]');
|
119
|
+
if (!node)
|
120
|
+
return null;
|
121
|
+
return node.closest('tree-view-sub-tree-node');
|
122
|
+
}
|
123
|
+
leafAtPath(path) {
|
124
|
+
return this.nodeAtPath(path, '[data-node-type=leaf]');
|
125
|
+
}
|
126
|
+
getNodeCheckedValue(node) {
|
127
|
+
return (node.getAttribute('aria-checked') || 'false');
|
128
|
+
}
|
129
|
+
// PRIVATE API METHOD
|
130
|
+
//
|
131
|
+
// This would normally be marked private, but it's called by TreeViewSubTreeNodes
|
132
|
+
// and thus must be public.
|
133
|
+
infoFromNode(node, newCheckedValue) {
|
134
|
+
const type = this.getNodeType(node);
|
135
|
+
if (!type)
|
136
|
+
return null;
|
137
|
+
const checkedValue = this.getNodeCheckedValue(node);
|
138
|
+
return {
|
139
|
+
node,
|
140
|
+
type,
|
141
|
+
path: this.getNodePath(node),
|
142
|
+
checkedValue: newCheckedValue || checkedValue,
|
143
|
+
previousCheckedValue: checkedValue,
|
144
|
+
};
|
145
|
+
}
|
146
|
+
};
|
147
|
+
_TreeViewElement_abortController = new WeakMap();
|
148
|
+
_TreeViewElement_instances = new WeakSet();
|
149
|
+
_TreeViewElement_eventIsActivation = function _TreeViewElement_eventIsActivation(event) {
|
150
|
+
return event.type === 'click';
|
151
|
+
};
|
152
|
+
_TreeViewElement_nodeForEvent = function _TreeViewElement_nodeForEvent(event) {
|
153
|
+
const target = event.target;
|
154
|
+
const node = target.closest('[role=treeitem]');
|
155
|
+
if (!node)
|
156
|
+
return null;
|
157
|
+
if (target.closest('.TreeViewItemToggle'))
|
158
|
+
return null;
|
159
|
+
if (target.closest('.TreeViewItemLeadingAction'))
|
160
|
+
return null;
|
161
|
+
return node;
|
162
|
+
};
|
163
|
+
_TreeViewElement_handleNodeEvent = function _TreeViewElement_handleNodeEvent(node, event) {
|
164
|
+
if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsCheckboxToggle).call(this, event)) {
|
165
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleCheckboxToggle).call(this, node);
|
166
|
+
}
|
167
|
+
else if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsActivation).call(this, event)) {
|
168
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeActivated).call(this, node);
|
169
|
+
}
|
170
|
+
else if (event.type === 'focusin') {
|
171
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeFocused).call(this, node);
|
172
|
+
}
|
173
|
+
else if (event instanceof KeyboardEvent) {
|
174
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeKeyboardEvent).call(this, event, node);
|
175
|
+
}
|
176
|
+
};
|
177
|
+
_TreeViewElement_eventIsCheckboxToggle = function _TreeViewElement_eventIsCheckboxToggle(event) {
|
178
|
+
return event.type === 'click' && event.target.closest('.TreeViewItemCheckbox') !== null;
|
179
|
+
};
|
180
|
+
_TreeViewElement_handleCheckboxToggle = function _TreeViewElement_handleCheckboxToggle(node) {
|
181
|
+
// only handle checking of leaf nodes
|
182
|
+
const type = this.getNodeType(node);
|
183
|
+
if (type !== 'leaf')
|
184
|
+
return;
|
185
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
186
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'false');
|
187
|
+
}
|
188
|
+
else {
|
189
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'true');
|
190
|
+
}
|
191
|
+
};
|
192
|
+
_TreeViewElement_handleNodeActivated = function _TreeViewElement_handleNodeActivated(node) {
|
193
|
+
const path = this.getNodePath(node);
|
194
|
+
const activationSuccess = this.dispatchEvent(new CustomEvent('treeViewBeforeNodeActivated', {
|
195
|
+
bubbles: true,
|
196
|
+
cancelable: true,
|
197
|
+
detail: this.infoFromNode(node),
|
198
|
+
}));
|
199
|
+
if (!activationSuccess)
|
200
|
+
return;
|
201
|
+
this.toggleAtPath(path);
|
202
|
+
this.dispatchEvent(new CustomEvent('treeViewNodeActivated', {
|
203
|
+
bubbles: true,
|
204
|
+
detail: this.infoFromNode(node),
|
205
|
+
}));
|
206
|
+
};
|
207
|
+
_TreeViewElement_handleNodeFocused = function _TreeViewElement_handleNodeFocused(node) {
|
208
|
+
const previousNode = this.querySelector('[aria-selected=true]');
|
209
|
+
previousNode?.setAttribute('aria-selected', 'false');
|
210
|
+
node.setAttribute('aria-selected', 'true');
|
211
|
+
};
|
212
|
+
_TreeViewElement_handleNodeKeyboardEvent = function _TreeViewElement_handleNodeKeyboardEvent(event, node) {
|
213
|
+
if (!node || this.getNodeType(node) !== 'leaf') {
|
214
|
+
return;
|
215
|
+
}
|
216
|
+
switch (event.key) {
|
217
|
+
case ' ':
|
218
|
+
event.preventDefault();
|
219
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
220
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'false');
|
221
|
+
}
|
222
|
+
else {
|
223
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'true');
|
224
|
+
}
|
225
|
+
break;
|
226
|
+
}
|
227
|
+
};
|
228
|
+
_TreeViewElement_setNodeCheckedValue = function _TreeViewElement_setNodeCheckedValue(node, value) {
|
229
|
+
node.setAttribute('aria-checked', value.toString());
|
230
|
+
};
|
231
|
+
TreeViewElement = __decorate([
|
232
|
+
controller
|
233
|
+
], TreeViewElement);
|
234
|
+
export { TreeViewElement };
|
235
|
+
if (!window.customElements.get('tree-view')) {
|
236
|
+
window.TreeViewElement = TreeViewElement;
|
237
|
+
window.customElements.define('tree-view', TreeViewElement);
|
238
|
+
}
|
@@ -0,0 +1,257 @@
|
|
1
|
+
import {controller} from '@github/catalyst'
|
2
|
+
import {TreeViewSubTreeNodeElement} from './tree_view_sub_tree_node_element'
|
3
|
+
import {useRovingTabIndex} from './tree_view_roving_tab_index'
|
4
|
+
import type {TreeViewNodeType, TreeViewCheckedValue, TreeViewNodeInfo} from '../../shared_events'
|
5
|
+
|
6
|
+
@controller
|
7
|
+
export class TreeViewElement extends HTMLElement {
|
8
|
+
#abortController: AbortController
|
9
|
+
|
10
|
+
connectedCallback() {
|
11
|
+
const {signal} = (this.#abortController = new AbortController())
|
12
|
+
this.addEventListener('click', this, {signal})
|
13
|
+
this.addEventListener('focusin', this, {signal})
|
14
|
+
this.addEventListener('keydown', this, {signal})
|
15
|
+
|
16
|
+
useRovingTabIndex(this)
|
17
|
+
}
|
18
|
+
|
19
|
+
disconnectedCallback() {
|
20
|
+
this.#abortController.abort()
|
21
|
+
}
|
22
|
+
|
23
|
+
handleEvent(event: Event) {
|
24
|
+
const node = this.#nodeForEvent(event)
|
25
|
+
|
26
|
+
if (node) {
|
27
|
+
this.#handleNodeEvent(node, event)
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
#eventIsActivation(event: Event): boolean {
|
32
|
+
return event.type === 'click'
|
33
|
+
}
|
34
|
+
|
35
|
+
#nodeForEvent(event: Event): Element | null {
|
36
|
+
const target = event.target as Element
|
37
|
+
const node = target.closest('[role=treeitem]')
|
38
|
+
if (!node) return null
|
39
|
+
|
40
|
+
if (target.closest('.TreeViewItemToggle')) return null
|
41
|
+
if (target.closest('.TreeViewItemLeadingAction')) return null
|
42
|
+
|
43
|
+
return node
|
44
|
+
}
|
45
|
+
|
46
|
+
#handleNodeEvent(node: Element, event: Event) {
|
47
|
+
if (this.#eventIsCheckboxToggle(event)) {
|
48
|
+
this.#handleCheckboxToggle(node)
|
49
|
+
} else if (this.#eventIsActivation(event)) {
|
50
|
+
this.#handleNodeActivated(node)
|
51
|
+
} else if (event.type === 'focusin') {
|
52
|
+
this.#handleNodeFocused(node)
|
53
|
+
} else if (event instanceof KeyboardEvent) {
|
54
|
+
this.#handleNodeKeyboardEvent(event, node)
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
#eventIsCheckboxToggle(event: Event) {
|
59
|
+
return event.type === 'click' && (event.target as HTMLElement).closest('.TreeViewItemCheckbox') !== null
|
60
|
+
}
|
61
|
+
|
62
|
+
#handleCheckboxToggle(node: Element) {
|
63
|
+
// only handle checking of leaf nodes
|
64
|
+
const type = this.getNodeType(node)
|
65
|
+
if (type !== 'leaf') return
|
66
|
+
|
67
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
68
|
+
this.#setNodeCheckedValue(node, 'false')
|
69
|
+
} else {
|
70
|
+
this.#setNodeCheckedValue(node, 'true')
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
#handleNodeActivated(node: Element) {
|
75
|
+
const path = this.getNodePath(node)
|
76
|
+
|
77
|
+
const activationSuccess = this.dispatchEvent(
|
78
|
+
new CustomEvent('treeViewBeforeNodeActivated', {
|
79
|
+
bubbles: true,
|
80
|
+
cancelable: true,
|
81
|
+
detail: this.infoFromNode(node),
|
82
|
+
}),
|
83
|
+
)
|
84
|
+
|
85
|
+
if (!activationSuccess) return
|
86
|
+
|
87
|
+
this.toggleAtPath(path)
|
88
|
+
|
89
|
+
this.dispatchEvent(
|
90
|
+
new CustomEvent('treeViewNodeActivated', {
|
91
|
+
bubbles: true,
|
92
|
+
detail: this.infoFromNode(node),
|
93
|
+
}),
|
94
|
+
)
|
95
|
+
}
|
96
|
+
|
97
|
+
#handleNodeFocused(node: Element) {
|
98
|
+
const previousNode = this.querySelector('[aria-selected=true]')
|
99
|
+
previousNode?.setAttribute('aria-selected', 'false')
|
100
|
+
node.setAttribute('aria-selected', 'true')
|
101
|
+
}
|
102
|
+
|
103
|
+
#handleNodeKeyboardEvent(event: KeyboardEvent, node: Element) {
|
104
|
+
if (!node || this.getNodeType(node) !== 'leaf') {
|
105
|
+
return
|
106
|
+
}
|
107
|
+
|
108
|
+
switch (event.key) {
|
109
|
+
case ' ':
|
110
|
+
event.preventDefault()
|
111
|
+
|
112
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
113
|
+
this.#setNodeCheckedValue(node, 'false')
|
114
|
+
} else {
|
115
|
+
this.#setNodeCheckedValue(node, 'true')
|
116
|
+
}
|
117
|
+
|
118
|
+
break
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
getNodePath(node: Element): string[] {
|
123
|
+
const rawPath = node.getAttribute('data-path')
|
124
|
+
|
125
|
+
if (rawPath) {
|
126
|
+
return JSON.parse(rawPath)
|
127
|
+
}
|
128
|
+
|
129
|
+
return []
|
130
|
+
}
|
131
|
+
|
132
|
+
getNodeType(node: Element): TreeViewNodeType | null {
|
133
|
+
return node.getAttribute('data-node-type') as TreeViewNodeType | null
|
134
|
+
}
|
135
|
+
|
136
|
+
markCurrentAtPath(path: string[]) {
|
137
|
+
const pathStr = JSON.stringify(path)
|
138
|
+
const nodeToMark = this.querySelector(`[data-path="${CSS.escape(pathStr)}"`)
|
139
|
+
if (!nodeToMark) return
|
140
|
+
|
141
|
+
this.currentNode?.setAttribute('aria-current', 'false')
|
142
|
+
nodeToMark.setAttribute('aria-current', 'true')
|
143
|
+
}
|
144
|
+
|
145
|
+
get currentNode(): HTMLLIElement | null {
|
146
|
+
return this.querySelector('[aria-current=true]')
|
147
|
+
}
|
148
|
+
|
149
|
+
expandAtPath(path: string[]) {
|
150
|
+
const node = this.subTreeAtPath(path)
|
151
|
+
if (!node) return
|
152
|
+
|
153
|
+
node.expand()
|
154
|
+
}
|
155
|
+
|
156
|
+
collapseAtPath(path: string[]) {
|
157
|
+
const node = this.subTreeAtPath(path)
|
158
|
+
if (!node) return
|
159
|
+
|
160
|
+
node.collapse()
|
161
|
+
}
|
162
|
+
|
163
|
+
toggleAtPath(path: string[]) {
|
164
|
+
const node = this.subTreeAtPath(path)
|
165
|
+
if (!node) return
|
166
|
+
|
167
|
+
node.toggle()
|
168
|
+
}
|
169
|
+
|
170
|
+
checkAtPath(path: string[]) {
|
171
|
+
const node = this.nodeAtPath(path)
|
172
|
+
if (!node) return
|
173
|
+
|
174
|
+
this.#setNodeCheckedValue(node, 'true')
|
175
|
+
}
|
176
|
+
|
177
|
+
uncheckAtPath(path: string[]) {
|
178
|
+
const node = this.nodeAtPath(path)
|
179
|
+
if (!node) return
|
180
|
+
|
181
|
+
this.#setNodeCheckedValue(node, 'false')
|
182
|
+
}
|
183
|
+
|
184
|
+
toggleCheckedAtPath(path: string[]) {
|
185
|
+
const node = this.nodeAtPath(path)
|
186
|
+
if (!node) return
|
187
|
+
|
188
|
+
if (this.getNodeType(node) === 'leaf') {
|
189
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
190
|
+
this.uncheckAtPath(path)
|
191
|
+
} else {
|
192
|
+
this.checkAtPath(path)
|
193
|
+
}
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
checkedValueAtPath(path: string[]): TreeViewCheckedValue {
|
198
|
+
const node = this.nodeAtPath(path)
|
199
|
+
if (!node) return 'false'
|
200
|
+
|
201
|
+
return this.getNodeCheckedValue(node)
|
202
|
+
}
|
203
|
+
|
204
|
+
nodeAtPath(path: string[], selector?: string): Element | null {
|
205
|
+
const pathStr = JSON.stringify(path)
|
206
|
+
return this.querySelector(`${selector || ''}[data-path="${CSS.escape(pathStr)}"]`)
|
207
|
+
}
|
208
|
+
|
209
|
+
subTreeAtPath(path: string[]): TreeViewSubTreeNodeElement | null {
|
210
|
+
const node = this.nodeAtPath(path, '[data-node-type=sub-tree]')
|
211
|
+
if (!node) return null
|
212
|
+
|
213
|
+
return node.closest('tree-view-sub-tree-node') as TreeViewSubTreeNodeElement | null
|
214
|
+
}
|
215
|
+
|
216
|
+
leafAtPath(path: string[]): HTMLLIElement | null {
|
217
|
+
return this.nodeAtPath(path, '[data-node-type=leaf]') as HTMLLIElement | null
|
218
|
+
}
|
219
|
+
|
220
|
+
#setNodeCheckedValue(node: Element, value: TreeViewCheckedValue) {
|
221
|
+
node.setAttribute('aria-checked', value.toString())
|
222
|
+
}
|
223
|
+
|
224
|
+
getNodeCheckedValue(node: Element): TreeViewCheckedValue {
|
225
|
+
return (node.getAttribute('aria-checked') || 'false') as TreeViewCheckedValue
|
226
|
+
}
|
227
|
+
|
228
|
+
// PRIVATE API METHOD
|
229
|
+
//
|
230
|
+
// This would normally be marked private, but it's called by TreeViewSubTreeNodes
|
231
|
+
// and thus must be public.
|
232
|
+
infoFromNode(node: Element, newCheckedValue?: TreeViewCheckedValue): TreeViewNodeInfo | null {
|
233
|
+
const type = this.getNodeType(node)
|
234
|
+
if (!type) return null
|
235
|
+
|
236
|
+
const checkedValue = this.getNodeCheckedValue(node)
|
237
|
+
|
238
|
+
return {
|
239
|
+
node,
|
240
|
+
type,
|
241
|
+
path: this.getNodePath(node),
|
242
|
+
checkedValue: newCheckedValue || checkedValue,
|
243
|
+
previousCheckedValue: checkedValue,
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
if (!window.customElements.get('tree-view')) {
|
249
|
+
window.TreeViewElement = TreeViewElement
|
250
|
+
window.customElements.define('tree-view', TreeViewElement)
|
251
|
+
}
|
252
|
+
|
253
|
+
declare global {
|
254
|
+
interface Window {
|
255
|
+
TreeViewElement: typeof TreeViewElement
|
256
|
+
}
|
257
|
+
}
|