openproject-primer_view_components 0.66.1 → 0.67.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 +16 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +2 -0
- data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +4 -2
- 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/border_box/collapsible_header.rb +3 -0
- data/app/components/primer/open_project/collapsible_section.rb +7 -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 +2 -0
- data/app/components/primer/open_project/tree_view/tree_view.js +29 -9
- data/app/components/primer/open_project/tree_view/tree_view.ts +31 -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 +4 -2
- data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +60 -30
- data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +66 -33
- 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/border_box/collapsible_header_preview/playground.html.erb +1 -1
- 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 +29 -3
- data/static/arguments.json +38 -2
- data/static/constants.json +17 -0
- data/static/info_arch.json +95 -3
- data/static/previews.json +26 -0
- metadata +4 -2
@@ -12,6 +12,8 @@ module Primer
|
|
12
12
|
#
|
13
13
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
14
14
|
renders_one :title, lambda { |**system_arguments, &block|
|
15
|
+
raise ArgumentError, "Title must be a string" unless block.call.is_a?(String)
|
16
|
+
|
15
17
|
system_arguments[:mr] ||= 2
|
16
18
|
|
17
19
|
Primer::Beta::Text.new(**system_arguments, &block)
|
@@ -31,6 +33,7 @@ module Primer
|
|
31
33
|
#
|
32
34
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
33
35
|
renders_one :description, lambda { |**system_arguments, &block|
|
36
|
+
raise ArgumentError, "Description must be a string" unless block.call.is_a?(String)
|
34
37
|
system_arguments[:color] ||= :subtle
|
35
38
|
system_arguments[:hidden] = @collapsed
|
36
39
|
|
@@ -15,6 +15,8 @@ module Primer
|
|
15
15
|
# @param tag [Symbol] Customize the element type of the rendered title container.
|
16
16
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
17
17
|
renders_one :title, lambda { |tag: TITLE_TAG_FALLBACK, **system_arguments, &block|
|
18
|
+
raise ArgumentError, "Title must be a string" unless block.call.is_a?(String)
|
19
|
+
|
18
20
|
system_arguments[:tag] = fetch_or_fallback(TITLE_TAG_OPTIONS, tag, TITLE_TAG_FALLBACK)
|
19
21
|
system_arguments[:font_size] ||= 3
|
20
22
|
system_arguments[:mr] ||= 2
|
@@ -26,6 +28,8 @@ module Primer
|
|
26
28
|
#
|
27
29
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
28
30
|
renders_one :caption, lambda { |**system_arguments, &block|
|
31
|
+
raise ArgumentError, "Caption must be a string" unless block.call.is_a?(String)
|
32
|
+
|
29
33
|
system_arguments[:color] ||= :subtle
|
30
34
|
system_arguments[:mr] ||= 2
|
31
35
|
system_arguments[:display] ||= [:none, :block]
|
@@ -36,7 +40,9 @@ module Primer
|
|
36
40
|
# Optional right-side content
|
37
41
|
#
|
38
42
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
39
|
-
renders_one :additional_information, lambda { |**system_arguments|
|
43
|
+
renders_one :additional_information, lambda { |**system_arguments, &block|
|
44
|
+
raise ArgumentError, "The additional information must be a string" unless block.call.is_a?(String)
|
45
|
+
|
40
46
|
Primer::BaseComponent.new(tag: :div, **system_arguments)
|
41
47
|
}
|
42
48
|
|
@@ -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,8 @@ 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;
|
23
25
|
infoFromNode(node: Element, newCheckedValue?: TreeViewCheckedValue): TreeViewNodeInfo | null;
|
24
26
|
}
|
25
27
|
declare global {
|
@@ -126,6 +126,12 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
|
|
126
126
|
getNodeCheckedValue(node) {
|
127
127
|
return (node.getAttribute('aria-checked') || 'false');
|
128
128
|
}
|
129
|
+
nodeHasCheckBox(node) {
|
130
|
+
return node.querySelector('.TreeViewItemCheckbox') !== null;
|
131
|
+
}
|
132
|
+
nodeHasNativeAction(node) {
|
133
|
+
return node instanceof HTMLAnchorElement || node instanceof HTMLButtonElement;
|
134
|
+
}
|
129
135
|
// PRIVATE API METHOD
|
130
136
|
//
|
131
137
|
// This would normally be marked private, but it's called by TreeViewSubTreeNodes
|
@@ -161,7 +167,7 @@ _TreeViewElement_nodeForEvent = function _TreeViewElement_nodeForEvent(event) {
|
|
161
167
|
return node;
|
162
168
|
};
|
163
169
|
_TreeViewElement_handleNodeEvent = function _TreeViewElement_handleNodeEvent(node, event) {
|
164
|
-
if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsCheckboxToggle).call(this, event)) {
|
170
|
+
if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsCheckboxToggle).call(this, event, node)) {
|
165
171
|
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleCheckboxToggle).call(this, node);
|
166
172
|
}
|
167
173
|
else if (__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_eventIsActivation).call(this, event)) {
|
@@ -174,8 +180,8 @@ _TreeViewElement_handleNodeEvent = function _TreeViewElement_handleNodeEvent(nod
|
|
174
180
|
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_handleNodeKeyboardEvent).call(this, event, node);
|
175
181
|
}
|
176
182
|
};
|
177
|
-
_TreeViewElement_eventIsCheckboxToggle = function _TreeViewElement_eventIsCheckboxToggle(event) {
|
178
|
-
return event.type === 'click' &&
|
183
|
+
_TreeViewElement_eventIsCheckboxToggle = function _TreeViewElement_eventIsCheckboxToggle(event, node) {
|
184
|
+
return event.type === 'click' && this.nodeHasCheckBox(node);
|
179
185
|
};
|
180
186
|
_TreeViewElement_handleCheckboxToggle = function _TreeViewElement_handleCheckboxToggle(node) {
|
181
187
|
// only handle checking of leaf nodes
|
@@ -190,6 +196,10 @@ _TreeViewElement_handleCheckboxToggle = function _TreeViewElement_handleCheckbox
|
|
190
196
|
}
|
191
197
|
};
|
192
198
|
_TreeViewElement_handleNodeActivated = function _TreeViewElement_handleNodeActivated(node) {
|
199
|
+
// do not emit activation events for buttons and anchors, since it is assumed any activation
|
200
|
+
// behavior for these element types is user- or browser-defined
|
201
|
+
if (!(node instanceof HTMLDivElement))
|
202
|
+
return;
|
193
203
|
const path = this.getNodePath(node);
|
194
204
|
const activationSuccess = this.dispatchEvent(new CustomEvent('treeViewBeforeNodeActivated', {
|
195
205
|
bubbles: true,
|
@@ -198,7 +208,10 @@ _TreeViewElement_handleNodeActivated = function _TreeViewElement_handleNodeActiv
|
|
198
208
|
}));
|
199
209
|
if (!activationSuccess)
|
200
210
|
return;
|
201
|
-
|
211
|
+
// navigate or trigger button, don't toggle
|
212
|
+
if (!this.nodeHasNativeAction(node)) {
|
213
|
+
this.toggleAtPath(path);
|
214
|
+
}
|
202
215
|
this.dispatchEvent(new CustomEvent('treeViewNodeActivated', {
|
203
216
|
bubbles: true,
|
204
217
|
detail: this.infoFromNode(node),
|
@@ -215,12 +228,19 @@ _TreeViewElement_handleNodeKeyboardEvent = function _TreeViewElement_handleNodeK
|
|
215
228
|
}
|
216
229
|
switch (event.key) {
|
217
230
|
case ' ':
|
218
|
-
|
219
|
-
if (this.
|
220
|
-
|
231
|
+
case 'Enter':
|
232
|
+
if (this.nodeHasCheckBox(node)) {
|
233
|
+
event.preventDefault();
|
234
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
235
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'false');
|
236
|
+
}
|
237
|
+
else {
|
238
|
+
__classPrivateFieldGet(this, _TreeViewElement_instances, "m", _TreeViewElement_setNodeCheckedValue).call(this, node, 'true');
|
239
|
+
}
|
221
240
|
}
|
222
|
-
else {
|
223
|
-
|
241
|
+
else if (node instanceof HTMLAnchorElement) {
|
242
|
+
// simulate click on space
|
243
|
+
node.click();
|
224
244
|
}
|
225
245
|
break;
|
226
246
|
}
|
@@ -44,7 +44,7 @@ export class TreeViewElement extends HTMLElement {
|
|
44
44
|
}
|
45
45
|
|
46
46
|
#handleNodeEvent(node: Element, event: Event) {
|
47
|
-
if (this.#eventIsCheckboxToggle(event)) {
|
47
|
+
if (this.#eventIsCheckboxToggle(event, node)) {
|
48
48
|
this.#handleCheckboxToggle(node)
|
49
49
|
} else if (this.#eventIsActivation(event)) {
|
50
50
|
this.#handleNodeActivated(node)
|
@@ -55,8 +55,8 @@ export class TreeViewElement extends HTMLElement {
|
|
55
55
|
}
|
56
56
|
}
|
57
57
|
|
58
|
-
#eventIsCheckboxToggle(event: Event) {
|
59
|
-
return event.type === 'click' &&
|
58
|
+
#eventIsCheckboxToggle(event: Event, node: Element) {
|
59
|
+
return event.type === 'click' && this.nodeHasCheckBox(node)
|
60
60
|
}
|
61
61
|
|
62
62
|
#handleCheckboxToggle(node: Element) {
|
@@ -72,6 +72,10 @@ export class TreeViewElement extends HTMLElement {
|
|
72
72
|
}
|
73
73
|
|
74
74
|
#handleNodeActivated(node: Element) {
|
75
|
+
// do not emit activation events for buttons and anchors, since it is assumed any activation
|
76
|
+
// behavior for these element types is user- or browser-defined
|
77
|
+
if (!(node instanceof HTMLDivElement)) return
|
78
|
+
|
75
79
|
const path = this.getNodePath(node)
|
76
80
|
|
77
81
|
const activationSuccess = this.dispatchEvent(
|
@@ -84,7 +88,10 @@ export class TreeViewElement extends HTMLElement {
|
|
84
88
|
|
85
89
|
if (!activationSuccess) return
|
86
90
|
|
87
|
-
|
91
|
+
// navigate or trigger button, don't toggle
|
92
|
+
if (!this.nodeHasNativeAction(node)) {
|
93
|
+
this.toggleAtPath(path)
|
94
|
+
}
|
88
95
|
|
89
96
|
this.dispatchEvent(
|
90
97
|
new CustomEvent('treeViewNodeActivated', {
|
@@ -107,12 +114,18 @@ export class TreeViewElement extends HTMLElement {
|
|
107
114
|
|
108
115
|
switch (event.key) {
|
109
116
|
case ' ':
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
117
|
+
case 'Enter':
|
118
|
+
if (this.nodeHasCheckBox(node)) {
|
119
|
+
event.preventDefault()
|
120
|
+
|
121
|
+
if (this.getNodeCheckedValue(node) === 'true') {
|
122
|
+
this.#setNodeCheckedValue(node, 'false')
|
123
|
+
} else {
|
124
|
+
this.#setNodeCheckedValue(node, 'true')
|
125
|
+
}
|
126
|
+
} else if (node instanceof HTMLAnchorElement) {
|
127
|
+
// simulate click on space
|
128
|
+
node.click()
|
116
129
|
}
|
117
130
|
|
118
131
|
break
|
@@ -225,6 +238,14 @@ export class TreeViewElement extends HTMLElement {
|
|
225
238
|
return (node.getAttribute('aria-checked') || 'false') as TreeViewCheckedValue
|
226
239
|
}
|
227
240
|
|
241
|
+
nodeHasCheckBox(node: Element): boolean {
|
242
|
+
return node.querySelector('.TreeViewItemCheckbox') !== null
|
243
|
+
}
|
244
|
+
|
245
|
+
nodeHasNativeAction(node: Element): boolean {
|
246
|
+
return node instanceof HTMLAnchorElement || node instanceof HTMLButtonElement
|
247
|
+
}
|
248
|
+
|
228
249
|
// PRIVATE API METHOD
|
229
250
|
//
|
230
251
|
// 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
|
@@ -14,9 +14,11 @@ export declare class TreeViewSubTreeNodeElement extends HTMLElement {
|
|
14
14
|
loadingIndicator: HTMLElement;
|
15
15
|
loadingFailureMessage: HTMLElement;
|
16
16
|
retryButton: HTMLButtonElement;
|
17
|
-
expanded: boolean;
|
18
|
-
loadingState: LoadingState;
|
19
17
|
connectedCallback(): void;
|
18
|
+
get expanded(): boolean;
|
19
|
+
set expanded(newValue: boolean);
|
20
|
+
get loadingState(): LoadingState;
|
21
|
+
set loadingState(newState: LoadingState);
|
20
22
|
get selectStrategy(): string;
|
21
23
|
disconnectedCallback(): void;
|
22
24
|
handleEvent(event: Event): void;
|