openproject-primer_view_components 0.67.0 → 0.69.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/app/assets/javascripts/components/primer/alpha/action_menu/action_menu_element.d.ts +5 -0
  4. data/app/assets/javascripts/components/primer/alpha/action_menu/action_menu_focus_zone_stack.d.ts +17 -0
  5. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +1 -0
  6. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +1 -0
  7. data/app/assets/javascripts/primer_view_components.js +1 -1
  8. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  9. data/app/components/primer/alpha/action_menu/action_menu_element.d.ts +5 -0
  10. data/app/components/primer/alpha/action_menu/action_menu_element.js +111 -16
  11. data/app/components/primer/alpha/action_menu/action_menu_element.ts +136 -23
  12. data/app/components/primer/alpha/action_menu/action_menu_focus_zone_stack.d.ts +17 -0
  13. data/app/components/primer/alpha/action_menu/action_menu_focus_zone_stack.js +62 -0
  14. data/app/components/primer/alpha/action_menu/action_menu_focus_zone_stack.ts +67 -0
  15. data/app/components/primer/alpha/action_menu/list.rb +3 -1
  16. data/app/components/primer/alpha/action_menu/list_wrapper.rb +31 -0
  17. data/app/components/primer/alpha/action_menu/menu.html.erb +24 -0
  18. data/app/components/primer/alpha/action_menu/menu.rb +136 -0
  19. data/app/components/primer/alpha/action_menu/primary_menu.rb +86 -0
  20. data/app/components/primer/alpha/action_menu/sub_menu.rb +74 -0
  21. data/app/components/primer/alpha/action_menu/sub_menu_item.html.erb +5 -0
  22. data/app/components/primer/alpha/action_menu/sub_menu_item.rb +54 -0
  23. data/app/components/primer/alpha/action_menu.html.erb +1 -26
  24. data/app/components/primer/alpha/action_menu.rb +44 -118
  25. data/app/components/primer/alpha/select_panel.rb +3 -3
  26. data/app/components/primer/open_project/border_box/collapsible_header.rb +0 -3
  27. data/app/components/primer/open_project/collapsible_section.rb +1 -7
  28. data/app/components/primer/open_project/page_header/title.rb +1 -1
  29. data/app/components/primer/open_project/tree_view/tree_view.d.ts +1 -0
  30. data/app/components/primer/open_project/tree_view/tree_view.js +34 -1
  31. data/app/components/primer/open_project/tree_view/tree_view.ts +37 -0
  32. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +1 -0
  33. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +14 -0
  34. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +18 -0
  35. data/lib/primer/view_components/version.rb +1 -1
  36. data/previews/primer/alpha/action_menu_preview/multiple_select_form.html.erb +13 -4
  37. data/previews/primer/alpha/action_menu_preview/opens_dialog.html.erb +20 -11
  38. data/previews/primer/alpha/action_menu_preview/single_select_form_items.html.erb +13 -2
  39. data/previews/primer/alpha/action_menu_preview/sub_menus.html.erb +19 -0
  40. data/previews/primer/alpha/action_menu_preview/with_actions.html.erb +20 -11
  41. data/previews/primer/alpha/action_menu_preview/with_deferred_content.html.erb +24 -0
  42. data/previews/primer/alpha/action_menu_preview.rb +93 -29
  43. data/previews/primer/open_project/border_box/collapsible_header_preview/playground.html.erb +1 -1
  44. data/previews/primer/open_project/tree_view_preview/async_alpha.html.erb +12 -0
  45. data/previews/primer/open_project/tree_view_preview.rb +24 -0
  46. data/static/arguments.json +169 -68
  47. data/static/audited_at.json +4 -0
  48. data/static/constants.json +28 -8
  49. data/static/info_arch.json +794 -170
  50. data/static/previews.json +39 -0
  51. data/static/statuses.json +4 -0
  52. metadata +15 -2
@@ -171,143 +171,69 @@ module Primer
171
171
  class ActionMenu < Primer::Component
172
172
  status :alpha
173
173
 
174
- DEFAULT_PRELOAD = false
174
+ delegate :preload, :preload?, :list, to: :@primary_menu
175
+ delegate :with_show_button, :with_item, :items, :with_divider, :with_avatar_item, :with_group, :with_sub_menu_item, to: :@primary_menu
175
176
 
176
- DEFAULT_SELECT_VARIANT = :none
177
- SELECT_VARIANT_OPTIONS = [
178
- :single,
179
- :multiple,
180
- DEFAULT_SELECT_VARIANT
181
- ].freeze
182
-
183
- attr_reader :list, :preload
184
-
185
- alias preload? preload
186
-
187
- # @param menu_id [String] Id of the menu.
188
- # @param anchor_align [Symbol] <%= one_of(Primer::Alpha::Overlay::ANCHOR_ALIGN_OPTIONS) %>.
189
- # @param anchor_side [Symbol] <%= one_of(Primer::Alpha::Overlay::ANCHOR_SIDE_OPTIONS) %>.
190
- # @param size [Symbol] <%= one_of(Primer::Alpha::Overlay::SIZE_OPTIONS) %>.
191
- # @param src [String] Used with an `include-fragment` element to load menu content from the given source URL.
192
- # @param preload [Boolean] When true, and src is present, loads the `include-fragment` on trigger hover.
193
- # @param dynamic_label [Boolean] Whether or not to display the text of the currently selected item in the show button.
194
- # @param dynamic_label_prefix [String] If provided, the prefix is prepended to the dynamic label and displayed in the show button.
195
- # @param select_variant [Symbol] <%= one_of(Primer::Alpha::ActionMenu::SELECT_VARIANT_OPTIONS) %>
196
- # @param form_arguments [Hash] Allows an `ActionMenu` to act as a select list in multi- and single-select modes. Pass the `builder:` and `name:` options to this hash. `builder:` should be an instance of `ActionView::Helpers::FormBuilder`, which are created by the standard Rails `#form_with` and `#form_for` helpers. The `name:` option is the desired name of the field that will be included in the params sent to the server on form submission.
197
- # @param overlay_arguments [Hash] Arguments to pass to the underlying <%= link_to_component(Primer::Alpha::Overlay) %>
198
- # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>.
199
- def initialize(
200
- menu_id: self.class.generate_id,
201
- anchor_align: Primer::Alpha::Overlay::DEFAULT_ANCHOR_ALIGN,
202
- anchor_side: Primer::Alpha::Overlay::DEFAULT_ANCHOR_SIDE,
203
- size: Primer::Alpha::Overlay::DEFAULT_SIZE,
204
- src: nil,
205
- preload: DEFAULT_PRELOAD,
206
- dynamic_label: false,
207
- dynamic_label_prefix: nil,
208
- select_variant: DEFAULT_SELECT_VARIANT,
209
- form_arguments: {},
210
- overlay_arguments: {},
211
- **system_arguments
212
- )
213
- @menu_id = menu_id
214
- @src = src
215
- @preload = fetch_or_fallback_boolean(preload, DEFAULT_PRELOAD)
216
- @system_arguments = deny_tag_argument(**system_arguments)
217
-
218
- @system_arguments[:preload] = true if @src.present? && preload?
177
+ # @!parse
178
+ # # Adds an item to the menu.
179
+ # #
180
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>'s `item` slot.
181
+ # def with_item(**system_arguments)
182
+ # end
183
+ #
184
+ # # Adds an avatar item to the menu.
185
+ # #
186
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>'s `item` slot.
187
+ # def with_avatar_item(**system_arguments)
188
+ # end
189
+ #
190
+ # # Adds a divider to the list. Dividers visually separate items.
191
+ # #
192
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Divider) %>.
193
+ # def with_divider(**system_arguments)
194
+ # end
195
+ #
196
+ # # Adds a group to the menu. Groups are a logical set of items with a header.
197
+ # #
198
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionMenu::Group) %>.
199
+ # def with_group(**system_arguments)
200
+ # end
201
+ #
202
+ # # Gets the list of configured menu items, which includes regular items, avatar items, groups, and dividers.
203
+ # #
204
+ # # @return [Array<ViewComponent::Slot>]
205
+ # def items
206
+ # end
219
207
 
220
- @select_variant = fetch_or_fallback(SELECT_VARIANT_OPTIONS, select_variant, DEFAULT_SELECT_VARIANT)
208
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionMenu::PrimaryMenu) %>.
209
+ def initialize(**system_arguments)
210
+ @primary_menu = PrimaryMenu.allocate
211
+ @system_arguments = @primary_menu.send(:initialize, **system_arguments)
221
212
 
222
213
  @system_arguments[:tag] = :"action-menu"
223
- @system_arguments[:"data-select-variant"] = select_variant
224
- @system_arguments[:"data-dynamic-label"] = "" if dynamic_label
225
- @system_arguments[:"data-dynamic-label-prefix"] = dynamic_label_prefix if dynamic_label_prefix.present?
214
+ @system_arguments[:preload] = true if @primary_menu.preload?
226
215
 
227
- overlay_arguments[:data] = merge_data(
228
- overlay_arguments, data: {
229
- target: "action-menu.overlay"
216
+ @system_arguments[:data] = merge_data(
217
+ @system_arguments, {
218
+ data: { "select-variant": @primary_menu.select_variant }
230
219
  }
231
220
  )
232
221
 
233
- @overlay = Primer::Alpha::Overlay.new(
234
- id: "#{@menu_id}-overlay",
235
- title: "Menu",
236
- visually_hide_title: true,
237
- anchor_align: anchor_align,
238
- anchor_side: anchor_side,
239
- size: size,
240
- **overlay_arguments
241
- )
222
+ @system_arguments[:"data-dynamic-label"] = "" if @primary_menu.dynamic_label
242
223
 
243
- @list = Primer::Alpha::ActionMenu::List.new(
244
- menu_id: @menu_id,
245
- select_variant: select_variant,
246
- form_arguments: form_arguments
247
- )
248
- end
249
-
250
- # @!parse
251
- # # Button to activate the menu.
252
- # #
253
- # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::Overlay) %>'s `show_button` slot.
254
- # renders_one(:show_button)
255
-
256
- # Button to activate the menu.
257
- #
258
- # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::Overlay) %>'s `show_button` slot.
259
- def with_show_button(**system_arguments, &block)
260
- @overlay.with_show_button(**system_arguments, id: "#{@menu_id}-button", controls: "#{@menu_id}-list") do |button|
261
- evaluate_block(button, &block)
224
+ if @primary_menu.dynamic_label_prefix.present?
225
+ @system_arguments[:"data-dynamic-label-prefix"] = @primary_menu.dynamic_label_prefix
262
226
  end
263
227
  end
264
228
 
265
- # @!parse
266
- # # Adds a new item to the list.
267
- # #
268
- # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
269
- # renders_many(:items)
270
-
271
- # Adds a new item to the list.
272
- #
273
- # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
274
- def with_item(**system_arguments, &block)
275
- @list.with_item(**system_arguments, &block)
276
- end
277
-
278
- # Adds a divider to the list.
279
- #
280
- # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>'s `divider` slot.
281
- def with_divider(**system_arguments, &block)
282
- @list.with_divider(**system_arguments, &block)
283
- end
284
-
285
- # Adds an avatar item to the list. Avatar items are a convenient way to accessibly add an item with a leading avatar image.
286
- #
287
- # @param src [String] The source url of the avatar image.
288
- # @param username [String] The username associated with the avatar.
289
- # @param full_name [String] Optional. The user's full name.
290
- # @param full_name_scheme [Symbol] Optional. How to display the user's full name.
291
- # @param avatar_arguments [Hash] Optional. The arguments accepted by <%= link_to_component(Primer::Beta::Avatar) %>.
292
- # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
293
- def with_avatar_item(**system_arguments, &block)
294
- @list.with_avatar_item(**system_arguments, &block)
295
- end
296
-
297
- def with_group(**system_arguments, &block)
298
- @list.with_group(**system_arguments, &block)
299
- end
300
-
301
229
  private
302
230
 
303
231
  def before_render
304
232
  content
305
-
306
- raise ArgumentError, "`items` cannot be set when `src` is specified" if @src.present? && @list.items.any?
307
233
  end
308
234
 
309
235
  def render?
310
- @list.items.any? || @src.present?
236
+ @primary_menu.items.any? || @primary_menu.src.present?
311
237
  end
312
238
  end
313
239
  end
@@ -312,7 +312,7 @@ module Primer
312
312
  # @return [String]
313
313
  attr_reader :body_id
314
314
 
315
- # <%= one_of(Primer::Alpha::ActionMenu::SELECT_VARIANT_OPTIONS) %>
315
+ # <%= one_of(Primer::Alpha::ActionMenu::Menu::SELECT_VARIANT_OPTIONS) %>
316
316
  #
317
317
  # @return [Symbol]
318
318
  attr_reader :select_variant
@@ -461,7 +461,7 @@ module Primer
461
461
 
462
462
  @list = Primer::Alpha::SelectPanel::ItemList.new(
463
463
  **list_arguments,
464
- form_arguments: @list_form_arguments,
464
+ form_arguments: @list_form_arguments,
465
465
  id: "#{@panel_id}-list",
466
466
  select_variant: @select_variant,
467
467
  aria: {
@@ -546,4 +546,4 @@ module Primer
546
546
  end
547
547
  end
548
548
  end
549
- end
549
+ end
@@ -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 = 69
9
9
  PATCH = 0
10
10
 
11
11
  STRING = [MAJOR, MINOR, PATCH].join(".")
@@ -1,10 +1,19 @@
1
1
  <%= form_with(url: action_menu_form_action_path(format: route_format)) do |f| %>
2
+ <% content = -> (base) do %>
3
+ <% base.with_item(label: "Fast forward", data: { value: "fast_forward" }) %>
4
+ <% base.with_item(label: "Recursive", data: { value: "recursive" }) %>
5
+ <% base.with_item(label: "Ours", data: { value: "ours" }, active: true) %>
6
+ <% base.with_item(label: "Resolve") %>
7
+ <% end %>
2
8
  <%= render(Primer::Alpha::ActionMenu.new(select_variant: :multiple, dynamic_label: true, dynamic_label_prefix: "Strategy", form_arguments: { builder: f, name: "foo" })) do |menu| %>
3
9
  <% menu.with_show_button { "Strategy" } %>
4
- <% menu.with_item(label: "Fast forward", data: { value: "fast_forward" }) %>
5
- <% menu.with_item(label: "Recursive", data: { value: "recursive" }) %>
6
- <% menu.with_item(label: "Ours", data: { value: "ours" }, active: true) %>
7
- <% menu.with_item(label: "Resolve") %>
10
+ <% if nest_in_sub_menu %>
11
+ <% menu.with_sub_menu_item(label: "Sub-menu") do |sub_menu| %>
12
+ <% content.call(sub_menu) %>
13
+ <% end %>
14
+ <% else %>
15
+ <% content.call(menu) %>
16
+ <% end %>
8
17
  <% end %>
9
18
  <hr>
10
19
  <div>
@@ -1,20 +1,29 @@
1
1
  <%= render(Primer::Alpha::ActionMenu.new) do |component| %>
2
2
  <% component.with_show_button { "Menu" } %>
3
- <% component.with_item(label: "Item", tag: :button, value: "") %>
4
- <% component.with_item(
5
- label: "Show dialog",
6
- tag: :button,
7
- content_arguments: { "data-show-dialog-id": "my-dialog" },
8
- value: "",
9
- scheme: :danger
10
- ) %>
3
+ <% contents = -> (base) do %>
4
+ <% base.with_item(label: "Item", tag: :button, value: "") %>
5
+ <% base.with_item(
6
+ label: "Show dialog",
7
+ tag: :button,
8
+ content_arguments: { "data-show-dialog-id": "my-dialog" },
9
+ value: "",
10
+ scheme: :danger
11
+ ) %>
12
+ <% end %>
13
+ <% if nest_in_sub_menu %>
14
+ <% component.with_sub_menu_item(label: "Sub-menu") do |sub_menu_item| %>
15
+ <% contents.call(sub_menu_item) %>
16
+ <% end %>
17
+ <% else %>
18
+ <% contents.call(component) %>
19
+ <% end %>
11
20
  <% end %>
12
21
 
13
- <%= render(Primer::Alpha::Dialog.new(id: "my-dialog", title: "Confirm deletion")) do |d| %>
14
- <%= render(Primer::Alpha::Dialog::Body.new()) do %>
22
+ <%= render(Primer::Alpha::Dialog.new(id: "my-dialog", title: "Confirm deletion")) do |dialog| %>
23
+ <% dialog.with_body do %>
15
24
  Are you sure you want to delete this?
16
25
  <% end %>
17
- <%= render(Primer::Alpha::Dialog::Footer.new()) do %>
26
+ <% dialog.with_footer do %>
18
27
  <%= render(Primer::Beta::Button.new(data: { "close-dialog-id": "my-dialog" })) { "Cancel" } %>
19
28
  <%= render(Primer::Beta::Button.new(scheme: :danger)) { "Delete" } %>
20
29
  <% end %>
@@ -1,5 +1,4 @@
1
- <%= render(Primer::Alpha::ActionMenu.new(select_variant: :single)) do |menu| %>
2
- <% menu.with_show_button { "Group By" } %>
1
+ <% contents = -> (menu) do %>
3
2
  <% menu.with_item(
4
3
  label: "Repository",
5
4
  href: action_menu_form_action_path(format: route_format),
@@ -29,3 +28,15 @@
29
28
  }
30
29
  ) %>
31
30
  <% end %>
31
+
32
+ <%= render(Primer::Alpha::ActionMenu.new(select_variant: :single)) do |menu| %>
33
+ <% menu.with_show_button { "Group By" } %>
34
+
35
+ <% if nest_in_sub_menu %>
36
+ <% menu.with_sub_menu_item(label: "Sub-menu") do |sub_menu_item| %>
37
+ <% contents.call(sub_menu_item) %>
38
+ <% end %>
39
+ <% else %>
40
+ <% contents.call(menu) %>
41
+ <% end %>
42
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <%# Center the invoker button to give left-appearing sub-menus enough space %>
2
+ <div style="<%= sub_menu_anchor_side.to_s.include?("left") ? "text-align: center" : "" %>">
3
+ <%= render(Primer::Alpha::ActionMenu.new(anchor_align: anchor_align, anchor_side: anchor_side)) do |menu| %>
4
+ <% menu.with_show_button { "Edit" } %>
5
+ <% menu.with_item(label: "Cut") %>
6
+ <% menu.with_item(label: "Copy") %>
7
+ <% menu.with_sub_menu_item(label: "Paste special", anchor_align: sub_menu_anchor_align, anchor_side: sub_menu_anchor_side) do |sub_menu| %>
8
+ <% sub_menu.with_leading_visual_icon(icon: :"sparkle-fill") %>
9
+ <% sub_menu.with_item(label: "Paste plain text") %>
10
+ <% sub_menu.with_item(label: "Paste formulas") %>
11
+ <% sub_menu.with_item(label: "Paste with formatting") %>
12
+ <% sub_menu.with_sub_menu_item(label: "Paste from") do |sub_menu| %>
13
+ <% sub_menu.with_item(label: "Current clipboard") %>
14
+ <% sub_menu.with_item(label: "History") %>
15
+ <% sub_menu.with_item(label: "Another device") %>
16
+ <% end %>
17
+ <% end %>
18
+ <% end %>
19
+ </div>
@@ -8,15 +8,24 @@
8
8
 
9
9
  <%= render(Primer::Alpha::ActionMenu.new) do |component| %>
10
10
  <% component.with_show_button { "Trigger" } %>
11
- <% component.with_item(label: "Alert", tag: :button, id: "alert-item", disabled: disable_items) %>
12
- <% component.with_item(label: "Navigate", tag: :a, content_arguments: { href: action_menu_landing_path }, disabled: disable_items) %>
13
- <% component.with_item(label: "Copy text", tag: :"clipboard-copy", content_arguments: { value: "Text to copy" }, disabled: disable_items) %>
14
- <% component.with_item(
15
- label: "Submit form",
16
- href: action_menu_form_action_path(format: route_format),
17
- form_arguments: {
18
- name: "foo", value: "bar", method: :post
19
- },
20
- disabled: disable_items
21
- ) %>
11
+ <% contents = -> (base) do %>
12
+ <% base.with_item(label: "Alert", tag: :button, id: "alert-item", disabled: disable_items) %>
13
+ <% base.with_item(label: "Navigate", tag: :a, content_arguments: { href: action_menu_landing_path }, disabled: disable_items) %>
14
+ <% base.with_item(label: "Copy text", tag: :"clipboard-copy", content_arguments: { value: "Text to copy" }, disabled: disable_items) %>
15
+ <% base.with_item(
16
+ label: "Submit form",
17
+ href: action_menu_form_action_path(format: route_format),
18
+ form_arguments: {
19
+ name: "foo", value: "bar", method: :post
20
+ },
21
+ disabled: disable_items
22
+ ) %>
23
+ <% end %>
24
+ <% if nest_in_sub_menu %>
25
+ <% component.with_sub_menu_item(label: "Sub-menu") do |sub_menu_item| %>
26
+ <% contents.call(sub_menu_item) %>
27
+ <% end %>
28
+ <% else %>
29
+ <% contents.call(component) %>
30
+ <% end %>
22
31
  <% end %>