openproject-primer_view_components 0.73.1 → 0.74.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/app/assets/javascripts/components/primer/alpha/tree_view/tree_view.d.ts +6 -2
  4. data/app/assets/javascripts/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.d.ts +2 -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/assets/styles/primer_view_components.css +1 -1
  8. data/app/assets/styles/primer_view_components.css.map +1 -1
  9. data/app/components/primer/alpha/tree_view/node.html.erb +5 -1
  10. data/app/components/primer/alpha/tree_view/node.rb +7 -1
  11. data/app/components/primer/alpha/tree_view/sub_tree_node.rb +1 -1
  12. data/app/components/primer/alpha/tree_view/tree_view.d.ts +6 -2
  13. data/app/components/primer/alpha/tree_view/tree_view.js +52 -2
  14. data/app/components/primer/alpha/tree_view/tree_view.ts +67 -3
  15. data/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.d.ts +2 -0
  16. data/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.js +11 -0
  17. data/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts +12 -0
  18. data/app/components/primer/alpha/tree_view.css +1 -1
  19. data/app/components/primer/alpha/tree_view.css.json +1 -0
  20. data/app/components/primer/alpha/tree_view.css.map +1 -1
  21. data/app/components/primer/alpha/tree_view.pcss +7 -0
  22. data/app/components/primer/alpha/tree_view.rb +15 -1
  23. data/app/components/primer/open_project/filterable_tree_view/sub_tree.rb +14 -4
  24. data/app/components/primer/open_project/filterable_tree_view.rb +26 -2
  25. data/lib/primer/view_components/version.rb +2 -2
  26. data/previews/primer/alpha/tree_view_preview/default.html.erb +1 -1
  27. data/previews/primer/alpha/tree_view_preview/form_input.html.erb +4 -4
  28. data/previews/primer/alpha/tree_view_preview/multi_select.html.erb +10 -0
  29. data/previews/primer/alpha/tree_view_preview/single_select.html.erb +10 -0
  30. data/previews/primer/alpha/tree_view_preview.rb +38 -4
  31. data/previews/primer/open_project/filterable_tree_view_preview/form_input.html.erb +15 -15
  32. data/previews/primer/open_project/filterable_tree_view_preview/playground.html.erb +15 -15
  33. data/previews/primer/open_project/filterable_tree_view_preview.rb +6 -2
  34. data/static/arguments.json +1 -1
  35. data/static/constants.json +1 -0
  36. data/static/info_arch.json +28 -2
  37. data/static/previews.json +26 -0
  38. metadata +4 -2
@@ -230,19 +230,43 @@ module Primer
230
230
  end
231
231
 
232
232
  def with_sub_tree(**system_arguments, &block)
233
+ system_arguments[:select_variant] ||= :multiple
234
+
235
+ if system_arguments[:select_variant] != :multiple && system_arguments[:select_variant] != :single
236
+ raise ArgumentError, "FilterableTreeView only supports `:multiple` or `:single` as select_variant"
237
+ end
238
+
239
+ if system_arguments[:select_variant] == :single
240
+ # In single selection, the include sub-items checkbox and the SegmentedControl make no sense
241
+ @include_sub_items_check_box_arguments[:hidden] = true
242
+ @include_sub_items_check_box_arguments[:checked] = false
243
+ @filter_mode_control_arguments[:hidden] = true
244
+ end
245
+
233
246
  @tree_view.with_sub_tree(
234
247
  sub_tree_component_klass: SubTree,
235
248
  **system_arguments,
236
- select_variant: :multiple,
237
249
  select_strategy: :self,
238
250
  &block
239
251
  )
240
252
  end
241
253
 
242
254
  def with_leaf(**system_arguments, &block)
255
+ system_arguments[:select_variant] ||= :multiple
256
+
257
+ if system_arguments[:select_variant] != :multiple && system_arguments[:select_variant] != :single
258
+ raise ArgumentError, "FilterableTreeView only supports `:multiple` or `:single` as select_variant"
259
+ end
260
+
261
+ if system_arguments[:select_variant] == :single
262
+ # In single selection, the include sub-items checkbox and the SegmentedControl make no sense
263
+ @include_sub_items_check_box_arguments[:hidden] = true
264
+ @include_sub_items_check_box_arguments[:checked] = false
265
+ @filter_mode_control_arguments[:hidden] = true
266
+ end
267
+
243
268
  @tree_view.with_leaf(
244
269
  **system_arguments,
245
- select_variant: :multiple,
246
270
  &block
247
271
  )
248
272
  end
@@ -5,8 +5,8 @@ module Primer
5
5
  module ViewComponents
6
6
  module VERSION
7
7
  MAJOR = 0
8
- MINOR = 73
9
- PATCH = 1
8
+ MINOR = 74
9
+ PATCH = 0
10
10
 
11
11
  STRING = [MAJOR, MINOR, PATCH].join(".")
12
12
  end
@@ -6,7 +6,7 @@
6
6
  <% icons.with_collapsed_icon(icon: :"file-directory-fill", color: :accent) %>
7
7
  <% end %>
8
8
 
9
- <% sub_tree.with_trailing_visual_icon(icon: :"diff-modified") %>
9
+ <% sub_tree.with_trailing_visual_icon(icon: :"diff-modified") unless select_variant == :single %>
10
10
 
11
11
  <% sub_tree.with_leaf(label: "button.rb", disabled: disabled, select_variant: select_variant) do |item| %>
12
12
  <% item.with_leading_visual_icon(icon: :file) %>
@@ -1,12 +1,12 @@
1
1
  <%= form_with(url: primer_view_components.generic_form_submission_path(format: :json)) do |f| %>
2
2
  <%= render(Primer::Alpha::Stack.new) do %>
3
3
  <%= render(Primer::Alpha::TreeView.new(form_arguments: { builder: f, name: "folder_structure" })) do |tree| %>
4
- <% tree.with_sub_tree(label: "src", expanded: expanded, select_variant: :multiple, value: 0) do |sub_tree| %>
5
- <% sub_tree.with_leaf(label: "button.rb", select_variant: :multiple, value: 1) %>
6
- <% sub_tree.with_leaf(label: "icon_button.rb", current: true, select_variant: :multiple, value: 2) %>
4
+ <% tree.with_sub_tree(label: "src", expanded: expanded, select_variant: select_variant, value: 0) do |sub_tree| %>
5
+ <% sub_tree.with_leaf(label: "button.rb", select_variant: select_variant, value: 1) %>
6
+ <% sub_tree.with_leaf(label: "icon_button.rb", current: true, select_variant: select_variant, value: 2) %>
7
7
  <% end %>
8
8
 
9
- <% tree.with_leaf(label: "action_menu.rb", select_variant: :multiple, value: 3) %>
9
+ <% tree.with_leaf(label: "action_menu.rb", select_variant: select_variant, value: 3) %>
10
10
  <% end %>
11
11
 
12
12
  <%= render(Primer::Alpha::SubmitButton.new(name: :submit, label: "Submit")) %>
@@ -0,0 +1,10 @@
1
+ <div style="max-width: 400px">
2
+ <%= render(Primer::Alpha::TreeView.new) do |tree_view| %>
3
+ <% tree_view.with_sub_tree(label: "src", expanded: expanded, disabled: disabled, select_variant: select_variant) do |sub_tree| %>
4
+ <% sub_tree.with_leaf(label: "button.rb", disabled: disabled, select_variant: select_variant) %>
5
+ <% sub_tree.with_leaf(label: "icon_button.rb", current: true, disabled: disabled, select_variant: select_variant) %>
6
+ <% end %>
7
+
8
+ <% tree_view.with_leaf(label: "action_menu.rb", disabled: disabled, select_variant: select_variant) %>
9
+ <% end %>
10
+ </div>
@@ -0,0 +1,10 @@
1
+ <div style="max-width: 400px">
2
+ <%= render(Primer::Alpha::TreeView.new) do |tree_view| %>
3
+ <% tree_view.with_sub_tree(label: "src", expanded: expanded, disabled: disabled, select_variant: select_variant) do |sub_tree| %>
4
+ <% sub_tree.with_leaf(label: "button.rb", disabled: disabled, select_variant: select_variant) %>
5
+ <% sub_tree.with_leaf(label: "icon_button.rb", current: true, disabled: disabled, select_variant: select_variant) %>
6
+ <% end %>
7
+
8
+ <% tree_view.with_leaf(label: "action_menu.rb", disabled: disabled, select_variant: select_variant) %>
9
+ <% end %>
10
+ </div>
@@ -9,7 +9,7 @@ module Primer
9
9
  # @snapshot interactive
10
10
  # @param expanded [Boolean] toggle
11
11
  # @param disabled [Boolean] toggle
12
- # @param select_variant [Symbol] select [multiple, none]
12
+ # @param select_variant [Symbol] select [multiple, single, none]
13
13
  # @param select_strategy [Symbol] select [self, descendants, mixed_descendants]
14
14
  def default(
15
15
  expanded: false,
@@ -28,7 +28,7 @@ module Primer
28
28
  # @label Playground
29
29
  #
30
30
  # @param expanded [Boolean] toggle
31
- # @param select_variant [Symbol] select [multiple, none]
31
+ # @param select_variant [Symbol] select [multiple, single, none]
32
32
  # @param select_strategy [Symbol] select [self, descendants, mixed_descendants]
33
33
  def playground(
34
34
  expanded: false,
@@ -43,6 +43,38 @@ module Primer
43
43
  })
44
44
  end
45
45
 
46
+ # @label Single select
47
+ #
48
+ # @snapshot interactive
49
+ # @param expanded [Boolean] toggle
50
+ # @param disabled [Boolean] toggle
51
+ def single_select(
52
+ expanded: false,
53
+ disabled: false
54
+ )
55
+ render_with_template(locals: {
56
+ expanded: coerce_bool(expanded),
57
+ disabled: coerce_bool(disabled),
58
+ select_variant: :single,
59
+ })
60
+ end
61
+
62
+ # @label Multi select
63
+ #
64
+ # @snapshot interactive
65
+ # @param expanded [Boolean] toggle
66
+ # @param disabled [Boolean] toggle
67
+ def multi_select(
68
+ expanded: false,
69
+ disabled: false
70
+ )
71
+ render_with_template(locals: {
72
+ expanded: coerce_bool(expanded),
73
+ disabled: coerce_bool(disabled),
74
+ select_variant: :multiple,
75
+ })
76
+ end
77
+
46
78
  # @label Empty
47
79
  #
48
80
  # @snapshot interactive
@@ -94,7 +126,7 @@ module Primer
94
126
  # @param leading_visual_icon [Symbol] octicon
95
127
  # @param leading_action_icon [Symbol] octicon
96
128
  # @param trailing_visual_icon [Symbol] octicon
97
- # @param select_variant [Symbol] select [multiple, none]
129
+ # @param select_variant [Symbol] select [multiple, single, none]
98
130
  # @param disabled [Boolean] toggle
99
131
  def leaf_node_playground(
100
132
  label: "Leaf node",
@@ -153,9 +185,11 @@ module Primer
153
185
 
154
186
  # @label Form input
155
187
  #
188
+ # @param select_variant [Symbol] select [multiple, single]
156
189
  # @param expanded [Boolean] toggle
157
- def form_input(expanded: true)
190
+ def form_input(select_variant: :multiple, expanded: true)
158
191
  render_with_template(locals: {
192
+ select_variant: select_variant.to_sym,
159
193
  expanded: coerce_bool(expanded)
160
194
  })
161
195
  end
@@ -1,30 +1,30 @@
1
1
  <%= form_with(url: primer_view_components.generic_form_submission_path(format: :json)) do |f| %>
2
2
  <%= render(Primer::Alpha::Stack.new) do %>
3
3
  <%= render(Primer::OpenProject::FilterableTreeView.new(form_arguments: { builder: f, name: "characters" })) do |tree| %>
4
- <% tree.with_sub_tree(label: "Students", expanded: expanded) do |hogwarts| %>
5
- <% hogwarts.with_sub_tree(label: "Ravenclaw", expanded: expanded) do |ravenclaw| %>
6
- <% ravenclaw.with_leaf(label: "Luna Lovegood") %>
4
+ <% tree.with_sub_tree(label: "Students", select_variant: select_variant, expanded: expanded) do |hogwarts| %>
5
+ <% hogwarts.with_sub_tree(label: "Ravenclaw", select_variant: select_variant, expanded: expanded) do |ravenclaw| %>
6
+ <% ravenclaw.with_leaf(label: "Luna Lovegood", select_variant: select_variant) %>
7
7
  <% end %>
8
8
 
9
- <% hogwarts.with_sub_tree(label: "Slytherin", expanded: expanded) do |hufflepuff| %>
10
- <% hufflepuff.with_leaf(label: "Draco Malfoy") %>
9
+ <% hogwarts.with_sub_tree(label: "Slytherin", select_variant: select_variant, expanded: expanded) do |hufflepuff| %>
10
+ <% hufflepuff.with_leaf(label: "Draco Malfoy", select_variant: select_variant) %>
11
11
  <% end %>
12
12
 
13
- <% hogwarts.with_sub_tree(label: "Hufflepuff", expanded: expanded) do |hufflepuff| %>
14
- <% hufflepuff.with_leaf(label: "Susan Bones") %>
13
+ <% hogwarts.with_sub_tree(label: "Hufflepuff", select_variant: select_variant, expanded: expanded) do |hufflepuff| %>
14
+ <% hufflepuff.with_leaf(label: "Susan Bones", select_variant: select_variant) %>
15
15
  <% end %>
16
16
 
17
- <% hogwarts.with_sub_tree(label: "Gryffindor", expanded: expanded) do |hufflepuff| %>
18
- <% hufflepuff.with_leaf(label: "Harry Potter") %>
19
- <% hufflepuff.with_leaf(label: "Ronald Weasley") %>
20
- <% hufflepuff.with_leaf(label: "Hermione Granger") %>
17
+ <% hogwarts.with_sub_tree(label: "Gryffindor", select_variant: select_variant, expanded: expanded) do |hufflepuff| %>
18
+ <% hufflepuff.with_leaf(label: "Harry Potter", select_variant: select_variant) %>
19
+ <% hufflepuff.with_leaf(label: "Ronald Weasley", select_variant: select_variant) %>
20
+ <% hufflepuff.with_leaf(label: "Hermione Granger", select_variant: select_variant) %>
21
21
  <% end %>
22
22
  <% end %>
23
23
 
24
- <% tree.with_leaf(label: "Albus Dumbledore") %>
25
- <% tree.with_leaf(label: "Minerva McGonagall") %>
26
- <% tree.with_leaf(label: "Severus Snape") %>
27
- <% tree.with_leaf(label: "Rubeus Hagrid") %>
24
+ <% tree.with_leaf(label: "Albus Dumbledore", select_variant: select_variant) %>
25
+ <% tree.with_leaf(label: "Minerva McGonagall", select_variant: select_variant) %>
26
+ <% tree.with_leaf(label: "Severus Snape", select_variant: select_variant) %>
27
+ <% tree.with_leaf(label: "Rubeus Hagrid", select_variant: select_variant) %>
28
28
  <% end %>
29
29
 
30
30
  <%= render(Primer::Alpha::SubmitButton.new(name: :submit, label: "Submit")) %>
@@ -2,28 +2,28 @@
2
2
  include_sub_items_check_box_arguments: { hidden: !show_checkbox },
3
3
  filter_mode_control_arguments: { hidden: !show_segmented_control }
4
4
  )) do |tree| %>
5
- <% tree.with_sub_tree(label: "Students", expanded: expanded) do |hogwarts| %>
6
- <% hogwarts.with_sub_tree(label: "Ravenclaw", expanded: expanded) do |ravenclaw| %>
7
- <% ravenclaw.with_leaf(label: "Luna Lovegood") %>
5
+ <% tree.with_sub_tree(label: "Students", select_variant: select_variant, expanded: expanded) do |hogwarts| %>
6
+ <% hogwarts.with_sub_tree(label: "Ravenclaw", select_variant: select_variant, expanded: expanded) do |ravenclaw| %>
7
+ <% ravenclaw.with_leaf(label: "Luna Lovegood", select_variant: select_variant) %>
8
8
  <% end %>
9
9
 
10
- <% hogwarts.with_sub_tree(label: "Slytherin", expanded: expanded) do |hufflepuff| %>
11
- <% hufflepuff.with_leaf(label: "Draco Malfoy") %>
10
+ <% hogwarts.with_sub_tree(label: "Slytherin", select_variant: select_variant, expanded: expanded) do |hufflepuff| %>
11
+ <% hufflepuff.with_leaf(label: "Draco Malfoy", select_variant: select_variant) %>
12
12
  <% end %>
13
13
 
14
- <% hogwarts.with_sub_tree(label: "Hufflepuff", expanded: expanded) do |hufflepuff| %>
15
- <% hufflepuff.with_leaf(label: "Susan Bones") %>
14
+ <% hogwarts.with_sub_tree(label: "Hufflepuff", select_variant: select_variant, expanded: expanded) do |hufflepuff| %>
15
+ <% hufflepuff.with_leaf(label: "Susan Bones", select_variant: select_variant) %>
16
16
  <% end %>
17
17
 
18
- <% hogwarts.with_sub_tree(label: "Gryffindor", expanded: expanded) do |hufflepuff| %>
19
- <% hufflepuff.with_leaf(label: "Harry Potter") %>
20
- <% hufflepuff.with_leaf(label: "Ronald Weasley") %>
21
- <% hufflepuff.with_leaf(label: "Hermione Granger") %>
18
+ <% hogwarts.with_sub_tree(label: "Gryffindor", select_variant: select_variant, expanded: expanded) do |hufflepuff| %>
19
+ <% hufflepuff.with_leaf(label: "Harry Potter", select_variant: select_variant) %>
20
+ <% hufflepuff.with_leaf(label: "Ronald Weasley", select_variant: select_variant) %>
21
+ <% hufflepuff.with_leaf(label: "Hermione Granger", select_variant: select_variant) %>
22
22
  <% end %>
23
23
  <% end %>
24
24
 
25
- <% tree.with_leaf(label: "Albus Dumbledore") %>
26
- <% tree.with_leaf(label: "Minerva McGonagall") %>
27
- <% tree.with_leaf(label: "Severus Snape") %>
28
- <% tree.with_leaf(label: "Rubeus Hagrid") %>
25
+ <% tree.with_leaf(label: "Albus Dumbledore", select_variant: select_variant) %>
26
+ <% tree.with_leaf(label: "Minerva McGonagall", select_variant: select_variant) %>
27
+ <% tree.with_leaf(label: "Severus Snape", select_variant: select_variant) %>
28
+ <% tree.with_leaf(label: "Rubeus Hagrid", select_variant: select_variant) %>
29
29
  <% end %>
@@ -7,11 +7,13 @@ module Primer
7
7
  # @label Playground
8
8
  #
9
9
  # @param expanded [Boolean] toggle
10
+ # @param select_variant [Symbol] select [multiple, single]
10
11
  # @param show_checkbox [Boolean] toggle
11
12
  # @param show_segmented_control [Boolean] toggle
12
- def playground(expanded: true, show_checkbox: true, show_segmented_control: true)
13
+ def playground(expanded: true, select_variant: :multiple, show_checkbox: true, show_segmented_control: true)
13
14
  render_with_template(locals: {
14
15
  expanded: coerce_bool(expanded),
16
+ select_variant: select_variant.to_sym,
15
17
  show_checkbox: coerce_bool(show_checkbox),
16
18
  show_segmented_control: coerce_bool(show_segmented_control)
17
19
  })
@@ -29,9 +31,11 @@ module Primer
29
31
 
30
32
  # @label Form input
31
33
  #
34
+ # @param select_variant [Symbol] select [multiple, single]
32
35
  # @param expanded [Boolean] toggle
33
- def form_input(expanded: true)
36
+ def form_input(select_variant: :multiple, expanded: true)
34
37
  render_with_template(locals: {
38
+ select_variant: select_variant.to_sym,
35
39
  expanded: coerce_bool(expanded)
36
40
  })
37
41
  end
@@ -3529,7 +3529,7 @@
3529
3529
  "name": "select_variant",
3530
3530
  "type": "Symbol",
3531
3531
  "default": "`:none`",
3532
- "description": "Controls the type of checkbox that appears. One of `:multiple` or `:none`."
3532
+ "description": "Controls the type of checkbox that appears. One of `:multiple`, `:none`, or `:single`."
3533
3533
  },
3534
3534
  {
3535
3535
  "name": "checked",
@@ -875,6 +875,7 @@
875
875
  "anchor"
876
876
  ],
877
877
  "SELECT_VARIANT_OPTIONS": [
878
+ "single",
878
879
  "multiple",
879
880
  "none"
880
881
  ]
@@ -10428,7 +10428,7 @@
10428
10428
  },
10429
10429
  {
10430
10430
  "fully_qualified_name": "Primer::Alpha::TreeView",
10431
- "description": "TreeView is a hierarchical list of items that may have a parent-child relationship where children\ncan be toggled into view by expanding or collapsing their parent item.\n\n## Terminology\n\nConsider the following tree structure:\n\nsrc\n├ button.rb\n└ action_list\n ├ item.rb\n └ header.rb\n\n1. **Node**. A node is an item in the tree. Nodes can either be \"leaf\" nodes (i.e. have no children), or \"sub-tree\"\nnodes, which do have children. In the example above, button.rb, item.rb, and header.rb are all leaf nodes, while\naction_list is a sub-tree node.\n2. **Path**. A node's path is like its ID. It's an array of strings containing the current node's label and all the\nlabels of its ancestors, in order. In the example above, header.rb's path is [\"src\", \"action_list\", \"header.rb\"].\n\n## Static nodes\n\nThe `TreeView` component allows items to be provided statically or loaded dynamically from the server.\nProviding items statically is done using the `leaf` and `sub_tree` slots:\n\n```erb\n<%= render(Primer::Alpha::TreeView.new) do |tree| %>\n <% tree.with_sub_tree(label: \"Directory\") do |sub_tree| %>\n <% sub_tree.with_leaf(label: \"File 1\")\n <% end %>\n <% tree.with_leaf(label: \"File 2\") %>\n<% end %>\n```\n\n## Dynamic nodes\n\nTree nodes can also be fetched dynamically from the server and will require creating a Rails controller action\nto respond with the list of nodes. Unlike other Primer components, `TreeView` allows the programmer to specify\nloading behavior on a per-sub-tree basis, i.e. each sub-tree must specify how its nodes are loaded. To load nodes\ndynamically for a given sub-tree, configure it with either a loading spinner or a loading skeleton, and provide\nthe URL to fetch nodes from:\n\n```erb\n<%= render(Primer::Alpha::TreeView.new) do |tree| %>\n <% tree.with_sub_tree(label: \"Directory\") do |sub_tree| %>\n <% sub_tree.with_loading_spinner(src: primer_view_components.tree_view_items_path) %>\n <% end %>\n<% end %>\n```\n\nDefine a controller action to serve the list of nodes. The `TreeView` component automatically includes the\nsub-tree's path as a GET parameter, encoded as a JSON array.\n\n```ruby\nclass TreeViewItemsController < ApplicationController\n def show\n @path = JSON.parse(params[:path])\n @results = get_tree_items(starting_at: path)\n end\nend\n```\n\nResponses must be HTML fragments, eg. have a content type of `text/html+fragment`. This content type isn't\navailable by default in Rails, so you may have to register it eg. in an initializer:\n\n```ruby\nMime::Type.register(\"text/fragment+html\", :html_fragment)\n```\n\nRender a `Primer::Alpha::TreeView::SubTree` in the action's template, tree_view_items/show.html_fragment.erb:\n\n```erb\n<%= render(Primer::Alpha::TreeView::SubTree.new(path: @path, node_variant: :div)) do |tree| %>\n <% tree.with_leaf(...) %>\n <% tree.with_sub_tree(...) do |sub_tree| %>\n ...\n <% end %>\n<% end %>\n```\n\n## Multi-select mode\n\nPassing `select_variant: :multiple` to both sub-tree and leaf nodes will add a check box to the left of the node's\nlabel. These check boxes behave according to the value of a second argument, `select_strategy:`.\n\nThe default select strategy, `:descendants`, will cause all child nodes to be checked when the node is checked.\nThis includes both sub-tree and leaf nodes. When the node is unchecked, all child nodes will also be unchecked.\nUnchecking a child node of a checked parent will cause the parent to enter a mixed or indeterminate state, which\nis represented by a horizontal line icon instead of a check mark. This icon indicates that some children are\nchecked, but not all.\n\nA secondary select strategy, `:self`, is provided to allow disabling the automatic checking of child nodes. When\n`select_strategy: :self` is specified, checking sub-tree nodes does not check child nodes, and sub-tree nodes\ncannot enter a mixed or indeterminate state.\n\nNodes can be checked via the keyboard by pressing the space key.\n\n## Node tags\n\n`TreeView`s support three different node variants, `:anchor`, `:button`, and `:div` (the default), which controls\nwhich HTML tag is used to construct the nodes. The `:anchor` and `:button` variants correspond to `<a>` and\n`<button>` tags respectively, which are browser-native elements. Anchors and buttons can be activated (i.e.\n\"clicked\") using the mouse or keyboard via the enter or space keys. The node variant must be the same for all\nnodes in the tree, and is therefore specified at the root level, eg. `TreeView.new(node_variant: :anchor)`.\n\nTrees with node variants other than `:div` cannot have check boxes, i.e. cannot be put into multi-select mode.\n\nTrees with node variants other than `:div` do not emit the `treeViewNodeActivated` or `treeViewBeforeNodeActivated`\nevents, since it is assumed any behavior associated with these variants is user- or browser-defined.\n\n## Interaction behavior matrix\n\n|Interaction |Select variant|Tag |Result |\n|:---------------|:-------------|:------------|:--------------------------|\n|Enter/space |none |div |Expands/collapses |\n|Enter/space |none |anchor/button|Activates anchor/button |\n|Enter/space |multiple |div |Checks or unchecks |\n|Enter/space |multiple |anchor/button|N/A (not allowed) |\n|Left/right arrow|none |div |Expands/collapses |\n|Left/right arrow|none |anchor/button|Expands/collapses |\n|Left/right arrow|multiple |div |Expands/collapses |\n|Left/right arrow|multiple |anchor/button|N/A (not allowed) |\n|Click |none |div |Expands/collapses |\n|Click |multiple |div |Checks or unchecks |\n|Click |multiple |anchor/button|N/A (not allowed) |\n\n## JavaScript API\n\n`TreeView`s render a `<tree-view>` custom element that exposes behavior to the client.\n\n|Name |Notes |\n|:-----------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------|\n|`getNodePath(node: Element): string[]` |Returns the path to the given node. |\n|`getNodeType(node: Element): TreeViewNodeType | null` |Returns either `\"leaf\"` or `\"sub-tree\"`. |\n|`markCurrentAtPath(path: string[])` |Marks the node as the \"current\" node, which appears visually distinct from other nodes. |\n|`get currentNode(): HTMLLIElement | null` |Returns the current node. |\n|`expandAtPath(path: string[])` |Expands the sub-tree at `path`. |\n|`collapseAtPath(path: string[])` |Collapses the sub-tree at `path`. |\n|`toggleAtPath(path: string[])` |If the sub-tree at `path` is collapsed, this function expands it, and vice-versa. |\n|`checkAtPath(path: string[])` |If the node at `path` has a checkbox, this function checks it. |\n|`uncheckAtPath(path: string[])` |If the node at `path` has a checkbox, this function unchecks it. |\n|`toggleCheckedAtPath(path: string[])` |If the sub-tree at `path` is checked, this function unchecks it, and vice-versa. |\n|`checkedValueAtPath(path: string[]): TreeViewCheckedValue` |Returns `\"true\"` (all child nodes are checked), `\"false\"` (no child nodes are checked), or `\"mixed\"` (some child nodes are checked, some are not).|\n|`nodeAtPath(path: string[], selector?: string): Element | null` |Returns the node for the given `path`, either a leaf node or sub-tree node. |\n|`subTreeAtPath(path: string[]): TreeViewSubTreeNodeElement | null`|Returns the sub-tree at the given `path`, if it exists. |\n|`leafAtPath(path: string[]): HTMLLIElement | null` |Returns the leaf node at the given `path`, if it exists. |\n|`getNodeCheckedValue(node: Element): TreeViewCheckedValue` |The same as `checkedValueAtPath`, but accepts a node instead of a path. |\n\n### Events\n\nThe events enumerated below include node information by way of the `TreeViewNodeInfo` object, which has the\nfollowing signature:\n\n```typescript\ntype TreeViewNodeType = 'leaf' | 'sub-tree'\ntype TreeViewCheckedValue = 'true' | 'false' | 'mixed'\n\ntype TreeViewNodeInfo = {\n node: Element\n type: TreeViewNodeType\n path: string[]\n checkedValue: TreeViewCheckedValue\n previousCheckedValue: TreeViewCheckedValue\n}\n```\n\n|Name |Type |Bubbles |Cancelable |\n|:----------------------------|:------------------------------------------|:-------|:----------|\n|`treeViewNodeActivated` |`CustomEvent<TreeViewNodeInfo>` |Yes |No |\n|`treeViewBeforeNodeActivated`|`CustomEvent<TreeViewNodeInfo>` |Yes |Yes |\n|`treeViewNodeExpanded` |`CustomEvent<TreeViewNodeInfo>>` |Yes |No |\n|`treeViewNodeCollapsed` |`CustomEvent<TreeViewNodeInfo>>` |Yes |No |\n|`treeViewNodeChecked` |`CustomEvent<TreeViewNodeInfo[]>` |Yes |Yes |\n|`treeViewBeforeNodeChecked` |`CustomEvent<TreeViewNodeInfo[]>` |Yes |No |\n\n_Item activation_\n\nThe `<tree-view>` element fires an `treeViewNodeActivated` event whenever a node is activated (eg. clicked)\nvia the mouse or keyboard.\n\nThe `treeViewBeforeNodeActivated` event fires before a node is activated. Canceling this event will prevent the\nnode from being activated.\n\n```typescript\ndocument.querySelector(\"select-panel\").addEventListener(\n \"treeViewBeforeNodeActivated\",\n (event: CustomEvent<TreeViewNodeInfo>) => {\n event.preventDefault() // Cancel the event to prevent activation (eg. expanding/collapsing)\n }\n)\n```\n\n_Item checking/unchecking_\n\nThe `tree-view` element fires a `treeViewNodeChecked` event whenever a node is checked or unchecked.\n\nThe `treeViewBeforeNodeChecked` event fires before a node is checked or unchecked. Canceling this event will\nprevent the check/uncheck operation.\n\n```typescript\ndocument.querySelector(\"select-panel\").addEventListener(\n \"treeViewBeforeNodeChecked\",\n (event: CustomEvent<TreeViewNodeInfo[]>) => {\n event.preventDefault() // Cancel the event to prevent activation (eg. expanding/collapsing)\n }\n)\n```\n\nBecause checking or unchecking a sub-tree results in the checking or unchecking of all its children recursively,\nboth the `treeViewNodeChecked` and `treeViewBeforeNodeChecked` events provide an array of `TreeViewNodeInfo`\nobjects, which contain entries for every modified node in the tree.",
10431
+ "description": "TreeView is a hierarchical list of items that may have a parent-child relationship where children\ncan be toggled into view by expanding or collapsing their parent item.\n\n## Terminology\n\nConsider the following tree structure:\n\nsrc\n├ button.rb\n└ action_list\n ├ item.rb\n └ header.rb\n\n1. **Node**. A node is an item in the tree. Nodes can either be \"leaf\" nodes (i.e. have no children), or \"sub-tree\"\nnodes, which do have children. In the example above, button.rb, item.rb, and header.rb are all leaf nodes, while\naction_list is a sub-tree node.\n2. **Path**. A node's path is like its ID. It's an array of strings containing the current node's label and all the\nlabels of its ancestors, in order. In the example above, header.rb's path is [\"src\", \"action_list\", \"header.rb\"].\n\n## Static nodes\n\nThe `TreeView` component allows items to be provided statically or loaded dynamically from the server.\nProviding items statically is done using the `leaf` and `sub_tree` slots:\n\n```erb\n<%= render(Primer::Alpha::TreeView.new) do |tree| %>\n <% tree.with_sub_tree(label: \"Directory\") do |sub_tree| %>\n <% sub_tree.with_leaf(label: \"File 1\")\n <% end %>\n <% tree.with_leaf(label: \"File 2\") %>\n<% end %>\n```\n\n## Dynamic nodes\n\nTree nodes can also be fetched dynamically from the server and will require creating a Rails controller action\nto respond with the list of nodes. Unlike other Primer components, `TreeView` allows the programmer to specify\nloading behavior on a per-sub-tree basis, i.e. each sub-tree must specify how its nodes are loaded. To load nodes\ndynamically for a given sub-tree, configure it with either a loading spinner or a loading skeleton, and provide\nthe URL to fetch nodes from:\n\n```erb\n<%= render(Primer::Alpha::TreeView.new) do |tree| %>\n <% tree.with_sub_tree(label: \"Directory\") do |sub_tree| %>\n <% sub_tree.with_loading_spinner(src: primer_view_components.tree_view_items_path) %>\n <% end %>\n<% end %>\n```\n\nDefine a controller action to serve the list of nodes. The `TreeView` component automatically includes the\nsub-tree's path as a GET parameter, encoded as a JSON array.\n\n```ruby\nclass TreeViewItemsController < ApplicationController\n def show\n @path = JSON.parse(params[:path])\n @results = get_tree_items(starting_at: path)\n end\nend\n```\n\nResponses must be HTML fragments, eg. have a content type of `text/html+fragment`. This content type isn't\navailable by default in Rails, so you may have to register it eg. in an initializer:\n\n```ruby\nMime::Type.register(\"text/fragment+html\", :html_fragment)\n```\n\nRender a `Primer::Alpha::TreeView::SubTree` in the action's template, tree_view_items/show.html_fragment.erb:\n\n```erb\n<%= render(Primer::Alpha::TreeView::SubTree.new(path: @path, node_variant: :div)) do |tree| %>\n <% tree.with_leaf(...) %>\n <% tree.with_sub_tree(...) do |sub_tree| %>\n ...\n <% end %>\n<% end %>\n```\n\n## Multi-select mode\n\nPassing `select_variant: :multiple` to both sub-tree and leaf nodes will add a check box to the left of the node's\nlabel. These check boxes behave according to the value of a second argument, `select_strategy:`.\n\nThe default select strategy, `:descendants`, will cause all child nodes to be checked when the node is checked.\nThis includes both sub-tree and leaf nodes. When the node is unchecked, all child nodes will also be unchecked.\nUnchecking a child node of a checked parent will cause the parent to enter a mixed or indeterminate state, which\nis represented by a horizontal line icon instead of a check mark. This icon indicates that some children are\nchecked, but not all.\n\nA secondary select strategy, `:self`, is provided to allow disabling the automatic checking of child nodes. When\n`select_strategy: :self` is specified, checking sub-tree nodes does not check child nodes, and sub-tree nodes\ncannot enter a mixed or indeterminate state.\n\nNodes can be checked via the keyboard by pressing the space key.\n\n## Single-select mode\n\nBy passing `select_variant: :single` to both sub-tree and leaf nodes:\n- Nodes become selectable and can be toggled via keyboard (space key).\n- A selected node displays a checkmark at the end of the line.\nNote: This checkmark conflicts with the `trailing_visual_icon` slot,\nso both cannot be used simultaneously.\n\n## Node tags\n\n`TreeView`s support three different node variants, `:anchor`, `:button`, and `:div` (the default), which controls\nwhich HTML tag is used to construct the nodes. The `:anchor` and `:button` variants correspond to `<a>` and\n`<button>` tags respectively, which are browser-native elements. Anchors and buttons can be activated (i.e.\n\"clicked\") using the mouse or keyboard via the enter or space keys. The node variant must be the same for all\nnodes in the tree, and is therefore specified at the root level, eg. `TreeView.new(node_variant: :anchor)`.\n\nTrees with node variants other than `:div` cannot have check boxes, i.e. cannot be put into multi-select mode.\n\nTrees with node variants other than `:div` do not emit the `treeViewNodeActivated` or `treeViewBeforeNodeActivated`\nevents, since it is assumed any behavior associated with these variants is user- or browser-defined.\n\n## Interaction behavior matrix\n\n|Interaction |Select variant|Tag |Result |\n|:---------------|:-------------|:------------|:--------------------------|\n|Enter/space |none |div |Expands/collapses |\n|Enter/space |none |anchor/button|Activates anchor/button |\n|Enter/space |single |div |Selects |\n|Enter/space |single |anchor/button|N/A (not allowed) |\n|Enter/space |multiple |div |Checks or unchecks |\n|Enter/space |multiple |anchor/button|N/A (not allowed) |\n|Left/right arrow|none |div |Expands/collapses |\n|Left/right arrow|none |anchor/button|Expands/collapses |\n|Left/right arrow|single |div |Expands/collapses |\n|Left/right arrow|single |anchor/button|N/A (not allowed) |\n|Left/right arrow|multiple |div |Expands/collapses |\n|Left/right arrow|multiple |anchor/button|N/A (not allowed) |\n|Click |none |div |Expands/collapses |\n|Click |single |div |Selects |\n|Click |single |anchor/button|N/A (not allowed) |\n|Click |multiple |div |Checks or unchecks |\n|Click |multiple |anchor/button|N/A (not allowed) |\n\n## JavaScript API\n\n`TreeView`s render a `<tree-view>` custom element that exposes behavior to the client.\n\n|Name |Notes |\n|:-----------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------|\n|`getNodePath(node: Element): string[]` |Returns the path to the given node. |\n|`getNodeType(node: Element): TreeViewNodeType | null` |Returns either `\"leaf\"` or `\"sub-tree\"`. |\n|`markCurrentAtPath(path: string[])` |Marks the node as the \"current\" node, which appears visually distinct from other nodes. |\n|`get currentNode(): HTMLLIElement | null` |Returns the current node. |\n|`expandAtPath(path: string[])` |Expands the sub-tree at `path`. |\n|`collapseAtPath(path: string[])` |Collapses the sub-tree at `path`. |\n|`toggleAtPath(path: string[])` |If the sub-tree at `path` is collapsed, this function expands it, and vice-versa. |\n|`checkAtPath(path: string[])` |If the node at `path` has a checkbox, this function checks it. |\n|`uncheckAtPath(path: string[])` |If the node at `path` has a checkbox, this function unchecks it. |\n|`toggleCheckedAtPath(path: string[])` |If the sub-tree at `path` is checked, this function unchecks it, and vice-versa. |\n|`checkedValueAtPath(path: string[]): TreeViewCheckedValue` |Returns `\"true\"` (all child nodes are checked), `\"false\"` (no child nodes are checked), or `\"mixed\"` (some child nodes are checked, some are not).|\n|`nodeAtPath(path: string[], selector?: string): Element | null` |Returns the node for the given `path`, either a leaf node or sub-tree node. |\n|`subTreeAtPath(path: string[]): TreeViewSubTreeNodeElement | null`|Returns the sub-tree at the given `path`, if it exists. |\n|`leafAtPath(path: string[]): HTMLLIElement | null` |Returns the leaf node at the given `path`, if it exists. |\n|`getNodeCheckedValue(node: Element): TreeViewCheckedValue` |The same as `checkedValueAtPath`, but accepts a node instead of a path. |\n\n### Events\n\nThe events enumerated below include node information by way of the `TreeViewNodeInfo` object, which has the\nfollowing signature:\n\n```typescript\ntype TreeViewNodeType = 'leaf' | 'sub-tree'\ntype TreeViewCheckedValue = 'true' | 'false' | 'mixed'\n\ntype TreeViewNodeInfo = {\n node: Element\n type: TreeViewNodeType\n path: string[]\n checkedValue: TreeViewCheckedValue\n previousCheckedValue: TreeViewCheckedValue\n}\n```\n\n|Name |Type |Bubbles |Cancelable |\n|:----------------------------|:------------------------------------------|:-------|:----------|\n|`treeViewNodeActivated` |`CustomEvent<TreeViewNodeInfo>` |Yes |No |\n|`treeViewBeforeNodeActivated`|`CustomEvent<TreeViewNodeInfo>` |Yes |Yes |\n|`treeViewNodeExpanded` |`CustomEvent<TreeViewNodeInfo>>` |Yes |No |\n|`treeViewNodeCollapsed` |`CustomEvent<TreeViewNodeInfo>>` |Yes |No |\n|`treeViewNodeChecked` |`CustomEvent<TreeViewNodeInfo[]>` |Yes |Yes |\n|`treeViewBeforeNodeChecked` |`CustomEvent<TreeViewNodeInfo[]>` |Yes |No |\n\n_Item activation_\n\nThe `<tree-view>` element fires an `treeViewNodeActivated` event whenever a node is activated (eg. clicked)\nvia the mouse or keyboard.\n\nThe `treeViewBeforeNodeActivated` event fires before a node is activated. Canceling this event will prevent the\nnode from being activated.\n\n```typescript\ndocument.querySelector(\"select-panel\").addEventListener(\n \"treeViewBeforeNodeActivated\",\n (event: CustomEvent<TreeViewNodeInfo>) => {\n event.preventDefault() // Cancel the event to prevent activation (eg. expanding/collapsing)\n }\n)\n```\n\n_Item checking/unchecking_\n\nThe `tree-view` element fires a `treeViewNodeChecked` event whenever a node is checked or unchecked.\n\nThe `treeViewBeforeNodeChecked` event fires before a node is checked or unchecked. Canceling this event will\nprevent the check/uncheck operation.\n\n```typescript\ndocument.querySelector(\"select-panel\").addEventListener(\n \"treeViewBeforeNodeChecked\",\n (event: CustomEvent<TreeViewNodeInfo[]>) => {\n event.preventDefault() // Cancel the event to prevent activation (eg. expanding/collapsing)\n }\n)\n```\n\nBecause checking or unchecking a sub-tree may result in the checking or unchecking of all its children recursively,\nboth the `treeViewNodeChecked` and `treeViewBeforeNodeChecked` events provide an array of `TreeViewNodeInfo`\nobjects, which contain entries for every modified node in the tree.",
10432
10432
  "accessibility_docs": null,
10433
10433
  "is_form_component": false,
10434
10434
  "is_published": true,
@@ -10539,6 +10539,32 @@
10539
10539
  ]
10540
10540
  }
10541
10541
  },
10542
+ {
10543
+ "preview_path": "primer/alpha/tree_view/single_select",
10544
+ "name": "single_select",
10545
+ "snapshot": "interactive",
10546
+ "skip_rules": {
10547
+ "wont_fix": [
10548
+ "region"
10549
+ ],
10550
+ "will_fix": [
10551
+ "color-contrast"
10552
+ ]
10553
+ }
10554
+ },
10555
+ {
10556
+ "preview_path": "primer/alpha/tree_view/multi_select",
10557
+ "name": "multi_select",
10558
+ "snapshot": "interactive",
10559
+ "skip_rules": {
10560
+ "wont_fix": [
10561
+ "region"
10562
+ ],
10563
+ "will_fix": [
10564
+ "color-contrast"
10565
+ ]
10566
+ }
10567
+ },
10542
10568
  {
10543
10569
  "preview_path": "primer/alpha/tree_view/empty",
10544
10570
  "name": "empty",
@@ -11016,7 +11042,7 @@
11016
11042
  "name": "select_variant",
11017
11043
  "type": "Symbol",
11018
11044
  "default": "`:none`",
11019
- "description": "Controls the type of checkbox that appears. One of `:multiple` or `:none`."
11045
+ "description": "Controls the type of checkbox that appears. One of `:multiple`, `:none`, or `:single`."
11020
11046
  },
11021
11047
  {
11022
11048
  "name": "checked",
data/static/previews.json CHANGED
@@ -9113,6 +9113,32 @@
9113
9113
  ]
9114
9114
  }
9115
9115
  },
9116
+ {
9117
+ "preview_path": "primer/alpha/tree_view/single_select",
9118
+ "name": "single_select",
9119
+ "snapshot": "interactive",
9120
+ "skip_rules": {
9121
+ "wont_fix": [
9122
+ "region"
9123
+ ],
9124
+ "will_fix": [
9125
+ "color-contrast"
9126
+ ]
9127
+ }
9128
+ },
9129
+ {
9130
+ "preview_path": "primer/alpha/tree_view/multi_select",
9131
+ "name": "multi_select",
9132
+ "snapshot": "interactive",
9133
+ "skip_rules": {
9134
+ "wont_fix": [
9135
+ "region"
9136
+ ],
9137
+ "will_fix": [
9138
+ "color-contrast"
9139
+ ]
9140
+ }
9141
+ },
9116
9142
  {
9117
9143
  "preview_path": "primer/alpha/tree_view/empty",
9118
9144
  "name": "empty",
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.73.1
4
+ version: 0.74.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-09-30 00:00:00.000000000 Z
12
+ date: 2025-10-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionview
@@ -1104,7 +1104,9 @@ files:
1104
1104
  - previews/primer/alpha/tree_view_preview/loading_failure.html.erb
1105
1105
  - previews/primer/alpha/tree_view_preview/loading_skeleton.html.erb
1106
1106
  - previews/primer/alpha/tree_view_preview/loading_spinner.html.erb
1107
+ - previews/primer/alpha/tree_view_preview/multi_select.html.erb
1107
1108
  - previews/primer/alpha/tree_view_preview/playground.html.erb
1109
+ - previews/primer/alpha/tree_view_preview/single_select.html.erb
1108
1110
  - previews/primer/alpha/underline_nav_preview.rb
1109
1111
  - previews/primer/alpha/underline_nav_preview/default.html.erb
1110
1112
  - previews/primer/alpha/underline_nav_preview/playground.html.erb