openproject-primer_view_components 0.66.2 → 0.68.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 +22 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +3 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +1 -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/open_project/page_header/title.rb +1 -1
- data/app/components/primer/open_project/tree_view/node.html.erb +2 -2
- data/app/components/primer/open_project/tree_view/node.rb +49 -26
- data/app/components/primer/open_project/tree_view/skeleton_loader.html.erb +1 -1
- data/app/components/primer/open_project/tree_view/spinner_loader.html.erb +2 -2
- data/app/components/primer/open_project/tree_view/sub_tree.html.erb +1 -1
- data/app/components/primer/open_project/tree_view/sub_tree.rb +8 -1
- data/app/components/primer/open_project/tree_view/sub_tree_node.rb +9 -3
- data/app/components/primer/open_project/tree_view/tree_view.d.ts +3 -0
- data/app/components/primer/open_project/tree_view/tree_view.js +63 -10
- data/app/components/primer/open_project/tree_view/tree_view.ts +68 -10
- data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.js +11 -7
- data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.ts +13 -8
- data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +1 -0
- data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +52 -21
- data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +60 -20
- data/app/components/primer/open_project/tree_view.css +1 -1
- data/app/components/primer/open_project/tree_view.css.json +9 -6
- data/app/components/primer/open_project/tree_view.css.map +1 -1
- data/app/components/primer/open_project/tree_view.pcss +53 -38
- data/app/components/primer/open_project/tree_view.rb +88 -24
- data/lib/primer/view_components/version.rb +2 -2
- data/previews/primer/open_project/tree_view_preview/async_alpha.html.erb +12 -0
- data/previews/primer/open_project/tree_view_preview/buttons.html.erb +10 -0
- data/previews/primer/open_project/tree_view_preview/links.html.erb +17 -0
- data/previews/primer/open_project/tree_view_preview.rb +53 -3
- data/static/arguments.json +38 -2
- data/static/constants.json +18 -1
- data/static/info_arch.json +121 -3
- data/static/previews.json +52 -0
- metadata +5 -2
@@ -9,7 +9,7 @@ module Primer
|
|
9
9
|
status :open_project
|
10
10
|
|
11
11
|
HEADING_TAG_OPTIONS = [:h1, :h2, :h3, :h4, :h5, :h6].freeze
|
12
|
-
HEADING_TAG_FALLBACK = :
|
12
|
+
HEADING_TAG_FALLBACK = :h2
|
13
13
|
|
14
14
|
renders_one :editable_form, lambda { |model: false, update_path:, cancel_path:, input_name: :title, method: :put, label: I18n.t(:label_title), placeholder: I18n.t(:label_title), **system_arguments|
|
15
15
|
primer_form_with(
|
@@ -13,7 +13,7 @@
|
|
13
13
|
<% if leading_action? %>
|
14
14
|
<%= leading_action %>
|
15
15
|
<% end %>
|
16
|
-
|
16
|
+
<%= render(Primer::BaseComponent.new(**@content_arguments)) do %>
|
17
17
|
<% if @select_variant == :multiple %>
|
18
18
|
<div aria-hidden="true" class="TreeViewItemCheckbox" tabindex="-1">
|
19
19
|
<div class="FormControl-checkbox"></div>
|
@@ -26,7 +26,7 @@
|
|
26
26
|
<% if trailing_visual? %>
|
27
27
|
<%= trailing_visual %>
|
28
28
|
<% end %>
|
29
|
-
|
29
|
+
<% end %>
|
30
30
|
</div>
|
31
31
|
<%= content %>
|
32
32
|
<% end %>
|
@@ -8,6 +8,10 @@ module Primer
|
|
8
8
|
# This component is part of the <%= link_to_component(Primer::OpenProject::TreeView) %> component and should
|
9
9
|
# not be used directly.
|
10
10
|
class Node < Primer::Component
|
11
|
+
DEFAULT_NODE_VARIANT = Primer::OpenProject::TreeView::DEFAULT_NODE_VARIANT
|
12
|
+
NODE_VARIANT_TAG_MAP = { DEFAULT_NODE_VARIANT => :div, :button => :button, :anchor => :a }.freeze
|
13
|
+
NODE_VARIANT_TAG_OPTIONS = NODE_VARIANT_TAG_MAP.keys.freeze
|
14
|
+
|
11
15
|
# Generic leading action slot
|
12
16
|
renders_one :leading_action
|
13
17
|
|
@@ -39,6 +43,11 @@ module Primer
|
|
39
43
|
# @return [Symbol]
|
40
44
|
attr_reader :select_variant
|
41
45
|
|
46
|
+
# This node's variant, eg. `:button`, `:div`, etc.
|
47
|
+
#
|
48
|
+
# @return [Symbol]
|
49
|
+
attr_reader :node_variant
|
50
|
+
|
42
51
|
DEFAULT_SELECT_VARIANT = :none
|
43
52
|
SELECT_VARIANT_OPTIONS = [
|
44
53
|
:multiple,
|
@@ -53,34 +62,47 @@ module Primer
|
|
53
62
|
]
|
54
63
|
|
55
64
|
# @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.
|
65
|
+
# @param node_variant [Symbol] The node variant to use for the node's content, i.e. the `:button` or `:div`. <%= one_of(Primer::OpenProject::TreeView::NODE_VARIANT_OPTIONS) %>
|
66
|
+
# @param href [String] The URL to use as the `href` attribute for this node. If set to a truthy value, the `tag:` parameter is ignored and assumed to be `:a`.
|
56
67
|
# @param current [Boolean] Whether or not this node is the current node. The current node is styled differently than regular nodes and is the first element that receives focus when tabbing to the `TreeView` component.
|
57
68
|
# @param select_variant [Symbol] Controls the type of checkbox that appears. <%= one_of(Primer::OpenProject::TreeView::Node::SELECT_VARIANT_OPTIONS) %>
|
58
69
|
# @param checked [Boolean | String] The checked state of the node's checkbox. <%= one_of(Primer::OpenProject::TreeView::Node::CHECKED_STATES) %>
|
59
|
-
# @param
|
70
|
+
# @param content_arguments [Hash] Arguments attached to the node's content, i.e the `<button>` or `<a>` element. <%= link_to_system_arguments_docs %>
|
60
71
|
def initialize(
|
61
72
|
path:,
|
73
|
+
node_variant:,
|
74
|
+
href: nil,
|
62
75
|
current: false,
|
63
76
|
select_variant: DEFAULT_SELECT_VARIANT,
|
64
77
|
checked: DEFAULT_CHECKED_STATE,
|
65
|
-
**
|
78
|
+
**content_arguments
|
66
79
|
)
|
67
|
-
@system_arguments =
|
80
|
+
@system_arguments = {
|
81
|
+
tag: :li,
|
82
|
+
role: :none,
|
83
|
+
classes: "TreeViewItem"
|
84
|
+
}
|
85
|
+
|
86
|
+
@content_arguments = content_arguments
|
68
87
|
|
69
88
|
@path = path
|
70
89
|
@current = current
|
71
90
|
@select_variant = fetch_or_fallback(SELECT_VARIANT_OPTIONS, select_variant, DEFAULT_SELECT_VARIANT)
|
72
91
|
@checked = fetch_or_fallback(CHECKED_STATES, checked, DEFAULT_CHECKED_STATE)
|
73
|
-
|
74
|
-
|
75
|
-
@
|
76
|
-
@
|
77
|
-
@
|
78
|
-
|
79
|
-
|
92
|
+
@node_variant = fetch_or_fallback(NODE_VARIANT_TAG_OPTIONS, node_variant, DEFAULT_NODE_VARIANT)
|
93
|
+
|
94
|
+
@content_arguments[:tag] = NODE_VARIANT_TAG_MAP[@node_variant]
|
95
|
+
@content_arguments[:href] = href if href
|
96
|
+
@content_arguments[:id] = content_id
|
97
|
+
@content_arguments[:role] = :treeitem
|
98
|
+
@content_arguments[:tabindex] = current? ? 0 : -1
|
99
|
+
@content_arguments[:classes] = class_names(
|
100
|
+
@content_arguments.delete(:classes),
|
101
|
+
"TreeViewItemContent"
|
80
102
|
)
|
81
103
|
|
82
|
-
@
|
83
|
-
@
|
104
|
+
@content_arguments[:aria] = merge_aria(
|
105
|
+
@content_arguments, {
|
84
106
|
aria: {
|
85
107
|
level: level,
|
86
108
|
selected: false,
|
@@ -90,15 +112,15 @@ module Primer
|
|
90
112
|
}
|
91
113
|
)
|
92
114
|
|
93
|
-
@
|
94
|
-
@
|
115
|
+
@content_arguments[:data] = merge_data(
|
116
|
+
@content_arguments,
|
95
117
|
{ data: { path: @path.to_json } }
|
96
118
|
)
|
97
119
|
|
98
120
|
return unless current?
|
99
121
|
|
100
|
-
@
|
101
|
-
@
|
122
|
+
@content_arguments[:aria] = merge_aria(
|
123
|
+
@content_arguments,
|
102
124
|
{ aria: { current: true } }
|
103
125
|
)
|
104
126
|
end
|
@@ -115,31 +137,32 @@ module Primer
|
|
115
137
|
#
|
116
138
|
# @param other_arguments [Hash] The other hash of system arguments to merge into the current one.
|
117
139
|
def merge_system_arguments!(**other_arguments)
|
118
|
-
@
|
119
|
-
@
|
140
|
+
@content_arguments[:aria] = merge_aria(
|
141
|
+
@content_arguments,
|
120
142
|
other_arguments
|
121
143
|
)
|
122
144
|
|
123
|
-
@
|
124
|
-
@
|
145
|
+
@content_arguments[:data] = merge_data(
|
146
|
+
@content_arguments,
|
125
147
|
other_arguments
|
126
148
|
)
|
127
149
|
|
128
|
-
@
|
150
|
+
@content_arguments.merge!(**other_arguments)
|
129
151
|
end
|
130
152
|
|
131
153
|
private
|
132
154
|
|
133
155
|
def before_render
|
134
|
-
if leading_visual?
|
135
|
-
end
|
136
|
-
|
137
156
|
if leading_action?
|
138
|
-
@
|
139
|
-
@
|
157
|
+
@content_arguments[:data] = merge_data(
|
158
|
+
@content_arguments,
|
140
159
|
{ data: { "has-leading-action": true } }
|
141
160
|
)
|
142
161
|
end
|
162
|
+
|
163
|
+
if select_variant != :none && node_variant != :div
|
164
|
+
raise ArgumentError, "TreeView nodes do not support select variants for tags other than :div."
|
165
|
+
end
|
143
166
|
end
|
144
167
|
|
145
168
|
def content_id
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
<%= render(Primer::BaseComponent.new(tag: :"tree-view-include-fragment", src: @src, loading: :lazy, data: { target: "tree-view-sub-tree-node.subTree tree-view-sub-tree-node.includeFragment", path: @container.path.to_json }, hidden: @container.expanded?, accept: "text/fragment+html")) do %>
|
6
6
|
<%= render(@container) do %>
|
7
|
-
<%= render(Primer::OpenProject::TreeView::Node.new(path: [*@container.path, :loader])) do |node| %>
|
7
|
+
<%= render(Primer::OpenProject::TreeView::Node.new(path: [*@container.path, :loader], node_variant: :div)) do |node| %>
|
8
8
|
<% node.with_text_content do %>
|
9
9
|
<div data-target="tree-view-sub-tree-node.loadingIndicator">
|
10
10
|
<% @count.times do %>
|
@@ -4,14 +4,14 @@
|
|
4
4
|
|
5
5
|
<%= render(Primer::BaseComponent.new(tag: :"tree-view-include-fragment", src: @src, loading: :lazy, data: { target: "tree-view-sub-tree-node.subTree tree-view-sub-tree-node.includeFragment", path: @container.path.to_json }, hidden: @container.expanded?, accept: "text/fragment+html")) do %>
|
6
6
|
<%= render(@container) do %>
|
7
|
-
<%= render(Primer::OpenProject::TreeView::Node.new(path: [*@container.path, :loader], data: { target: "tree-view-sub-tree-node.loadingIndicator" })) do |node| %>
|
7
|
+
<%= render(Primer::OpenProject::TreeView::Node.new(path: [*@container.path, :loader], data: { target: "tree-view-sub-tree-node.loadingIndicator" }, node_variant: :div)) do |node| %>
|
8
8
|
<% node.with_text_content { "Loading..." } %>
|
9
9
|
<% node.with_leading_visual do %>
|
10
10
|
<%= render(Primer::Beta::Spinner.new(size: :small, wrapper_arguments: { classes: "TreeViewItemVisual" })) %>
|
11
11
|
<% end %>
|
12
12
|
<% end %>
|
13
13
|
|
14
|
-
<%= render(Primer::OpenProject::TreeView::Node.new(path: [*@container.path, :failure_msg], data: { target: "tree-view-sub-tree-node.loadingFailureMessage" }, hidden: true)) do |node| %>
|
14
|
+
<%= render(Primer::OpenProject::TreeView::Node.new(path: [*@container.path, :failure_msg], data: { target: "tree-view-sub-tree-node.loadingFailureMessage" }, hidden: true, node_variant: :div)) do |node| %>
|
15
15
|
<% node.with_text_content do %>
|
16
16
|
<%= loading_failure_message %>
|
17
17
|
<% end %>
|
@@ -7,7 +7,7 @@
|
|
7
7
|
|
8
8
|
<%= render(@container) do %>
|
9
9
|
<% if nodes.empty? %>
|
10
|
-
<%= render(Primer::OpenProject::TreeView::Node.new(path: path, data: { "no-items": true })) do |node| %>
|
10
|
+
<%= render(Primer::OpenProject::TreeView::Node.new(path: path, data: { "no-items": true }, node_variant: :div)) do |node| %>
|
11
11
|
<% node.with_text_content do %>
|
12
12
|
<%= no_items_message %>
|
13
13
|
<% end %>
|
@@ -29,6 +29,7 @@ module Primer
|
|
29
29
|
renders: lambda { |component_klass: LeafNode, label:, **system_arguments|
|
30
30
|
component_klass.new(
|
31
31
|
**system_arguments,
|
32
|
+
node_variant: node_variant,
|
32
33
|
path: [*path, label],
|
33
34
|
label: label
|
34
35
|
)
|
@@ -41,6 +42,7 @@ module Primer
|
|
41
42
|
renders: lambda { |component_klass: SubTreeNode, label:, **system_arguments|
|
42
43
|
component_klass.new(
|
43
44
|
**system_arguments,
|
45
|
+
node_variant: node_variant,
|
44
46
|
path: [*path, label],
|
45
47
|
label: label
|
46
48
|
)
|
@@ -87,8 +89,13 @@ module Primer
|
|
87
89
|
|
88
90
|
delegate :path, :expanded?, to: :@container
|
89
91
|
|
92
|
+
attr_reader :node_variant
|
93
|
+
|
94
|
+
# @param node_variant [Symbol] The variant to use for this node. <%= one_of(Primer::OpenProject::TreeView::NODE_VARIANT_OPTIONS) %>
|
90
95
|
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::SubTreeContainer) %>.
|
91
|
-
def initialize(**system_arguments)
|
96
|
+
def initialize(node_variant:, **system_arguments)
|
97
|
+
@node_variant = node_variant
|
98
|
+
|
92
99
|
system_arguments[:data] = merge_data(
|
93
100
|
system_arguments,
|
94
101
|
{ data: { target: "tree-view-sub-tree-node.subTree" } }
|
@@ -102,15 +102,16 @@ module Primer
|
|
102
102
|
}
|
103
103
|
}
|
104
104
|
|
105
|
-
delegate :with_leaf, :with_sub_tree, :with_loading_spinner, :with_loading_skeleton, to: :@sub_tree
|
105
|
+
delegate :with_leaf, :with_sub_tree, :with_loading_spinner, :with_loading_skeleton, :nodes, to: :@sub_tree
|
106
106
|
delegate :current?, :merge_system_arguments!, to: :@node
|
107
107
|
|
108
108
|
# @param label [String] The node's label, i.e. it's textual content.
|
109
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 node_variant [Symbol] The variant to use for this node. <%= one_of(Primer::OpenProject::TreeView::NODE_VARIANT_OPTIONS) %>
|
110
111
|
# @param expanded [Boolean] Whether or not this sub-tree should be rendered expanded.
|
111
112
|
# @param select_strategy [Symbol] What should happen when this sub-tree node is checked. <%= one_of(Primer::OpenProject::TreeView::SubTreeNode::SELECT_STRATEGIES) %>
|
112
113
|
# @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
|
+
def initialize(label:, path:, node_variant:, expanded: false, select_strategy: DEFAULT_SELECT_STRATEGY, **system_arguments)
|
114
115
|
@label = label
|
115
116
|
@system_arguments = system_arguments
|
116
117
|
@select_strategy = fetch_or_fallback(SELECT_STRATEGIES, select_strategy, DEFAULT_SELECT_STRATEGY)
|
@@ -134,10 +135,15 @@ module Primer
|
|
134
135
|
@sub_tree = SubTree.new(
|
135
136
|
expanded: expanded,
|
136
137
|
path: path,
|
138
|
+
node_variant: node_variant,
|
137
139
|
**sub_tree_arguments
|
138
140
|
)
|
139
141
|
|
140
|
-
@node = Primer::OpenProject::TreeView::Node.new(
|
142
|
+
@node = Primer::OpenProject::TreeView::Node.new(
|
143
|
+
**@system_arguments,
|
144
|
+
path: @sub_tree.path,
|
145
|
+
node_variant: node_variant
|
146
|
+
)
|
141
147
|
|
142
148
|
return if @node.select_variant == :none
|
143
149
|
|
@@ -20,6 +20,9 @@ export declare class TreeViewElement extends HTMLElement {
|
|
20
20
|
subTreeAtPath(path: string[]): TreeViewSubTreeNodeElement | null;
|
21
21
|
leafAtPath(path: string[]): HTMLLIElement | null;
|
22
22
|
getNodeCheckedValue(node: Element): TreeViewCheckedValue;
|
23
|
+
nodeHasCheckBox(node: Element): boolean;
|
24
|
+
nodeHasNativeAction(node: Element): boolean;
|
25
|
+
expandAncestorsForNode(node: HTMLElement): void;
|
23
26
|
infoFromNode(node: Element, newCheckedValue?: TreeViewCheckedValue): TreeViewNodeInfo | null;
|
24
27
|
}
|
25
28
|
declare global {
|
@@ -15,7 +15,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
15
15
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
16
16
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
17
17
|
};
|
18
|
-
var _TreeViewElement_instances, _TreeViewElement_abortController, _TreeViewElement_eventIsActivation, _TreeViewElement_nodeForEvent, _TreeViewElement_handleNodeEvent, _TreeViewElement_eventIsCheckboxToggle, _TreeViewElement_handleCheckboxToggle, _TreeViewElement_handleNodeActivated, _TreeViewElement_handleNodeFocused, _TreeViewElement_handleNodeKeyboardEvent, _TreeViewElement_setNodeCheckedValue;
|
18
|
+
var _TreeViewElement_instances, _TreeViewElement_abortController, _TreeViewElement_autoExpandFrom, _TreeViewElement_eventIsActivation, _TreeViewElement_nodeForEvent, _TreeViewElement_handleNodeEvent, _TreeViewElement_eventIsCheckboxToggle, _TreeViewElement_handleCheckboxToggle, _TreeViewElement_handleNodeActivated, _TreeViewElement_handleNodeFocused, _TreeViewElement_handleNodeKeyboardEvent, _TreeViewElement_setNodeCheckedValue;
|
19
19
|
import { controller } from '@github/catalyst';
|
20
20
|
import { useRovingTabIndex } from './tree_view_roving_tab_index';
|
21
21
|
let TreeViewElement = class TreeViewElement extends HTMLElement {
|
@@ -30,6 +30,24 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
|
|
30
30
|
this.addEventListener('focusin', this, { signal });
|
31
31
|
this.addEventListener('keydown', this, { signal });
|
32
32
|
useRovingTabIndex(this);
|
33
|
+
// catch-all for any straggler nodes that aren't available when connectedCallback runs
|
34
|
+
new MutationObserver(mutations => {
|
35
|
+
for (const mutation of mutations) {
|
36
|
+
for (const addedNode of mutation.addedNodes) {
|
37
|
+
if (!(addedNode instanceof HTMLElement))
|
38
|
+
continue;
|
39
|
+
// eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback
|
40
|
+
if (addedNode.querySelector('[aria-expanded=true]')) {
|
41
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_autoExpandFrom).call(this, addedNode);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}).observe(this, { childList: true, subtree: true });
|
46
|
+
// eslint-disable-next-line github/no-then -- We don't want to wait for this to resolve, just get on with it
|
47
|
+
customElements.whenDefined('tree-view-sub-tree-node').then(() => {
|
48
|
+
// depends on TreeViewSubTreeNodeElement#eachAncestorSubTreeNode, which may not be defined yet
|
49
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_autoExpandFrom).call(this, this);
|
50
|
+
});
|
33
51
|
}
|
34
52
|
disconnectedCallback() {
|
35
53
|
__classPrivateFieldGet(this, _TreeViewElement_abortController, "f").abort();
|
@@ -126,6 +144,22 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
|
|
126
144
|
getNodeCheckedValue(node) {
|
127
145
|
return (node.getAttribute('aria-checked') || 'false');
|
128
146
|
}
|
147
|
+
nodeHasCheckBox(node) {
|
148
|
+
return node.querySelector('.TreeViewItemCheckbox') !== null;
|
149
|
+
}
|
150
|
+
nodeHasNativeAction(node) {
|
151
|
+
return node instanceof HTMLAnchorElement || node instanceof HTMLButtonElement;
|
152
|
+
}
|
153
|
+
expandAncestorsForNode(node) {
|
154
|
+
const subTreeNode = node.closest('tree-view-sub-tree-node');
|
155
|
+
if (!subTreeNode)
|
156
|
+
return;
|
157
|
+
for (const ancestor of subTreeNode.eachAncestorSubTreeNode()) {
|
158
|
+
if (!ancestor.expanded) {
|
159
|
+
ancestor.expand();
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}
|
129
163
|
// PRIVATE API METHOD
|
130
164
|
//
|
131
165
|
// This would normally be marked private, but it's called by TreeViewSubTreeNodes
|
@@ -146,6 +180,11 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
|
|
146
180
|
};
|
147
181
|
_TreeViewElement_abortController = new WeakMap();
|
148
182
|
_TreeViewElement_instances = new WeakSet();
|
183
|
+
_TreeViewElement_autoExpandFrom = function _TreeViewElement_autoExpandFrom(root) {
|
184
|
+
for (const element of root.querySelectorAll('[aria-expanded=true]')) {
|
185
|
+
this.expandAncestorsForNode(element);
|
186
|
+
}
|
187
|
+
};
|
149
188
|
_TreeViewElement_eventIsActivation = function _TreeViewElement_eventIsActivation(event) {
|
150
189
|
return event.type === 'click';
|
151
190
|
};
|
@@ -161,7 +200,7 @@ _TreeViewElement_nodeForEvent = function _TreeViewElement_nodeForEvent(event) {
|
|
161
200
|
return node;
|
162
201
|
};
|
163
202
|
_TreeViewElement_handleNodeEvent = function _TreeViewElement_handleNodeEvent(node, event) {
|
164
|
-
if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsCheckboxToggle).call(this, event)) {
|
203
|
+
if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsCheckboxToggle).call(this, event, node)) {
|
165
204
|
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleCheckboxToggle).call(this, node);
|
166
205
|
}
|
167
206
|
else if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsActivation).call(this, event)) {
|
@@ -174,8 +213,8 @@ _TreeViewElement_handleNodeEvent = function _TreeViewElement_handleNodeEvent(nod
|
|
174
213
|
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeKeyboardEvent).call(this, event, node);
|
175
214
|
}
|
176
215
|
};
|
177
|
-
_TreeViewElement_eventIsCheckboxToggle = function _TreeViewElement_eventIsCheckboxToggle(event) {
|
178
|
-
return event.type === 'click' &&
|
216
|
+
_TreeViewElement_eventIsCheckboxToggle = function _TreeViewElement_eventIsCheckboxToggle(event, node) {
|
217
|
+
return event.type === 'click' && this.nodeHasCheckBox(node);
|
179
218
|
};
|
180
219
|
_TreeViewElement_handleCheckboxToggle = function _TreeViewElement_handleCheckboxToggle(node) {
|
181
220
|
// only handle checking of leaf nodes
|
@@ -190,6 +229,10 @@ _TreeViewElement_handleCheckboxToggle = function _TreeViewElement_handleCheckbox
|
|
190
229
|
}
|
191
230
|
};
|
192
231
|
_TreeViewElement_handleNodeActivated = function _TreeViewElement_handleNodeActivated(node) {
|
232
|
+
// do not emit activation events for buttons and anchors, since it is assumed any activation
|
233
|
+
// behavior for these element types is user- or browser-defined
|
234
|
+
if (!(node instanceof HTMLDivElement))
|
235
|
+
return;
|
193
236
|
const path = this.getNodePath(node);
|
194
237
|
const activationSuccess = this.dispatchEvent(new CustomEvent('treeViewBeforeNodeActivated', {
|
195
238
|
bubbles: true,
|
@@ -198,7 +241,10 @@ _TreeViewElement_handleNodeActivated = function _TreeViewElement_handleNodeActiv
|
|
198
241
|
}));
|
199
242
|
if (!activationSuccess)
|
200
243
|
return;
|
201
|
-
|
244
|
+
// navigate or trigger button, don't toggle
|
245
|
+
if (!this.nodeHasNativeAction(node)) {
|
246
|
+
this.toggleAtPath(path);
|
247
|
+
}
|
202
248
|
this.dispatchEvent(new CustomEvent('treeViewNodeActivated', {
|
203
249
|
bubbles: true,
|
204
250
|
detail: this.infoFromNode(node),
|
@@ -215,12 +261,19 @@ _TreeViewElement_handleNodeKeyboardEvent = function _TreeViewElement_handleNodeK
|
|
215
261
|
}
|
216
262
|
switch (event.key) {
|
217
263
|
case ' ':
|
218
|
-
|
219
|
-
if (this.
|
220
|
-
|
264
|
+
case 'Enter':
|
265
|
+
if (this.nodeHasCheckBox(node)) {
|
266
|
+
event.preventDefault();
|
267
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
268
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'false');
|
269
|
+
}
|
270
|
+
else {
|
271
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'true');
|
272
|
+
}
|
221
273
|
}
|
222
|
-
else {
|
223
|
-
|
274
|
+
else if (node instanceof HTMLAnchorElement) {
|
275
|
+
// simulate click on space
|
276
|
+
node.click();
|
224
277
|
}
|
225
278
|
break;
|
226
279
|
}
|
@@ -14,6 +14,32 @@ export class TreeViewElement extends HTMLElement {
|
|
14
14
|
this.addEventListener('keydown', this, {signal})
|
15
15
|
|
16
16
|
useRovingTabIndex(this)
|
17
|
+
|
18
|
+
// catch-all for any straggler nodes that aren't available when connectedCallback runs
|
19
|
+
new MutationObserver(mutations => {
|
20
|
+
for (const mutation of mutations) {
|
21
|
+
for (const addedNode of mutation.addedNodes) {
|
22
|
+
if (!(addedNode instanceof HTMLElement)) continue
|
23
|
+
|
24
|
+
// eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback
|
25
|
+
if (addedNode.querySelector('[aria-expanded=true]')) {
|
26
|
+
this.#autoExpandFrom(addedNode)
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}).observe(this, {childList: true, subtree: true})
|
31
|
+
|
32
|
+
// eslint-disable-next-line github/no-then -- We don't want to wait for this to resolve, just get on with it
|
33
|
+
customElements.whenDefined('tree-view-sub-tree-node').then(() => {
|
34
|
+
// depends on TreeViewSubTreeNodeElement#eachAncestorSubTreeNode, which may not be defined yet
|
35
|
+
this.#autoExpandFrom(this)
|
36
|
+
})
|
37
|
+
}
|
38
|
+
|
39
|
+
#autoExpandFrom(root: HTMLElement) {
|
40
|
+
for (const element of root.querySelectorAll('[aria-expanded=true]')) {
|
41
|
+
this.expandAncestorsForNode(element as HTMLElement)
|
42
|
+
}
|
17
43
|
}
|
18
44
|
|
19
45
|
disconnectedCallback() {
|
@@ -44,7 +70,7 @@ export class TreeViewElement extends HTMLElement {
|
|
44
70
|
}
|
45
71
|
|
46
72
|
#handleNodeEvent(node: Element, event: Event) {
|
47
|
-
if (this.#eventIsCheckboxToggle(event)) {
|
73
|
+
if (this.#eventIsCheckboxToggle(event, node)) {
|
48
74
|
this.#handleCheckboxToggle(node)
|
49
75
|
} else if (this.#eventIsActivation(event)) {
|
50
76
|
this.#handleNodeActivated(node)
|
@@ -55,8 +81,8 @@ export class TreeViewElement extends HTMLElement {
|
|
55
81
|
}
|
56
82
|
}
|
57
83
|
|
58
|
-
#eventIsCheckboxToggle(event: Event) {
|
59
|
-
return event.type === 'click' &&
|
84
|
+
#eventIsCheckboxToggle(event: Event, node: Element) {
|
85
|
+
return event.type === 'click' && this.nodeHasCheckBox(node)
|
60
86
|
}
|
61
87
|
|
62
88
|
#handleCheckboxToggle(node: Element) {
|
@@ -72,6 +98,10 @@ export class TreeViewElement extends HTMLElement {
|
|
72
98
|
}
|
73
99
|
|
74
100
|
#handleNodeActivated(node: Element) {
|
101
|
+
// do not emit activation events for buttons and anchors, since it is assumed any activation
|
102
|
+
// behavior for these element types is user- or browser-defined
|
103
|
+
if (!(node instanceof HTMLDivElement)) return
|
104
|
+
|
75
105
|
const path = this.getNodePath(node)
|
76
106
|
|
77
107
|
const activationSuccess = this.dispatchEvent(
|
@@ -84,7 +114,10 @@ export class TreeViewElement extends HTMLElement {
|
|
84
114
|
|
85
115
|
if (!activationSuccess) return
|
86
116
|
|
87
|
-
|
117
|
+
// navigate or trigger button, don't toggle
|
118
|
+
if (!this.nodeHasNativeAction(node)) {
|
119
|
+
this.toggleAtPath(path)
|
120
|
+
}
|
88
121
|
|
89
122
|
this.dispatchEvent(
|
90
123
|
new CustomEvent('treeViewNodeActivated', {
|
@@ -107,12 +140,18 @@ export class TreeViewElement extends HTMLElement {
|
|
107
140
|
|
108
141
|
switch (event.key) {
|
109
142
|
case ' ':
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
143
|
+
case 'Enter':
|
144
|
+
if (this.nodeHasCheckBox(node)) {
|
145
|
+
event.preventDefault()
|
146
|
+
|
147
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
148
|
+
this.#setNodeCheckedValue(node, 'false')
|
149
|
+
} else {
|
150
|
+
this.#setNodeCheckedValue(node, 'true')
|
151
|
+
}
|
152
|
+
} else if (node instanceof HTMLAnchorElement) {
|
153
|
+
// simulate click on space
|
154
|
+
node.click()
|
116
155
|
}
|
117
156
|
|
118
157
|
break
|
@@ -225,6 +264,25 @@ export class TreeViewElement extends HTMLElement {
|
|
225
264
|
return (node.getAttribute('aria-checked') || 'false') as TreeViewCheckedValue
|
226
265
|
}
|
227
266
|
|
267
|
+
nodeHasCheckBox(node: Element): boolean {
|
268
|
+
return node.querySelector('.TreeViewItemCheckbox') !== null
|
269
|
+
}
|
270
|
+
|
271
|
+
nodeHasNativeAction(node: Element): boolean {
|
272
|
+
return node instanceof HTMLAnchorElement || node instanceof HTMLButtonElement
|
273
|
+
}
|
274
|
+
|
275
|
+
expandAncestorsForNode(node: HTMLElement) {
|
276
|
+
const subTreeNode = node.closest('tree-view-sub-tree-node') as TreeViewSubTreeNodeElement
|
277
|
+
if (!subTreeNode) return
|
278
|
+
|
279
|
+
for (const ancestor of subTreeNode.eachAncestorSubTreeNode()) {
|
280
|
+
if (!ancestor.expanded) {
|
281
|
+
ancestor.expand()
|
282
|
+
}
|
283
|
+
}
|
284
|
+
}
|
285
|
+
|
228
286
|
// PRIVATE API METHOD
|
229
287
|
//
|
230
288
|
// This would normally be marked private, but it's called by TreeViewSubTreeNodes
|
@@ -22,12 +22,6 @@ export function useRovingTabIndex(containerEl) {
|
|
22
22
|
return getNextFocusableElement(from, event) ?? from;
|
23
23
|
},
|
24
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
25
|
let currentItem = containerEl.querySelector('[aria-current]');
|
32
26
|
currentItem = currentItem?.checkVisibility() ? currentItem : null;
|
33
27
|
const firstItem = containerEl.querySelector('[role="treeitem"]');
|
@@ -110,11 +104,21 @@ function getVisibleElement(element, direction) {
|
|
110
104
|
}
|
111
105
|
let next = direction === 'next' ? walker.nextNode() : walker.previousNode();
|
112
106
|
// If next element is nested inside a collapsed subtree, continue iterating
|
113
|
-
while (next instanceof HTMLElement && next
|
107
|
+
while (next instanceof HTMLElement && collapsedParent(next, root)) {
|
114
108
|
next = direction === 'next' ? walker.nextNode() : walker.previousNode();
|
115
109
|
}
|
116
110
|
return next instanceof HTMLElement ? next : undefined;
|
117
111
|
}
|
112
|
+
function collapsedParent(node, root) {
|
113
|
+
for (const ancestor of root.querySelectorAll('[role=treeitem][aria-expanded=false]')) {
|
114
|
+
if (node === ancestor)
|
115
|
+
continue;
|
116
|
+
if (ancestor.closest('li')?.contains(node)) {
|
117
|
+
return ancestor;
|
118
|
+
}
|
119
|
+
}
|
120
|
+
return null;
|
121
|
+
}
|
118
122
|
function getFirstChildElement(element) {
|
119
123
|
const firstChild = element.querySelector('[role=treeitem]');
|
120
124
|
return firstChild instanceof HTMLElement ? firstChild : undefined;
|
@@ -24,13 +24,6 @@ export function useRovingTabIndex(containerEl: TreeViewElement) {
|
|
24
24
|
return getNextFocusableElement(from, event) ?? from
|
25
25
|
},
|
26
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
27
|
let currentItem = containerEl.querySelector('[aria-current]')
|
35
28
|
currentItem = currentItem?.checkVisibility() ? currentItem : null
|
36
29
|
|
@@ -137,13 +130,25 @@ function getVisibleElement(element: HTMLElement, direction: 'next' | 'previous')
|
|
137
130
|
let next = direction === 'next' ? walker.nextNode() : walker.previousNode()
|
138
131
|
|
139
132
|
// If next element is nested inside a collapsed subtree, continue iterating
|
140
|
-
while (next instanceof HTMLElement && next
|
133
|
+
while (next instanceof HTMLElement && collapsedParent(next, root)) {
|
141
134
|
next = direction === 'next' ? walker.nextNode() : walker.previousNode()
|
142
135
|
}
|
143
136
|
|
144
137
|
return next instanceof HTMLElement ? next : undefined
|
145
138
|
}
|
146
139
|
|
140
|
+
function collapsedParent(node: Element, root: Element): Element | null {
|
141
|
+
for (const ancestor of root.querySelectorAll('[role=treeitem][aria-expanded=false]')) {
|
142
|
+
if (node === ancestor) continue
|
143
|
+
|
144
|
+
if (ancestor.closest('li')?.contains(node)) {
|
145
|
+
return ancestor
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
return null
|
150
|
+
}
|
151
|
+
|
147
152
|
function getFirstChildElement(element: HTMLElement): HTMLElement | undefined {
|
148
153
|
const firstChild = element.querySelector('[role=treeitem]')
|
149
154
|
return firstChild instanceof HTMLElement ? firstChild : undefined
|
@@ -28,6 +28,7 @@ export declare class TreeViewSubTreeNodeElement extends HTMLElement {
|
|
28
28
|
get nodes(): NodeListOf<Element>;
|
29
29
|
eachDirectDescendantNode(): Generator<Element>;
|
30
30
|
eachDescendantNode(): Generator<Element>;
|
31
|
+
eachAncestorSubTreeNode(): Generator<TreeViewSubTreeNodeElement>;
|
31
32
|
get isEmpty(): boolean;
|
32
33
|
get treeView(): TreeViewElement | null;
|
33
34
|
toggleChecked(): void;
|