openproject-primer_view_components 0.64.1 → 0.66.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +29 -0
  4. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_icon_pair_element.d.ts +15 -0
  5. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_include_fragment_element.d.ts +9 -0
  6. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_roving_tab_index.d.ts +3 -0
  7. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +38 -0
  8. data/app/assets/javascripts/components/primer/primer.d.ts +4 -0
  9. data/app/assets/javascripts/components/primer/shared_events.d.ts +15 -0
  10. data/app/assets/javascripts/primer_view_components.js +1 -1
  11. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  12. data/app/assets/styles/primer_view_components.css +1 -1
  13. data/app/assets/styles/primer_view_components.css.map +1 -1
  14. data/app/components/primer/alpha/select_panel.css +1 -1
  15. data/app/components/primer/alpha/select_panel.css.json +2 -2
  16. data/app/components/primer/alpha/select_panel.css.map +1 -1
  17. data/app/components/primer/alpha/select_panel.html.erb +1 -1
  18. data/app/components/primer/alpha/select_panel.pcss +5 -2
  19. data/app/components/primer/beta/spinner.html.erb +1 -1
  20. data/app/components/primer/beta/spinner.rb +2 -0
  21. data/app/components/primer/open_project/file_tree_view/directory_node.html.erb +5 -0
  22. data/app/components/primer/open_project/file_tree_view/directory_node.rb +24 -0
  23. data/app/components/primer/open_project/file_tree_view/file_node.html.erb +2 -0
  24. data/app/components/primer/open_project/file_tree_view/file_node.rb +14 -0
  25. data/app/components/primer/open_project/file_tree_view.rb +15 -0
  26. data/app/components/primer/open_project/page_header.rb +1 -1
  27. data/app/components/primer/open_project/skeleton_box.css +1 -0
  28. data/app/components/primer/open_project/skeleton_box.css.json +6 -0
  29. data/app/components/primer/open_project/skeleton_box.css.map +1 -0
  30. data/app/components/primer/open_project/skeleton_box.html.erb +1 -0
  31. data/app/components/primer/open_project/skeleton_box.pcss +30 -0
  32. data/app/components/primer/open_project/skeleton_box.rb +27 -0
  33. data/app/components/primer/open_project/sub_header/button.rb +43 -0
  34. data/app/components/primer/open_project/sub_header/button_group.rb +16 -0
  35. data/app/components/primer/open_project/sub_header/menu.rb +67 -0
  36. data/app/components/primer/open_project/sub_header/segmented_control.rb +16 -0
  37. data/app/components/primer/open_project/sub_header.css +1 -1
  38. data/app/components/primer/open_project/sub_header.css.json +4 -1
  39. data/app/components/primer/open_project/sub_header.css.map +1 -1
  40. data/app/components/primer/open_project/sub_header.html.erb +21 -0
  41. data/app/components/primer/open_project/sub_header.pcss +29 -3
  42. data/app/components/primer/open_project/sub_header.rb +105 -21
  43. data/app/components/primer/open_project/tree_view/icon.html.erb +1 -0
  44. data/app/components/primer/open_project/tree_view/icon.rb +22 -0
  45. data/app/components/primer/open_project/tree_view/icon_pair.html.erb +13 -0
  46. data/app/components/primer/open_project/tree_view/icon_pair.rb +42 -0
  47. data/app/components/primer/open_project/tree_view/leading_action.html.erb +3 -0
  48. data/app/components/primer/open_project/tree_view/leading_action.rb +18 -0
  49. data/app/components/primer/open_project/tree_view/leaf_node.html.erb +18 -0
  50. data/app/components/primer/open_project/tree_view/leaf_node.rb +96 -0
  51. data/app/components/primer/open_project/tree_view/loading_failure_message.html.erb +13 -0
  52. data/app/components/primer/open_project/tree_view/loading_failure_message.rb +31 -0
  53. data/app/components/primer/open_project/tree_view/node.html.erb +32 -0
  54. data/app/components/primer/open_project/tree_view/node.rb +155 -0
  55. data/app/components/primer/open_project/tree_view/skeleton_loader.html.erb +23 -0
  56. data/app/components/primer/open_project/tree_view/skeleton_loader.rb +36 -0
  57. data/app/components/primer/open_project/tree_view/spinner_loader.html.erb +20 -0
  58. data/app/components/primer/open_project/tree_view/spinner_loader.rb +33 -0
  59. data/app/components/primer/open_project/tree_view/sub_tree.html.erb +21 -0
  60. data/app/components/primer/open_project/tree_view/sub_tree.rb +106 -0
  61. data/app/components/primer/open_project/tree_view/sub_tree_container.html.erb +3 -0
  62. data/app/components/primer/open_project/tree_view/sub_tree_container.rb +39 -0
  63. data/app/components/primer/open_project/tree_view/sub_tree_node.html.erb +49 -0
  64. data/app/components/primer/open_project/tree_view/sub_tree_node.rb +172 -0
  65. data/app/components/primer/open_project/tree_view/tree_view.d.ts +29 -0
  66. data/app/components/primer/open_project/tree_view/tree_view.js +238 -0
  67. data/app/components/primer/open_project/tree_view/tree_view.ts +257 -0
  68. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.d.ts +15 -0
  69. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.js +62 -0
  70. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.ts +56 -0
  71. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.d.ts +9 -0
  72. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.js +29 -0
  73. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.ts +29 -0
  74. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.d.ts +3 -0
  75. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.js +126 -0
  76. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.ts +156 -0
  77. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +38 -0
  78. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +362 -0
  79. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +402 -0
  80. data/app/components/primer/open_project/tree_view/visual.html.erb +14 -0
  81. data/app/components/primer/open_project/tree_view/visual.rb +27 -0
  82. data/app/components/primer/open_project/tree_view.css +1 -0
  83. data/app/components/primer/open_project/tree_view.css.json +42 -0
  84. data/app/components/primer/open_project/tree_view.css.map +1 -0
  85. data/app/components/primer/open_project/tree_view.html.erb +7 -0
  86. data/app/components/primer/open_project/tree_view.pcss +319 -0
  87. data/app/components/primer/open_project/tree_view.rb +367 -0
  88. data/app/components/primer/primer.d.ts +4 -0
  89. data/app/components/primer/primer.js +4 -0
  90. data/app/components/primer/primer.pcss +2 -0
  91. data/app/components/primer/primer.ts +4 -0
  92. data/app/components/primer/shared_events.d.ts +15 -0
  93. data/app/components/primer/shared_events.ts +19 -0
  94. data/app/lib/primer/forms/acts_as_component.rb +1 -12
  95. data/lib/primer/view_components/version.rb +2 -2
  96. data/previews/primer/open_project/file_tree_view_preview/default.html.erb +16 -0
  97. data/previews/primer/open_project/file_tree_view_preview/playground.html.erb +4 -0
  98. data/previews/primer/open_project/file_tree_view_preview.rb +69 -0
  99. data/previews/primer/open_project/page_header_preview/create_action.html.erb +1 -2
  100. data/previews/primer/open_project/skeleton_box_preview.rb +20 -0
  101. data/previews/primer/open_project/sub_header_preview/action_menu_buttons.html.erb +7 -10
  102. data/previews/primer/open_project/sub_header_preview/button_group.html.erb +13 -7
  103. data/previews/primer/open_project/sub_header_preview/custom_filter_button.html.erb +1 -1
  104. data/previews/primer/open_project/sub_header_preview/dialog_buttons.html.erb +9 -8
  105. data/previews/primer/open_project/sub_header_preview.rb +26 -7
  106. data/previews/primer/open_project/tree_view_preview/default.html.erb +24 -0
  107. data/previews/primer/open_project/tree_view_preview/empty.html.erb +10 -0
  108. data/previews/primer/open_project/tree_view_preview/leaf_node_playground.html.erb +15 -0
  109. data/previews/primer/open_project/tree_view_preview/loading_failure.html.erb +36 -0
  110. data/previews/primer/open_project/tree_view_preview/loading_skeleton.html.erb +12 -0
  111. data/previews/primer/open_project/tree_view_preview/loading_spinner.html.erb +12 -0
  112. data/previews/primer/open_project/tree_view_preview/playground.html.erb +4 -0
  113. data/previews/primer/open_project/tree_view_preview.rb +139 -0
  114. data/static/arguments.json +512 -0
  115. data/static/audited_at.json +21 -0
  116. data/static/classes.json +18 -0
  117. data/static/constants.json +108 -1
  118. data/static/info_arch.json +1612 -0
  119. data/static/previews.json +180 -0
  120. data/static/statuses.json +21 -0
  121. metadata +79 -2
@@ -10,6 +10,9 @@ module Primer
10
10
  HIDDEN_FILTER_TARGET_SELECTOR = "sub-header.hiddenItemsOnExpandedFilter"
11
11
  SHOWN_FILTER_TARGET_SELECTOR = "sub-header.shownItemsOnExpandedFilter"
12
12
 
13
+ MOBILE_ACTIONS_DISPLAY = [:flex, :none].freeze
14
+ DESKTOP_ACTIONS_DISPLAY = [:none, :flex].freeze
15
+
13
16
  # A button or custom content that will render on the right-hand side of the component.
14
17
  #
15
18
  # To render a button, call the `with_button` method, which accepts the arguments accepted by <%= link_to_component(Primer::Beta::Button) %>.
@@ -17,25 +20,63 @@ module Primer
17
20
  # To render custom content, call the `with_button_component` method and pass a block that returns HTML.
18
21
  renders_many :actions, types: {
19
22
  button: {
20
- renders: lambda { |icon: nil, **kwargs|
21
- if icon
22
- Primer::Beta::IconButton.new(icon: icon, **kwargs)
23
+ renders: lambda { |icon_only: false, leading_icon:, label:, **kwargs, &block|
24
+ if label.nil? || label.empty?
25
+ raise ArgumentError, "You need to provide a valid label."
26
+ end
27
+
28
+ kwargs[:icon] = leading_icon
29
+
30
+ kwargs[:aria] ||= merge_aria(
31
+ kwargs,
32
+ { aria: { label: label } }
33
+ )
34
+
35
+ icon_args = kwargs.deep_dup
36
+
37
+ if icon_only
38
+ Primer::Beta::IconButton.new(**icon_args)
23
39
  else
24
- Primer::Beta::Button.new(**kwargs)
40
+ @mobile_actions ||= []
41
+ mobile_component = Primer::Beta::IconButton.new(display: MOBILE_ACTIONS_DISPLAY,
42
+ **icon_args)
43
+ @mobile_actions.push({ component: mobile_component, block: block})
44
+
45
+ Primer::OpenProject::SubHeader::Button.new(display: DESKTOP_ACTIONS_DISPLAY, **kwargs)
25
46
  end
26
47
  },
27
48
  },
28
- component: {
29
- # A generic slot to render whatever component you like on the right side
49
+ button_group: {
30
50
  renders: lambda { |**kwargs|
31
- deny_tag_argument(**kwargs)
32
- kwargs[:tag] = :div
33
- Primer::BaseComponent.new(**kwargs)
51
+ Primer::OpenProject::SubHeader::ButtonGroup.new(**kwargs)
52
+ },
53
+ },
54
+ menu: {
55
+ renders: lambda { |icon_only: false, leading_icon:, label:, button_arguments: {}, **kwargs, &block|
56
+ if label.nil? || label.empty?
57
+ raise ArgumentError, "You need to provide a valid label."
58
+ end
59
+
60
+ kwargs[:leading_icon] = leading_icon
61
+ kwargs[:label] = label
62
+ kwargs[:button_arguments] = button_arguments
63
+
64
+ @mobile_actions ||= []
65
+ mobile_component = Primer::OpenProject::SubHeader::Menu.new(icon_only: true,
66
+ display: MOBILE_ACTIONS_DISPLAY,
67
+ **kwargs)
68
+ @mobile_actions.push({ component: mobile_component, block: block})
69
+
70
+ Primer::OpenProject::SubHeader::Menu.new(icon_only: icon_only,display: DESKTOP_ACTIONS_DISPLAY, **kwargs)
34
71
  },
35
72
  }
36
73
  }
37
74
 
38
75
  renders_one :filter_input, lambda { |name:, label:, **system_arguments|
76
+ if label.nil? || label.empty?
77
+ raise ArgumentError, "You need to provide a valid label."
78
+ end
79
+
39
80
  system_arguments[:classes] = class_names(
40
81
  system_arguments[:classes],
41
82
  "SubHeader-filterInput",
@@ -62,6 +103,7 @@ module Primer
62
103
  @mobile_filter_trigger = Primer::Beta::IconButton.new(icon: system_arguments[:leading_visual][:icon],
63
104
  display: [:inline_flex, :none],
64
105
  aria: { label: label },
106
+ mr: 2,
65
107
  "data-action": "click:sub-header#expandFilterInput",
66
108
  "data-targets": HIDDEN_FILTER_TARGET_SELECTOR)
67
109
 
@@ -83,18 +125,25 @@ module Primer
83
125
  # To render custom content, call the `with_filter_component` method and pass a block that returns HTML.
84
126
  renders_one :filter_button, types: {
85
127
  button: {
86
- renders: lambda { |icon: nil, **kwargs|
87
- kwargs[:classes] = class_names(
88
- kwargs[:classes],
89
- "SubHeader-filterButton"
128
+ renders: lambda { |icon_only: false, leading_icon: :filter, mobile_label: I18n.t("button_filter"), **kwargs|
129
+ kwargs[:mr] ||= 2
130
+ kwargs[:icon] = leading_icon
131
+
132
+ kwargs[:aria] ||= merge_aria(
133
+ kwargs,
134
+ { aria: { label: mobile_label } }
90
135
  )
91
- kwargs[:data] ||= {}
92
- kwargs[:data][:targets] ||= HIDDEN_FILTER_TARGET_SELECTOR
93
136
 
94
- if icon
95
- Primer::Beta::IconButton.new(icon: icon, **kwargs)
137
+ icon_args = kwargs.deep_dup
138
+ icon_args = set_as_hidden_filter_target(icon_args)
139
+
140
+ if icon_only
141
+ Primer::Beta::IconButton.new(**icon_args)
96
142
  else
97
- Primer::Beta::Button.new(**kwargs)
143
+ @mobile_filter_button = Primer::Beta::IconButton.new(display: MOBILE_ACTIONS_DISPLAY,
144
+ **icon_args)
145
+
146
+ Primer::OpenProject::SubHeader::Button.new(display: DESKTOP_ACTIONS_DISPLAY, **kwargs)
98
147
  end
99
148
  },
100
149
 
@@ -105,8 +154,7 @@ module Primer
105
154
  renders: lambda { |**kwargs|
106
155
  deny_tag_argument(**kwargs)
107
156
  kwargs[:tag] = :div
108
- kwargs[:data] ||= {}
109
- kwargs[:data][:targets] ||= HIDDEN_FILTER_TARGET_SELECTOR
157
+ kwargs = set_as_hidden_filter_target(kwargs)
110
158
 
111
159
  Primer::BaseComponent.new(**kwargs)
112
160
  },
@@ -115,6 +163,21 @@ module Primer
115
163
  }
116
164
  }
117
165
 
166
+ renders_one :segmented_control, lambda { |**system_arguments, &block|
167
+ deny_tag_argument(**system_arguments)
168
+ system_arguments[:mr] ||= 2
169
+ system_arguments = set_as_hidden_filter_target(system_arguments)
170
+
171
+ @segmented_control_block = block
172
+ @mobile_segmented_control = Primer::OpenProject::SubHeader::SegmentedControl.new(
173
+ hide_labels: true,
174
+ display: MOBILE_ACTIONS_DISPLAY,
175
+ **system_arguments
176
+ )
177
+
178
+ Primer::OpenProject::SubHeader::SegmentedControl.new(display: DESKTOP_ACTIONS_DISPLAY, **system_arguments)
179
+ }
180
+
118
181
  renders_one :text, lambda { |**system_arguments|
119
182
  system_arguments[:font_weight] ||= :bold
120
183
 
@@ -138,7 +201,9 @@ module Primer
138
201
 
139
202
  @filter_container = Primer::BaseComponent.new(tag: :div,
140
203
  classes: "SubHeader-filterContainer",
141
- display: [:none, :flex],
204
+ display: DESKTOP_ACTIONS_DISPLAY,
205
+
206
+ mr: 2,
142
207
  data: { targets: SHOWN_FILTER_TARGET_SELECTOR })
143
208
 
144
209
  @system_arguments[:classes] = class_names(
@@ -146,6 +211,25 @@ module Primer
146
211
  system_arguments[:classes]
147
212
  )
148
213
  end
214
+
215
+ def before_render
216
+ @system_arguments[:classes] = class_names(
217
+ @system_arguments[:classes],
218
+ "SubHeader--emptyLeftPane" => !segmented_control? && !filter_button && !filter_input
219
+ )
220
+ end
221
+
222
+ def set_as_hidden_filter_target(system_arguments)
223
+ system_arguments[:data] ||= {}
224
+ system_arguments[:data] = merge_data(
225
+ system_arguments, {
226
+ data: {
227
+ targets: HIDDEN_FILTER_TARGET_SELECTOR,
228
+ }
229
+ }
230
+ )
231
+ system_arguments
232
+ end
149
233
  end
150
234
  end
151
235
  end
@@ -0,0 +1 @@
1
+ <%= render(Primer::Beta::Octicon.new(**@system_arguments)) %>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module OpenProject
5
+ class TreeView
6
+ # An icon for a `TreeView` node.
7
+ #
8
+ # This component is part of the <%= link_to_component(Primer::OpenProject::TreeView) %> component and should
9
+ # not be used directly.
10
+ class Icon < Primer::Component
11
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::Octicon) %>.
12
+ def initialize(**system_arguments)
13
+ @system_arguments = system_arguments
14
+ @system_arguments[:focusable] = "false"
15
+ @system_arguments[:display] = :inline_block
16
+ @system_arguments[:overflow] = :visible
17
+ @system_arguments[:style] = "vertical-align: text-bottom;"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ <%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
2
+ <% if expanded_icon? %>
3
+ <div data-target="tree-view-icon-pair.expandedIcon" <%= expanded? ? "" : "hidden" %>>
4
+ <%= expanded_icon %>
5
+ </div>
6
+ <% end %>
7
+
8
+ <% if collapsed_icon? %>
9
+ <div data-target="tree-view-icon-pair.collapsedIcon" <%= expanded? ? "hidden" : "" %>>
10
+ <%= collapsed_icon %>
11
+ </div>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module OpenProject
5
+ class TreeView
6
+ # A pair of icons for a `TreeView` sub-tree that displays distinct icons when the sub-tree is
7
+ # expanded and collapsed.
8
+ #
9
+ # This component is part of the <%= link_to_component(Primer::OpenProject::TreeView) %> component and should
10
+ # not be used directly.
11
+ class IconPair < Primer::Component
12
+ # The expanded icon.
13
+ #
14
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::Icon) %>.
15
+ renders_one :expanded_icon, lambda { |**system_arguments|
16
+ Icon.new(**system_arguments)
17
+ }
18
+
19
+ # The collapsed icon.
20
+ #
21
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::Icon) %>.
22
+ renders_one :collapsed_icon, lambda { |**system_arguments|
23
+ Icon.new(**system_arguments)
24
+ }
25
+
26
+ # Whether or not this icon is expanded.
27
+ #
28
+ # @return [Boolean]
29
+ attr_reader :expanded
30
+ alias expanded? expanded
31
+
32
+ # @param expanded [Boolean] If true, the expanded icon is shown and the collapsed icon is hidden, etc.
33
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
34
+ def initialize(expanded: false, **system_arguments)
35
+ @expanded = expanded
36
+ @system_arguments = deny_tag_argument(**system_arguments)
37
+ @system_arguments[:tag] = :"tree-view-icon-pair"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ <div class="TreeViewItemLeadingAction" aria-hidden="true">
2
+ <%= render(@action) %>
3
+ </div>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module OpenProject
5
+ class TreeView
6
+ # The leading action for `TreeView` nodes.
7
+ #
8
+ # This component is part of the <%= link_to_component(Primer::OpenProject::TreeView) %> component and should
9
+ # not be used directly.
10
+ class LeadingAction < Primer::Component
11
+ # @param action [ViewComponent::Base] A component or other renderable to use as the action button etc.
12
+ def initialize(action:)
13
+ @action = action
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ <%= render(@node) do |node| %>
2
+ <% if leading_action? %>
3
+ <% node.with_leading_action do %>
4
+ <%= leading_action %>
5
+ <% end %>
6
+ <% end %>
7
+ <% if leading_visual? %>
8
+ <% node.with_leading_visual do %>
9
+ <%= leading_visual %>
10
+ <% end %>
11
+ <% end %>
12
+ <% if trailing_visual? %>
13
+ <% node.with_trailing_visual do %>
14
+ <%= trailing_visual %>
15
+ <% end %>
16
+ <% end %>
17
+ <% node.with_text_content { @label } %>
18
+ <% end %>
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module OpenProject
5
+ class TreeView
6
+ # A `TreeView` leaf node.
7
+ #
8
+ # This component is part of the <%= link_to_component(Primer::OpenProject::TreeView) %> component and should
9
+ # not be used directly.
10
+ class LeafNode < Primer::Component
11
+ # @!parse
12
+ # # Adds a leading visual icon rendered to the left of the node's label.
13
+ # #
14
+ # # @param label [String] A label describing the visual, displayed only to screen readers.
15
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::Icon) %>.
16
+ # def with_leading_visual_icon(label: nil, **system_arguments, &block)
17
+ # end
18
+
19
+ renders_one :leading_visual, types: {
20
+ icon: lambda { |label: nil, **system_arguments|
21
+ merge_system_arguments!(
22
+ aria: { describedby: leading_visual_label_id }
23
+ )
24
+
25
+ Visual.new(
26
+ id: leading_visual_label_id,
27
+ visual: Icon.new(**system_arguments),
28
+ label: label
29
+ )
30
+ }
31
+ }
32
+
33
+ # @!parse
34
+ # # Adds a leading action rendered to the left of the node's label and any leading visuals or checkboxes.
35
+ # #
36
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::IconButton) %>.
37
+ # def with_leading_action_button(**system_arguments, &block)
38
+ # end
39
+
40
+ renders_one :leading_action, types: {
41
+ button: lambda { |**system_arguments|
42
+ LeadingAction.new(
43
+ action: Primer::Beta::IconButton.new(
44
+ scheme: :invisible,
45
+ **system_arguments
46
+ )
47
+ )
48
+ }
49
+ }
50
+
51
+ # @!parse
52
+ # # Adds a trailing visual icon rendered to the right of the node's label.
53
+ # #
54
+ # # @param label [String] A label describing the visual, displayed only to screen readers.
55
+ # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::Icon) %>.
56
+ # def with_trailing_visual_icon(label: nil, **system_arguments, &block)
57
+ # end
58
+
59
+ renders_one :trailing_visual, types: {
60
+ icon: lambda { |label: nil, **system_arguments|
61
+ Visual.new(
62
+ id: nil,
63
+ visual: Icon.new(**system_arguments),
64
+ label: label
65
+ )
66
+ }
67
+ }
68
+
69
+ delegate :current?, :merge_system_arguments!, to: :@node
70
+
71
+ # @param label [String] The node's label, i.e. it's textual content.
72
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::OpenProject::TreeView::Node) %>.
73
+ def initialize(label:, **system_arguments)
74
+ @label = label
75
+ @system_arguments = system_arguments
76
+ @system_arguments[:data] = merge_data(
77
+ @system_arguments,
78
+ data: { "node-type": "leaf" }
79
+ )
80
+
81
+ @node = Primer::OpenProject::TreeView::Node.new(**@system_arguments)
82
+ end
83
+
84
+ private
85
+
86
+ def base_id
87
+ @base_id ||= self.class.generate_id
88
+ end
89
+
90
+ def leading_visual_label_id
91
+ @leading_visual_id ||= "#{base_id}-leading-visual-label"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,13 @@
1
+ <%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
2
+ <%= render(Primer::Beta::Octicon.new(icon: :"alert-fill", color: :danger)) %>
3
+ <span><%= @text %></span>
4
+ <div></div> <!-- spacer -->
5
+ <%= render(Primer::Beta::Button.new(
6
+ size: :small,
7
+ scheme: :default,
8
+ style: "justify-self: start",
9
+ **@retry_button_arguments
10
+ )) do %>
11
+ <%= @retry_button_label %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module OpenProject
5
+ class TreeView
6
+ # A `TreeView` loading failure message.
7
+ #
8
+ # This component is part of the <%= link_to_component(Primer::OpenProject::TreeView) %> component and should
9
+ # not be used directly.
10
+ class LoadingFailureMessage < Primer::Component
11
+ DEFAULT_TEXT = "Something went wrong"
12
+ DEFAULT_RETRY_BUTTON_LABEL = "Retry"
13
+
14
+ # @param text [String] The failure message to display.
15
+ # @param retry_button_label [String] The text shown on the retry button.
16
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
17
+ def initialize(text: DEFAULT_TEXT, retry_button_label: DEFAULT_RETRY_BUTTON_LABEL, **system_arguments)
18
+ @text = text
19
+ @retry_button_label = retry_button_label
20
+ @retry_button_arguments = system_arguments.delete(:retry_button_arguments)
21
+ @system_arguments = deny_tag_argument(**system_arguments)
22
+ @system_arguments[:tag] = :div
23
+ @system_arguments[:classes] = class_names(
24
+ @system_arguments.delete(:classes),
25
+ "TreeViewFailureMessage"
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ <%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
2
+ <div class="TreeViewItemContainer" style="--level: <%= level %>;">
3
+ <div style="grid-area: spacer; display: flex;">
4
+ <div style="width: 100%; display: flex;">
5
+ <% (0...(level - 1)).each do %>
6
+ <div class="TreeViewItemLevelLine"></div>
7
+ <% end %>
8
+ </div>
9
+ </div>
10
+ <% if toggle? %>
11
+ <%= toggle %>
12
+ <% end %>
13
+ <% if leading_action? %>
14
+ <%= leading_action %>
15
+ <% end %>
16
+ <div id="<%= content_id %>" class="TreeViewItemContent">
17
+ <% if @select_variant == :multiple %>
18
+ <div aria-hidden="true" class="TreeViewItemCheckbox" tabindex="-1">
19
+ <div class="FormControl-checkbox"></div>
20
+ </div>
21
+ <% end %>
22
+ <% if leading_visual? %>
23
+ <%= leading_visual %>
24
+ <% end %>
25
+ <span class="TreeViewItemContentText"><%= text_content %></span>
26
+ <% if trailing_visual? %>
27
+ <%= trailing_visual %>
28
+ <% end %>
29
+ </div>
30
+ </div>
31
+ <%= content %>
32
+ <% end %>
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module OpenProject
5
+ class TreeView
6
+ # A generic `TreeView` node.
7
+ #
8
+ # This component is part of the <%= link_to_component(Primer::OpenProject::TreeView) %> component and should
9
+ # not be used directly.
10
+ class Node < Primer::Component
11
+ # Generic leading action slot
12
+ renders_one :leading_action
13
+
14
+ # Generic leading visual slot
15
+ renders_one :leading_visual
16
+
17
+ # Generic trailing visual slot
18
+ renders_one :trailing_visual
19
+
20
+ # Generic toggle button slot
21
+ renders_one :toggle
22
+
23
+ # Generic text content slot (for node's label)
24
+ renders_one :text_content
25
+
26
+ # Wether or not this node is the current node.
27
+ #
28
+ # @return [Boolean]
29
+ attr_reader :current
30
+ alias current? current
31
+
32
+ # This node's checked state.
33
+ #
34
+ # @return [String]
35
+ attr_reader :checked
36
+
37
+ # This node's select variant (i.e. check box variant).
38
+ #
39
+ # @return [Symbol]
40
+ attr_reader :select_variant
41
+
42
+ DEFAULT_SELECT_VARIANT = :none
43
+ SELECT_VARIANT_OPTIONS = [
44
+ :multiple,
45
+ DEFAULT_SELECT_VARIANT
46
+ ].freeze
47
+
48
+ DEFAULT_CHECKED_STATE = false
49
+ CHECKED_STATES = [
50
+ DEFAULT_CHECKED_STATE,
51
+ true,
52
+ "mixed"
53
+ ]
54
+
55
+ # @param path [Array<String>] The node's "path," i.e. this node's label and the labels of all its ancestors. This node should be reachable by traversing the tree following this path.
56
+ # @param current [Boolean] Whether or not this node is the current node. The current node is styled differently than regular nodes and is the first element that receives focus when tabbing to the `TreeView` component.
57
+ # @param select_variant [Symbol] Controls the type of checkbox that appears. <%= one_of(Primer::OpenProject::TreeView::Node::SELECT_VARIANT_OPTIONS) %>
58
+ # @param checked [Boolean | String] The checked state of the node's checkbox. <%= one_of(Primer::OpenProject::TreeView::Node::CHECKED_STATES) %>
59
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>.
60
+ def initialize(
61
+ path:,
62
+ current: false,
63
+ select_variant: DEFAULT_SELECT_VARIANT,
64
+ checked: DEFAULT_CHECKED_STATE,
65
+ **system_arguments
66
+ )
67
+ @system_arguments = deny_tag_argument(**system_arguments)
68
+
69
+ @path = path
70
+ @current = current
71
+ @select_variant = fetch_or_fallback(SELECT_VARIANT_OPTIONS, select_variant, DEFAULT_SELECT_VARIANT)
72
+ @checked = fetch_or_fallback(CHECKED_STATES, checked, DEFAULT_CHECKED_STATE)
73
+
74
+ @system_arguments[:tag] = :li
75
+ @system_arguments[:role] = :treeitem
76
+ @system_arguments[:tabindex] = current? ? 0 : -1
77
+ @system_arguments[:classes] = class_names(
78
+ @system_arguments.delete(:classes),
79
+ "TreeViewItem"
80
+ )
81
+
82
+ @system_arguments[:aria] = merge_aria(
83
+ @system_arguments, {
84
+ aria: {
85
+ level: level,
86
+ selected: false,
87
+ checked: checked,
88
+ labelledby: content_id
89
+ }
90
+ }
91
+ )
92
+
93
+ @system_arguments[:data] = merge_data(
94
+ @system_arguments,
95
+ { data: { path: @path.to_json } }
96
+ )
97
+
98
+ return unless current?
99
+
100
+ @system_arguments[:aria] = merge_aria(
101
+ @system_arguments,
102
+ { aria: { current: true } }
103
+ )
104
+ end
105
+
106
+ # The numeric depth of this node.
107
+ #
108
+ # @return [Integer] This node's depth.
109
+ def level
110
+ @level ||= @path.size
111
+ end
112
+
113
+ # Merges the given arguments into the current hash of system arguments provided when the component was
114
+ # initially constructed. This method can be used to add additional arguments just before rendering.
115
+ #
116
+ # @param other_arguments [Hash] The other hash of system arguments to merge into the current one.
117
+ def merge_system_arguments!(**other_arguments)
118
+ @system_arguments[:aria] = merge_aria(
119
+ @system_arguments,
120
+ other_arguments
121
+ )
122
+
123
+ @system_arguments[:data] = merge_data(
124
+ @system_arguments,
125
+ other_arguments
126
+ )
127
+
128
+ @system_arguments.merge!(**other_arguments)
129
+ end
130
+
131
+ private
132
+
133
+ def before_render
134
+ if leading_visual?
135
+ end
136
+
137
+ if leading_action?
138
+ @system_arguments[:data] = merge_data(
139
+ @system_arguments,
140
+ { data: { "has-leading-action": true } }
141
+ )
142
+ end
143
+ end
144
+
145
+ def content_id
146
+ @content_id ||= "#{base_id}-content"
147
+ end
148
+
149
+ def base_id
150
+ @base_id ||= self.class.generate_id
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,23 @@
1
+ <% unless loading_failure_message? %>
2
+ <% with_loading_failure_message # set the default %>
3
+ <% end %>
4
+
5
+ <%= render(Primer::BaseComponent.new(tag: :"tree-view-include-fragment", src: @src, loading: :lazy, data: { target: "tree-view-sub-tree-node.subTree tree-view-sub-tree-node.includeFragment", path: @container.path.to_json }, hidden: @container.expanded?, accept: "text/fragment+html")) do %>
6
+ <%= render(@container) do %>
7
+ <%= render(Primer::OpenProject::TreeView::Node.new(path: [*@container.path, :loader])) do |node| %>
8
+ <% node.with_text_content do %>
9
+ <div data-target="tree-view-sub-tree-node.loadingIndicator">
10
+ <% @count.times do %>
11
+ <span class="TreeViewSkeletonItemContainerStyle TreeViewItemSkeleton">
12
+ <%= render(Primer::OpenProject::SkeletonBox.new(width: "16px")) %>
13
+ <%= render(Primer::OpenProject::SkeletonBox.new(width: "100%", classes: "TreeItemSkeletonTextStyles")) %>
14
+ </span>
15
+ <% end %>
16
+ </div>
17
+ <div data-target="tree-view-sub-tree-node.loadingFailureMessage" hidden>
18
+ <%= loading_failure_message %>
19
+ </div>
20
+ <% end %>
21
+ <% end %>
22
+ <% end %>
23
+ <% end %>