openproject-primer_view_components 0.67.0 → 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.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +1 -0
  4. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +1 -0
  5. data/app/assets/javascripts/primer_view_components.js +1 -1
  6. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  7. data/app/components/primer/open_project/border_box/collapsible_header.rb +0 -3
  8. data/app/components/primer/open_project/collapsible_section.rb +1 -7
  9. data/app/components/primer/open_project/page_header/title.rb +1 -1
  10. data/app/components/primer/open_project/tree_view/tree_view.d.ts +1 -0
  11. data/app/components/primer/open_project/tree_view/tree_view.js +34 -1
  12. data/app/components/primer/open_project/tree_view/tree_view.ts +37 -0
  13. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +1 -0
  14. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +14 -0
  15. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +18 -0
  16. data/lib/primer/view_components/version.rb +1 -1
  17. data/previews/primer/open_project/border_box/collapsible_header_preview/playground.html.erb +1 -1
  18. data/previews/primer/open_project/tree_view_preview/async_alpha.html.erb +12 -0
  19. data/previews/primer/open_project/tree_view_preview.rb +24 -0
  20. data/static/constants.json +1 -1
  21. data/static/info_arch.json +26 -0
  22. data/static/previews.json +26 -0
  23. metadata +3 -2
@@ -12,8 +12,6 @@ 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
-
17
15
  system_arguments[:mr] ||= 2
18
16
 
19
17
  Primer::Beta::Text.new(**system_arguments, &block)
@@ -33,7 +31,6 @@ module Primer
33
31
  #
34
32
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
35
33
  renders_one :description, lambda { |**system_arguments, &block|
36
- raise ArgumentError, "Description must be a string" unless block.call.is_a?(String)
37
34
  system_arguments[:color] ||= :subtle
38
35
  system_arguments[:hidden] = @collapsed
39
36
 
@@ -15,8 +15,6 @@ 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
-
20
18
  system_arguments[:tag] = fetch_or_fallback(TITLE_TAG_OPTIONS, tag, TITLE_TAG_FALLBACK)
21
19
  system_arguments[:font_size] ||= 3
22
20
  system_arguments[:mr] ||= 2
@@ -28,8 +26,6 @@ module Primer
28
26
  #
29
27
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
30
28
  renders_one :caption, lambda { |**system_arguments, &block|
31
- raise ArgumentError, "Caption must be a string" unless block.call.is_a?(String)
32
-
33
29
  system_arguments[:color] ||= :subtle
34
30
  system_arguments[:mr] ||= 2
35
31
  system_arguments[:display] ||= [:none, :block]
@@ -40,9 +36,7 @@ module Primer
40
36
  # Optional right-side content
41
37
  #
42
38
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
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
-
39
+ renders_one :additional_information, lambda { |**system_arguments|
46
40
  Primer::BaseComponent.new(tag: :div, **system_arguments)
47
41
  }
48
42
 
@@ -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 = :h1
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(
@@ -22,6 +22,7 @@ export declare class TreeViewElement extends HTMLElement {
22
22
  getNodeCheckedValue(node: Element): TreeViewCheckedValue;
23
23
  nodeHasCheckBox(node: Element): boolean;
24
24
  nodeHasNativeAction(node: Element): boolean;
25
+ expandAncestorsForNode(node: HTMLElement): void;
25
26
  infoFromNode(node: Element, newCheckedValue?: TreeViewCheckedValue): TreeViewNodeInfo | null;
26
27
  }
27
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();
@@ -132,6 +150,16 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
132
150
  nodeHasNativeAction(node) {
133
151
  return node instanceof HTMLAnchorElement || node instanceof HTMLButtonElement;
134
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
+ }
135
163
  // PRIVATE API METHOD
136
164
  //
137
165
  // This would normally be marked private, but it's called by TreeViewSubTreeNodes
@@ -152,6 +180,11 @@ let TreeViewElement = class TreeViewElement extends HTMLElement {
152
180
  };
153
181
  _TreeViewElement_abortController = new WeakMap();
154
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
+ };
155
188
  _TreeViewElement_eventIsActivation = function _TreeViewElement_eventIsActivation(event) {
156
189
  return event.type === 'click';
157
190
  };
@@ -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() {
@@ -246,6 +272,17 @@ export class TreeViewElement extends HTMLElement {
246
272
  return node instanceof HTMLAnchorElement || node instanceof HTMLButtonElement
247
273
  }
248
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
+
249
286
  // PRIVATE API METHOD
250
287
  //
251
288
  // This would normally be marked private, but it's called by TreeViewSubTreeNodes
@@ -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;
@@ -165,6 +165,16 @@ let TreeViewSubTreeNodeElement = class TreeViewSubTreeNodeElement extends HTMLEl
165
165
  yield node;
166
166
  }
167
167
  }
168
+ *eachAncestorSubTreeNode() {
169
+ if (!this.treeView)
170
+ return;
171
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
172
+ let current = this;
173
+ while (current && this.treeView.contains(current)) {
174
+ yield current;
175
+ current = current.parentElement?.closest('tree-view-sub-tree-node');
176
+ }
177
+ }
168
178
  get isEmpty() {
169
179
  return this.nodes.length === 0;
170
180
  }
@@ -230,6 +240,9 @@ _TreeViewSubTreeNodeElement_handleIncludeFragmentEvent = function _TreeViewSubTr
230
240
  this.loadingState = 'success';
231
241
  break;
232
242
  case 'include-fragment-replaced':
243
+ // Make sure to expand the new sub-tree, otherwise it looks like nothing happened. This prevents
244
+ // having to remember to pass `SubTree.new(expanded: true)` in the controller.
245
+ this.expanded = true;
233
246
  if (__classPrivateFieldGet(this, _TreeViewSubTreeNodeElement_activeElementIsLoader, "f")) {
234
247
  const firstItem = this.querySelector('[role=group] > :first-child');
235
248
  if (!firstItem)
@@ -308,6 +321,7 @@ _TreeViewSubTreeNodeElement_update = function _TreeViewSubTreeNodeElement_update
308
321
  if (this.subTree)
309
322
  this.subTree.hidden = false;
310
323
  this.node.setAttribute('aria-expanded', 'true');
324
+ this.treeView?.expandAncestorsForNode(this);
311
325
  if (this.iconPair) {
312
326
  this.iconPair.showExpanded();
313
327
  }
@@ -217,6 +217,19 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
217
217
  }
218
218
  }
219
219
 
220
+ *eachAncestorSubTreeNode(): Generator<TreeViewSubTreeNodeElement> {
221
+ if (!this.treeView) return
222
+
223
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
224
+ let current: TreeViewSubTreeNodeElement | null = this
225
+
226
+ while (current && this.treeView.contains(current)) {
227
+ yield current
228
+
229
+ current = current.parentElement?.closest('tree-view-sub-tree-node') as TreeViewSubTreeNodeElement | null
230
+ }
231
+ }
232
+
220
233
  get isEmpty(): boolean {
221
234
  return this.nodes.length === 0
222
235
  }
@@ -252,6 +265,10 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
252
265
  break
253
266
 
254
267
  case 'include-fragment-replaced':
268
+ // Make sure to expand the new sub-tree, otherwise it looks like nothing happened. This prevents
269
+ // having to remember to pass `SubTree.new(expanded: true)` in the controller.
270
+ this.expanded = true
271
+
255
272
  if (this.#activeElementIsLoader) {
256
273
  const firstItem = this.querySelector('[role=group] > :first-child') as HTMLElement | null
257
274
  if (!firstItem) return
@@ -377,6 +394,7 @@ export class TreeViewSubTreeNodeElement extends HTMLElement {
377
394
  if (this.expanded) {
378
395
  if (this.subTree) this.subTree.hidden = false
379
396
  this.node.setAttribute('aria-expanded', 'true')
397
+ this.treeView?.expandAncestorsForNode(this)
380
398
 
381
399
  if (this.iconPair) {
382
400
  this.iconPair.showExpanded()
@@ -5,7 +5,7 @@ module Primer
5
5
  module ViewComponents
6
6
  module VERSION
7
7
  MAJOR = 0
8
- MINOR = 67
8
+ MINOR = 68
9
9
  PATCH = 0
10
10
 
11
11
  STRING = [MAJOR, MINOR, PATCH].join(".")
@@ -4,7 +4,7 @@
4
4
  collapsed: collapsed)) do |header| %>
5
5
  <% header.with_title { title } %>
6
6
  <% header.with_count(count: count) %>
7
- <% header.with_description { description } unless description.nil? %>
7
+ <% header.with_description { description } %>
8
8
  <% end %>
9
9
  <% end %>
10
10
  <% component.with_body { "Body" } %>
@@ -0,0 +1,12 @@
1
+ <div style="max-width: 400px">
2
+ <%= render(Primer::OpenProject::TreeView.new) do |tree_view| %>
3
+ <% tree_view.with_sub_tree(label: "primer") do |sub_tree| %>
4
+ <% sub_tree.with_leading_visual_icons do |icons| %>
5
+ <% icons.with_expanded_icon(icon: :"file-directory-open-fill", color: :accent) %>
6
+ <% icons.with_collapsed_icon(icon: :"file-directory-fill", color: :accent) %>
7
+ <% end %>
8
+
9
+ <% sub_tree.with_loading_skeleton(src: tree_view_items_async_alpha_path(action_menu_expanded: action_menu_expanded)) %>
10
+ <% end %>
11
+ <% end %>
12
+ </div>
@@ -76,6 +76,15 @@ module Primer
76
76
  })
77
77
  end
78
78
 
79
+ # @label Async alpha
80
+ #
81
+ # @param action_menu_expanded [Boolean] toggle
82
+ def async_alpha(action_menu_expanded: false)
83
+ render_with_template(locals: {
84
+ action_menu_expanded: coerce_bool(action_menu_expanded)
85
+ })
86
+ end
87
+
79
88
  # @label Leaf node playground
80
89
  #
81
90
  # @param label [String] text
@@ -117,6 +126,21 @@ module Primer
117
126
  })
118
127
  end
119
128
 
129
+ # @label Auto expansion
130
+ #
131
+ def auto_expansion
132
+ render(Primer::OpenProject::TreeView.new) do |tree|
133
+ tree.with_sub_tree(label: "Level 1") do |level1|
134
+ level1.with_sub_tree(label: "Level 2") do |level2|
135
+ # marking this node as expanded should automatically expand all ancestors
136
+ level2.with_sub_tree(label: "Level 3", expanded: true) do |level3|
137
+ level3.with_leaf(label: "Level 4")
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+
120
144
  private
121
145
 
122
146
  def coerce_bool(value)
@@ -1721,7 +1721,7 @@
1721
1721
  },
1722
1722
  "Primer::OpenProject::PageHeader::Title": {
1723
1723
  "GeneratedSlotMethods": "Primer::OpenProject::PageHeader::Title::GeneratedSlotMethods",
1724
- "HEADING_TAG_FALLBACK": "h1",
1724
+ "HEADING_TAG_FALLBACK": "h2",
1725
1725
  "HEADING_TAG_OPTIONS": [
1726
1726
  "h1",
1727
1727
  "h2",
@@ -20619,6 +20619,19 @@
20619
20619
  ]
20620
20620
  }
20621
20621
  },
20622
+ {
20623
+ "preview_path": "primer/open_project/tree_view/async_alpha",
20624
+ "name": "async_alpha",
20625
+ "snapshot": "false",
20626
+ "skip_rules": {
20627
+ "wont_fix": [
20628
+ "region"
20629
+ ],
20630
+ "will_fix": [
20631
+ "color-contrast"
20632
+ ]
20633
+ }
20634
+ },
20622
20635
  {
20623
20636
  "preview_path": "primer/open_project/tree_view/leaf_node_playground",
20624
20637
  "name": "leaf_node_playground",
@@ -20657,6 +20670,19 @@
20657
20670
  "color-contrast"
20658
20671
  ]
20659
20672
  }
20673
+ },
20674
+ {
20675
+ "preview_path": "primer/open_project/tree_view/auto_expansion",
20676
+ "name": "auto_expansion",
20677
+ "snapshot": "false",
20678
+ "skip_rules": {
20679
+ "wont_fix": [
20680
+ "region"
20681
+ ],
20682
+ "will_fix": [
20683
+ "color-contrast"
20684
+ ]
20685
+ }
20660
20686
  }
20661
20687
  ],
20662
20688
  "subcomponents": [
data/static/previews.json CHANGED
@@ -8988,6 +8988,19 @@
8988
8988
  ]
8989
8989
  }
8990
8990
  },
8991
+ {
8992
+ "preview_path": "primer/open_project/tree_view/async_alpha",
8993
+ "name": "async_alpha",
8994
+ "snapshot": "false",
8995
+ "skip_rules": {
8996
+ "wont_fix": [
8997
+ "region"
8998
+ ],
8999
+ "will_fix": [
9000
+ "color-contrast"
9001
+ ]
9002
+ }
9003
+ },
8991
9004
  {
8992
9005
  "preview_path": "primer/open_project/tree_view/leaf_node_playground",
8993
9006
  "name": "leaf_node_playground",
@@ -9026,6 +9039,19 @@
9026
9039
  "color-contrast"
9027
9040
  ]
9028
9041
  }
9042
+ },
9043
+ {
9044
+ "preview_path": "primer/open_project/tree_view/auto_expansion",
9045
+ "name": "auto_expansion",
9046
+ "snapshot": "false",
9047
+ "skip_rules": {
9048
+ "wont_fix": [
9049
+ "region"
9050
+ ],
9051
+ "will_fix": [
9052
+ "color-contrast"
9053
+ ]
9054
+ }
9029
9055
  }
9030
9056
  ]
9031
9057
  },
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openproject-primer_view_components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.67.0
4
+ version: 0.68.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-05-23 00:00:00.000000000 Z
12
+ date: 2025-05-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionview
@@ -1175,6 +1175,7 @@ files:
1175
1175
  - previews/primer/open_project/sub_header_preview/custom_filter_button.html.erb
1176
1176
  - previews/primer/open_project/sub_header_preview/dialog_buttons.html.erb
1177
1177
  - previews/primer/open_project/tree_view_preview.rb
1178
+ - previews/primer/open_project/tree_view_preview/async_alpha.html.erb
1178
1179
  - previews/primer/open_project/tree_view_preview/buttons.html.erb
1179
1180
  - previews/primer/open_project/tree_view_preview/default.html.erb
1180
1181
  - previews/primer/open_project/tree_view_preview/empty.html.erb