openproject-primer_view_components 0.64.0 → 0.65.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/app/assets/javascripts/components/primer/open_project/collapsible.d.ts +2 -0
  4. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view.d.ts +29 -0
  5. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_icon_pair_element.d.ts +15 -0
  6. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_include_fragment_element.d.ts +9 -0
  7. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_roving_tab_index.d.ts +3 -0
  8. data/app/assets/javascripts/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +38 -0
  9. data/app/assets/javascripts/components/primer/primer.d.ts +4 -0
  10. data/app/assets/javascripts/components/primer/shared_events.d.ts +15 -0
  11. data/app/assets/javascripts/primer_view_components.js +1 -1
  12. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  13. data/app/assets/styles/primer_view_components.css +1 -1
  14. data/app/assets/styles/primer_view_components.css.map +1 -1
  15. data/app/components/primer/alpha/select_panel.css +1 -1
  16. data/app/components/primer/alpha/select_panel.css.json +2 -2
  17. data/app/components/primer/alpha/select_panel.css.map +1 -1
  18. data/app/components/primer/alpha/select_panel.html.erb +1 -1
  19. data/app/components/primer/alpha/select_panel.pcss +5 -2
  20. data/app/components/primer/beta/spinner.html.erb +1 -1
  21. data/app/components/primer/beta/spinner.rb +2 -0
  22. data/app/components/primer/open_project/border_box/collapsible_header.css +1 -1
  23. data/app/components/primer/open_project/border_box/collapsible_header.css.json +2 -1
  24. data/app/components/primer/open_project/border_box/collapsible_header.css.map +1 -1
  25. data/app/components/primer/open_project/border_box/collapsible_header.html.erb +12 -1
  26. data/app/components/primer/open_project/border_box/collapsible_header.pcss +4 -0
  27. data/app/components/primer/open_project/border_box/collapsible_header.rb +16 -12
  28. data/app/components/primer/open_project/collapsible.d.ts +2 -0
  29. data/app/components/primer/open_project/collapsible.js +11 -0
  30. data/app/components/primer/open_project/collapsible.ts +10 -0
  31. data/app/components/primer/open_project/collapsible_section.html.erb +14 -2
  32. data/app/components/primer/open_project/collapsible_section.rb +5 -1
  33. data/app/components/primer/open_project/danger_dialog.html.erb +4 -0
  34. data/app/components/primer/open_project/feedback_dialog.html.erb +5 -0
  35. data/app/components/primer/open_project/file_tree_view/directory_node.html.erb +5 -0
  36. data/app/components/primer/open_project/file_tree_view/directory_node.rb +24 -0
  37. data/app/components/primer/open_project/file_tree_view/file_node.html.erb +2 -0
  38. data/app/components/primer/open_project/file_tree_view/file_node.rb +14 -0
  39. data/app/components/primer/open_project/file_tree_view.rb +15 -0
  40. data/app/components/primer/open_project/skeleton_box.css +1 -0
  41. data/app/components/primer/open_project/skeleton_box.css.json +6 -0
  42. data/app/components/primer/open_project/skeleton_box.css.map +1 -0
  43. data/app/components/primer/open_project/skeleton_box.html.erb +1 -0
  44. data/app/components/primer/open_project/skeleton_box.pcss +30 -0
  45. data/app/components/primer/open_project/skeleton_box.rb +27 -0
  46. data/app/components/primer/open_project/tree_view/icon.html.erb +1 -0
  47. data/app/components/primer/open_project/tree_view/icon.rb +22 -0
  48. data/app/components/primer/open_project/tree_view/icon_pair.html.erb +13 -0
  49. data/app/components/primer/open_project/tree_view/icon_pair.rb +42 -0
  50. data/app/components/primer/open_project/tree_view/leading_action.html.erb +3 -0
  51. data/app/components/primer/open_project/tree_view/leading_action.rb +18 -0
  52. data/app/components/primer/open_project/tree_view/leaf_node.html.erb +18 -0
  53. data/app/components/primer/open_project/tree_view/leaf_node.rb +96 -0
  54. data/app/components/primer/open_project/tree_view/loading_failure_message.html.erb +13 -0
  55. data/app/components/primer/open_project/tree_view/loading_failure_message.rb +31 -0
  56. data/app/components/primer/open_project/tree_view/node.html.erb +32 -0
  57. data/app/components/primer/open_project/tree_view/node.rb +155 -0
  58. data/app/components/primer/open_project/tree_view/skeleton_loader.html.erb +23 -0
  59. data/app/components/primer/open_project/tree_view/skeleton_loader.rb +36 -0
  60. data/app/components/primer/open_project/tree_view/spinner_loader.html.erb +20 -0
  61. data/app/components/primer/open_project/tree_view/spinner_loader.rb +33 -0
  62. data/app/components/primer/open_project/tree_view/sub_tree.html.erb +21 -0
  63. data/app/components/primer/open_project/tree_view/sub_tree.rb +106 -0
  64. data/app/components/primer/open_project/tree_view/sub_tree_container.html.erb +3 -0
  65. data/app/components/primer/open_project/tree_view/sub_tree_container.rb +39 -0
  66. data/app/components/primer/open_project/tree_view/sub_tree_node.html.erb +49 -0
  67. data/app/components/primer/open_project/tree_view/sub_tree_node.rb +172 -0
  68. data/app/components/primer/open_project/tree_view/tree_view.d.ts +29 -0
  69. data/app/components/primer/open_project/tree_view/tree_view.js +238 -0
  70. data/app/components/primer/open_project/tree_view/tree_view.ts +257 -0
  71. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.d.ts +15 -0
  72. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.js +62 -0
  73. data/app/components/primer/open_project/tree_view/tree_view_icon_pair_element.ts +56 -0
  74. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.d.ts +9 -0
  75. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.js +29 -0
  76. data/app/components/primer/open_project/tree_view/tree_view_include_fragment_element.ts +29 -0
  77. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.d.ts +3 -0
  78. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.js +126 -0
  79. data/app/components/primer/open_project/tree_view/tree_view_roving_tab_index.ts +156 -0
  80. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.d.ts +38 -0
  81. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.js +362 -0
  82. data/app/components/primer/open_project/tree_view/tree_view_sub_tree_node_element.ts +402 -0
  83. data/app/components/primer/open_project/tree_view/visual.html.erb +14 -0
  84. data/app/components/primer/open_project/tree_view/visual.rb +27 -0
  85. data/app/components/primer/open_project/tree_view.css +1 -0
  86. data/app/components/primer/open_project/tree_view.css.json +42 -0
  87. data/app/components/primer/open_project/tree_view.css.map +1 -0
  88. data/app/components/primer/open_project/tree_view.html.erb +7 -0
  89. data/app/components/primer/open_project/tree_view.pcss +319 -0
  90. data/app/components/primer/open_project/tree_view.rb +367 -0
  91. data/app/components/primer/primer.d.ts +4 -0
  92. data/app/components/primer/primer.js +4 -0
  93. data/app/components/primer/primer.pcss +2 -0
  94. data/app/components/primer/primer.ts +4 -0
  95. data/app/components/primer/shared_events.d.ts +15 -0
  96. data/app/components/primer/shared_events.ts +19 -0
  97. data/app/lib/primer/forms/acts_as_component.rb +1 -12
  98. data/lib/primer/view_components/version.rb +1 -1
  99. data/previews/primer/open_project/file_tree_view_preview/default.html.erb +16 -0
  100. data/previews/primer/open_project/file_tree_view_preview/playground.html.erb +4 -0
  101. data/previews/primer/open_project/file_tree_view_preview.rb +69 -0
  102. data/previews/primer/open_project/skeleton_box_preview.rb +20 -0
  103. data/previews/primer/open_project/tree_view_preview/default.html.erb +24 -0
  104. data/previews/primer/open_project/tree_view_preview/empty.html.erb +10 -0
  105. data/previews/primer/open_project/tree_view_preview/leaf_node_playground.html.erb +15 -0
  106. data/previews/primer/open_project/tree_view_preview/loading_failure.html.erb +36 -0
  107. data/previews/primer/open_project/tree_view_preview/loading_skeleton.html.erb +12 -0
  108. data/previews/primer/open_project/tree_view_preview/loading_spinner.html.erb +12 -0
  109. data/previews/primer/open_project/tree_view_preview/playground.html.erb +4 -0
  110. data/previews/primer/open_project/tree_view_preview.rb +139 -0
  111. data/static/arguments.json +400 -0
  112. data/static/audited_at.json +17 -0
  113. data/static/classes.json +18 -0
  114. data/static/constants.json +83 -0
  115. data/static/info_arch.json +1379 -0
  116. data/static/previews.json +167 -0
  117. data/static/statuses.json +17 -0
  118. metadata +75 -2
@@ -0,0 +1,319 @@
1
+ /* stylelint-disable selector-max-type -- Copied from primer/react */
2
+
3
+ .TreeViewRootUlStyles {
4
+ padding: 0;
5
+ margin: 0;
6
+ list-style: none;
7
+
8
+ /*
9
+ * WARNING: This is a performance optimization.
10
+ *
11
+ * We define styles for the tree items at the root level of the tree
12
+ * to avoid recomputing the styles for each item when the tree updates.
13
+ * We're sacrificing maintainability for performance because TreeView
14
+ * needs to be performant enough to handle large trees (thousands of items).
15
+ *
16
+ * This is intended to be a temporary solution until we can improve the
17
+ * performance of our styling patterns.
18
+ *
19
+ * Do NOT copy this pattern without understanding the tradeoffs.
20
+ */
21
+ & .TreeViewItem {
22
+ outline: none;
23
+
24
+ &:focus-visible > div {
25
+ box-shadow: var(--boxShadow-thick) var(--fgColor-accent);
26
+
27
+ @media (forced-colors: active) {
28
+ outline: 2px solid HighlightText;
29
+ /* stylelint-disable-next-line declaration-property-value-no-unknown -- Copied from primer/react */
30
+ outline-offset: -2;
31
+ }
32
+ }
33
+
34
+ &[data-has-leading-action] {
35
+ --has-leading-action: 1;
36
+ }
37
+ }
38
+
39
+ & .TreeViewItemContainer {
40
+ --level: 1;
41
+ --toggle-width: 1rem;
42
+ --min-item-height: 2rem;
43
+
44
+ position: relative;
45
+ display: grid;
46
+ width: 100%;
47
+ font-size: var(--text-body-size-medium);
48
+ color: var(--fgColor-default);
49
+ cursor: pointer;
50
+ border-radius: var(--borderRadius-medium);
51
+ grid-template-columns: var(--spacer-width) var(--leading-action-width) var(--toggle-width) 1fr;
52
+ grid-template-areas: 'spacer leadingAction toggle content';
53
+
54
+ --leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem);
55
+ --spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2));
56
+
57
+ &:hover {
58
+ background-color: var(--control-transparent-bgColor-hover);
59
+
60
+ @media (forced-colors: active) {
61
+ outline: 2px solid transparent;
62
+ outline-offset: -2px;
63
+ }
64
+ }
65
+
66
+ @media (pointer: coarse) {
67
+ --toggle-width: 1.5rem;
68
+ --min-item-height: 2.75rem;
69
+ }
70
+
71
+ &:has(.TreeViewFailureMessage):hover {
72
+ cursor: default;
73
+ background-color: transparent;
74
+
75
+ @media (forced-colors: active) {
76
+ outline: none;
77
+ }
78
+ }
79
+ }
80
+
81
+ &:where([data-omit-spacer='true']) .TreeViewItemContainer {
82
+ grid-template-columns: 0 0 0 1fr;
83
+ }
84
+
85
+ & .TreeViewItem[aria-current='true'] > .TreeViewItemContainer {
86
+ background-color: var(--control-transparent-bgColor-selected);
87
+
88
+ /* Current item indicator */
89
+ /* stylelint-disable-next-line selector-max-specificity -- Copied from primer/react */
90
+ &::after {
91
+ position: absolute;
92
+ top: calc(50% - var(--base-size-12));
93
+ left: calc(-1 * var(--base-size-8));
94
+ width: 0.25rem;
95
+ height: 1.5rem;
96
+ content: '';
97
+
98
+ /*
99
+ * Use fgColor accent for consistency across all themes. Using the "correct" variable,
100
+ * --bgColor-accent-emphasis, causes vrt failures for dark high contrast mode
101
+ */
102
+ /* stylelint-disable-next-line primer/colors */
103
+ background-color: var(--fgColor-accent);
104
+ border-radius: var(--borderRadius-medium);
105
+
106
+ @media (forced-colors: active) {
107
+ background-color: HighlightText;
108
+ }
109
+ }
110
+ }
111
+
112
+ /* stylelint-disable-next-line no-duplicate-selectors -- Copied from primer/react */
113
+ & .TreeViewItem {
114
+ &[aria-checked='true'] {
115
+ /* stylelint-disable-next-line selector-max-compound-selectors, selector-max-specificity -- Copied from primer/react */
116
+ & > .TreeViewItemContainer .FormControl-checkbox {
117
+ background: var(--control-checked-bgColor-rest);
118
+ border-color: var(--control-checked-borderColor-rest);
119
+ transition: background-color, border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; /* unchecked -> checked */
120
+
121
+ /* stylelint-disable-next-line max-nesting-depth, selector-max-compound-selectors, selector-max-specificity -- Copied from primer/react */
122
+ &::before {
123
+ visibility: visible;
124
+ transition: visibility 0s linear 0s;
125
+ animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms;
126
+ }
127
+ }
128
+ }
129
+
130
+ &[aria-checked='mixed'] {
131
+ /* stylelint-disable-next-line selector-max-compound-selectors, selector-max-specificity -- Copied from primer/react */
132
+ & > .TreeViewItemContainer .FormControl-checkbox {
133
+ background: var(--control-checked-bgColor-rest);
134
+ border-color: var(--control-checked-borderColor-rest);
135
+ transition: background-color, border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; /* unchecked -> checked */
136
+
137
+ /* stylelint-disable-next-line max-nesting-depth, selector-max-compound-selectors, selector-max-specificity -- Copied from primer/react */
138
+ &::before {
139
+ visibility: visible;
140
+ mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMiIgdmlld0JveD0iMCAwIDEwIDIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxQzAgMC40NDc3MTUgMC40NDc3MTUgMCAxIDBIOUM5LjU1MjI5IDAgMTAgMC40NDc3MTUgMTAgMUMxMCAxLjU1MjI4IDkuNTUyMjkgMiA5IDJIMUMwLjQ0NzcxNSAyIDAgMS41NTIyOCAwIDFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K');
141
+ animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms;
142
+ clip-path: none;
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ & .TreeViewItemToggle {
149
+ display: flex;
150
+ height: 100%;
151
+
152
+ /* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap
153
+ across more lines. */
154
+ /* stylelint-disable-next-line primer/spacing */
155
+ padding-top: calc(var(--min-item-height) / 2 - var(--base-size-12) / 2);
156
+ color: var(--fgColor-muted);
157
+ grid-area: toggle;
158
+ justify-content: center;
159
+ align-items: flex-start;
160
+ }
161
+
162
+ & .TreeViewItemToggleHover:hover {
163
+ background-color: var(--control-transparent-bgColor-hover);
164
+ }
165
+
166
+ & .TreeViewItemToggleEnd {
167
+ border-top-left-radius: var(--borderRadius-medium);
168
+ border-bottom-left-radius: var(--borderRadius-medium);
169
+ }
170
+
171
+ & .TreeViewItemContent {
172
+ display: flex;
173
+ height: 100%;
174
+ padding: 0 var(--base-size-8);
175
+
176
+ /* The dynamic top and bottom padding to maintain the minimum item height for single line items */
177
+ /* stylelint-disable-next-line primer/spacing */
178
+ padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
179
+ /* stylelint-disable-next-line primer/spacing */
180
+ padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
181
+ line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285));
182
+ grid-area: content;
183
+ gap: var(--stack-gap-condensed);
184
+
185
+ & .TreeViewItemCheckbox {
186
+ position: relative;
187
+ color: var(--control-fgColor-rest);
188
+ text-align: left;
189
+ user-select: none;
190
+ background-color: transparent;
191
+ border: none;
192
+ border-radius: var(--borderRadius-medium);
193
+ transition: background 33.333ms linear;
194
+ touch-action: manipulation;
195
+ -webkit-tap-highlight-color: transparent;
196
+ }
197
+ }
198
+
199
+ & .TreeViewItemContentText {
200
+ flex: 1 1 auto;
201
+ width: 0;
202
+ }
203
+
204
+ &:where([data-truncate-text='true']) .TreeViewItemContentText {
205
+ overflow: hidden;
206
+ text-overflow: ellipsis;
207
+ white-space: nowrap;
208
+ }
209
+
210
+ &:where([data-truncate-text='false']) .TreeViewItemContentText {
211
+ /* stylelint-disable-next-line declaration-property-value-keyword-no-deprecated -- Copied from primer/react */
212
+ word-break: break-word;
213
+ }
214
+
215
+ & .TreeViewItemVisual {
216
+ display: flex;
217
+
218
+ /* The visual icons should appear vertically centered for single-line items, but remain at the top for items that wrap
219
+ across more lines. */
220
+ height: var(--custom-line-height, 1.3rem);
221
+ color: var(--fgColor-muted);
222
+ align-items: center;
223
+ }
224
+
225
+ & .TreeViewItemLeadingAction {
226
+ display: flex;
227
+ color: var(--fgColor-muted);
228
+ grid-area: leadingAction;
229
+
230
+ & > button {
231
+ flex-shrink: 1;
232
+ }
233
+ }
234
+
235
+ & .TreeViewItemLevelLine {
236
+ width: 100%;
237
+ height: 100%;
238
+
239
+ /*
240
+ * On devices without hover, the nesting indicator lines
241
+ * appear at all times.
242
+ */
243
+ border-color: var(--borderColor-muted);
244
+ border-right: var(--borderWidth-thin) solid;
245
+ }
246
+
247
+ /*
248
+ * On devices with :hover support, the nesting indicator lines
249
+ * fade in when the user mouses over the entire component,
250
+ * or when there's focus inside the component. This makes
251
+ * sure the component remains simple when not in use.
252
+ */
253
+ @media (hover: hover) {
254
+ .TreeViewItemLevelLine {
255
+ border-color: transparent;
256
+ }
257
+
258
+ &:hover .TreeViewItemLevelLine,
259
+ &:focus-within .TreeViewItemLevelLine {
260
+ border-color: var(--borderColor-muted);
261
+ }
262
+ }
263
+
264
+ & .TreeViewVisuallyHidden {
265
+ position: absolute;
266
+ width: 1px;
267
+ height: 1px;
268
+ padding: 0;
269
+ /* stylelint-disable-next-line primer/spacing */
270
+ margin: -1px;
271
+ overflow: hidden;
272
+ clip: rect(0, 0, 0, 0);
273
+ white-space: nowrap;
274
+ border-width: 0;
275
+ }
276
+ }
277
+
278
+ .TreeViewSkeletonItemContainerStyle {
279
+ display: flex;
280
+ align-items: center;
281
+ column-gap: 0.5rem;
282
+ height: 2rem;
283
+
284
+ @media (pointer: coarse) {
285
+ height: 2.75rem;
286
+ }
287
+
288
+ &:nth-of-type(5n + 1) {
289
+ --tree-item-loading-width: 67%;
290
+ }
291
+
292
+ &:nth-of-type(5n + 2) {
293
+ --tree-item-loading-width: 47%;
294
+ }
295
+
296
+ &:nth-of-type(5n + 3) {
297
+ --tree-item-loading-width: 73%;
298
+ }
299
+
300
+ &:nth-of-type(5n + 4) {
301
+ --tree-item-loading-width: 64%;
302
+ }
303
+
304
+ &:nth-of-type(5n + 5) {
305
+ --tree-item-loading-width: 50%;
306
+ }
307
+ }
308
+
309
+ .TreeItemSkeletonTextStyles {
310
+ width: var(--tree-item-loading-width, 67%);
311
+ }
312
+
313
+ .TreeViewFailureMessage {
314
+ display: grid;
315
+ grid-template-columns: auto 1fr;
316
+ gap: 0.5rem;
317
+ width: 100%;
318
+ align-items: center;
319
+ }
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The TreeView component is made up of a number of smaller components, quite a few more than have been created
4
+ # to construct complex components in the past. The current architecture was designed to achieve reusability for
5
+ # certain features like loading indicators, different icons for expanded and collapsed sub trees, etc. The
6
+ # following describes how the components fit together at a high level, using React-like syntax. Each element
7
+ # in the diagram corresponds to one of three types of object:
8
+ #
9
+ # 1. Elements with TitleCase tags represent components in the Primer::OpenProject::TreeView namespace, eg. <LeafNode>.
10
+ # 2. Elements with dasherized-tags are web components/custom elements, eg. <tree-view>.
11
+ # 3. Elements with lowercase tags are regular 'ol HTML elements, eg. <ul>.
12
+ #
13
+ # ### Overall structure
14
+ #
15
+ # <TreeView>
16
+ # <tree-view>
17
+ # <ul role="tree">
18
+ # <LeafNode>
19
+ # <Node>
20
+ # <li role="treeitem">
21
+ # ...
22
+ # </li>
23
+ # </Node>
24
+ # </LeafNode>
25
+ #
26
+ # <SubTreeNode>
27
+ # <tree-view-sub-tree-node>
28
+ # <li role="treeitem">
29
+ #
30
+ # <SubTreeContainer>
31
+ # <ul role="group">
32
+ # <SubTree>
33
+ # <LeafNode>
34
+ # <Node>
35
+ # <li role="treeitem">
36
+ # ...
37
+ # </li>
38
+ # </Node>
39
+ # </LeafNode>
40
+ #
41
+ # <SubTreeNode>
42
+ # <tree-view-sub-tree-node>
43
+ # <Node>
44
+ # <li role="treeitem">
45
+ # ...
46
+ # </li>
47
+ # </Node>
48
+ # <SubTreeContainer>
49
+ # <ul role="group">
50
+ # ...
51
+ # </ul>
52
+ # </SubTreeContainer>
53
+ # </tree-view-sub-tree-node>
54
+ # </SubTreeNode>
55
+ # </SubTree>
56
+ # </ul>
57
+ # </SubTreeContainer>
58
+ #
59
+ # </li>
60
+ # </tree-view-sub-tree-node>
61
+ # </SubTreeNode>
62
+ # </ul>
63
+ # </tree-view>
64
+ # </TreeView>
65
+ #
66
+ # ### Leading visuals
67
+ #
68
+ # TreeView nodes (i.e. both leaf and sub tree nodes) support leading and trailing visuals. At the time of this
69
+ # writing, only octicons are supported. The single icon case is achieved by using a standard slot, but the
70
+ # component also supports rendering distinct icons for both the expanded and collapsed states. An overview of the
71
+ # markup for this more complicated multi-icon feature is described below.
72
+ #
73
+ # <LeafNode>
74
+ # <Node>
75
+ # <li role="treeitem">
76
+ # <Visual>
77
+ # <IconPair>
78
+ # <tree-view-icon-pair>
79
+ # <Icon slot="expanded_icon">
80
+ # <Primer::Beta::Octicon />
81
+ # </Icon>
82
+ # <Icon slot="collapsed_icon">
83
+ # <Primer::Beta::Octicon />
84
+ # </Icon>
85
+ # </tree-view-icon-pair>
86
+ # </IconPair>
87
+ # </Visual>
88
+ # </li>
89
+ # </Node>
90
+ # </LeafNode>
91
+ #
92
+ # ### Loaders
93
+ #
94
+ # TreeViews support two types of loader: a loading spinner and a loading skeleton.
95
+ #
96
+ # #### Loading spinner
97
+ #
98
+ # <SubTree>
99
+ # <SpinnerLoader>
100
+ # <tree-view-include-fragment>
101
+ # <SubTreeContainer>
102
+ # <Node>
103
+ # <Primer::Beta::Spinner />
104
+ # </Node>
105
+ # <Node>
106
+ # <LoadingFailureMessage />
107
+ # </Node>
108
+ # </SubTreeContainer>
109
+ # </tree-view-include-fragment>
110
+ # </SpinnerLoader>
111
+ # </SubTree>
112
+ #
113
+ # #### Loading skeleton
114
+ #
115
+ # <SubTree>
116
+ # <SkeletonLoader>
117
+ # <tree-view-include-fragment>
118
+ # <SubTreeContainer>
119
+ # <Node>
120
+ # <span>
121
+ # <Primer::Alpha::SkeletonBox width="16px" />
122
+ # <Primer::Alpha::SkeletonBox width="100%" />
123
+ # </span>
124
+ # <span>
125
+ # ...
126
+ # </span>
127
+ # ...
128
+ # </Node>
129
+ # <Node>
130
+ # <LoadingFailureMessage />
131
+ # </Node>
132
+ # </SubTreeContainer>
133
+ # </tree-view-include-fragment>
134
+ # </SkeletonLoader>
135
+ # </SubTree>
136
+
137
+ module Primer
138
+ module OpenProject
139
+ # TreeView is a hierarchical list of items that may have a parent-child relationship where children
140
+ # can be toggled into view by expanding or collapsing their parent item.
141
+ #
142
+ # ## Terminology
143
+ #
144
+ # Consider the following tree structure:
145
+ #
146
+ # src
147
+ # ├ button.rb
148
+ # └ action_list
149
+ # ├ item.rb
150
+ # └ header.rb
151
+ #
152
+ # 1. **Node**. A node is an item in the tree. Nodes can either be "leaf" nodes (i.e. have no children), or "sub-tree"
153
+ # nodes, which do have children. In the example above, button.rb, item.rb, and header.rb are all leaf nodes, while
154
+ # action_list is a sub-tree node.
155
+ # 2. **Path**. A node's path is like its ID. It's an array of strings containing the current node's label and all the
156
+ # labels of its ancestors, in order. In the example above, header.rb's path is ["src", "action_list", "header.rb"].
157
+ #
158
+ # ## Static nodes
159
+ #
160
+ # The `TreeView` component allows items to be provided statically or loaded dynamically from the server.
161
+ # Providing items statically is done using the `leaf` and `sub_tree` slots:
162
+ #
163
+ # ```erb
164
+ # <%= render(Primer::OpenProject::TreeView.new) do |tree| %>
165
+ # <% tree.with_sub_tree(label: "Directory") do |sub_tree| %>
166
+ # <% sub_tree.with_leaf(label: "File 1")
167
+ # <% end %>
168
+ # <% tree.with_leaf(label: "File 2") %>
169
+ # <% end %>
170
+ # ```
171
+ #
172
+ # ## Dynamic nodes
173
+ #
174
+ # Tree nodes can also be fetched dynamically from the server and will require creating a Rails controller action
175
+ # to respond with the list of nodes. Unlike other Primer components, `TreeView` allows the programmer to specify
176
+ # loading behavior on a per-sub-tree basis, i.e. each sub-tree must specify how its nodes are loaded. To load nodes
177
+ # dynamically for a given sub-tree, configure it with either a loading spinner or a loading skeleton, and provide
178
+ # the URL to fetch nodes from:
179
+ #
180
+ # ```erb
181
+ # <%= render(Primer::OpenProject::TreeView.new) do |tree| %>
182
+ # <% tree.with_sub_tree(label: "Directory") do |sub_tree| %>
183
+ # <% sub_tree.with_loading_spinner(src: tree_view_items_path) %>
184
+ # <% end %>
185
+ # <% end %>
186
+ # ```
187
+ #
188
+ # Define a controller action to serve the list of nodes. The `TreeView` component automatically includes the
189
+ # sub-tree's path as a GET parameter, encoded as a JSON array.
190
+ #
191
+ # ```ruby
192
+ # class TreeViewItemsController < ApplicationController
193
+ # def show
194
+ # @path = JSON.parse(params[:path])
195
+ # @results = get_tree_items(starting_at: path)
196
+ # end
197
+ # end
198
+ # ```
199
+ #
200
+ # Responses must be HTML fragments, eg. have a content type of `text/html+fragment`. This content type isn't
201
+ # available by default in Rails, so you may have to register it eg. in an initializer:
202
+ #
203
+ # ```ruby
204
+ # Mime::Type.register("text/fragment+html", :html_fragment)
205
+ # ```
206
+ #
207
+ # Render a `Primer::OpenProject::TreeView::SubTree` in the action's template, tree_view_items/show.html_fragment.erb:
208
+ #
209
+ # ```erb
210
+ # <%= render(Primer::OpenProject::TreeView::SubTree.new(path: @path)) do |tree| %>
211
+ # <% tree.with_leaf(...) %>
212
+ # <% tree.with_sub_tree(...) do |sub_tree| %>
213
+ # ...
214
+ # <% end %>
215
+ # <% end %>
216
+ # ```
217
+ #
218
+ # ### JavaScript API
219
+ #
220
+ # `TreeView`s render a `<tree-view>` custom element that exposes behavior to the client.
221
+ #
222
+ # |Name |Notes |
223
+ # |:-----------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
224
+ # |`getNodePath(node: Element): string[]` |Returns the path to the given node. |
225
+ # |`getNodeType(node: Element): TreeViewNodeType | null` |Returns either `"leaf"` or `"sub-tree"`. |
226
+ # |`markCurrentAtPath(path: string[])` |Marks the node as the "current" node, which appears visually distinct from other nodes. |
227
+ # |`get currentNode(): HTMLLIElement | null` |Returns the current node. |
228
+ # |`expandAtPath(path: string[])` |Expands the sub-tree at `path`. |
229
+ # |`collapseAtPath(path: string[])` |Collapses the sub-tree at `path`. |
230
+ # |`toggleAtPath(path: string[])` |If the sub-tree at `path` is collapsed, this function expands it, and vice-versa. |
231
+ # |`checkAtPath(path: string[])` |If the node at `path` has a checkbox, this function checks it. |
232
+ # |`uncheckAtPath(path: string[])` |If the node at `path` has a checkbox, this function unchecks it. |
233
+ # |`toggleCheckedAtPath(path: string[])` |If the sub-tree at `path` is checked, this function unchecks it, and vice-versa. |
234
+ # |`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).|
235
+ # |`nodeAtPath(path: string[], selector?: string): Element | null` |Returns the node for the given `path`, either a leaf node or sub-tree node. |
236
+ # |`subTreeAtPath(path: string[]): TreeViewSubTreeNodeElement | null`|Returns the sub-tree at the given `path`, if it exists. |
237
+ # |`leafAtPath(path: string[]): HTMLLIElement | null` |Returns the leaf node at the given `path`, if it exists. |
238
+ # |`getNodeCheckedValue(node: Element): TreeViewCheckedValue` |The same as `checkedValueAtPath`, but accepts a node instead of a path. |
239
+ #
240
+ # #### Events
241
+ #
242
+ # The events enumerated below include node information by way of the `TreeViewNodeInfo` object, which has the
243
+ # following signature:
244
+ #
245
+ # ```typescript
246
+ # type TreeViewNodeType = 'leaf' | 'sub-tree'
247
+ # type TreeViewCheckedValue = 'true' | 'false' | 'mixed'
248
+ #
249
+ # type TreeViewNodeInfo = {
250
+ # node: Element
251
+ # type: TreeViewNodeType
252
+ # path: string[]
253
+ # checkedValue: TreeViewCheckedValue
254
+ # previousCheckedValue: TreeViewCheckedValue
255
+ # }
256
+ # ```
257
+ #
258
+ # |Name |Type |Bubbles |Cancelable |
259
+ # |:----------------------------|:------------------------------------------|:-------|:----------|
260
+ # |`treeViewNodeActivated` |`CustomEvent<TreeViewNodeInfo>` |Yes |No |
261
+ # |`treeViewBeforeNodeActivated`|`CustomEvent<TreeViewNodeInfo>` |Yes |Yes |
262
+ # |`treeViewNodeExpanded` |`CustomEvent<TreeViewNodeInfo>>` |Yes |No |
263
+ # |`treeViewNodeCollapsed` |`CustomEvent<TreeViewNodeInfo>>` |Yes |No |
264
+ # |`treeViewNodeChecked` |`CustomEvent<TreeViewNodeInfo[]>` |Yes |Yes |
265
+ # |`treeViewBeforeNodeChecked` |`CustomEvent<TreeViewNodeInfo[]>` |Yes |No |
266
+ #
267
+ # _Item activation_
268
+ #
269
+ # The `<tree-view>` element fires an `treeViewNodeActivated` event whenever a node is activated (eg. clicked)
270
+ # via the mouse or keyboard.
271
+ #
272
+ # The `treeViewBeforeNodeActivated` event fires before a node is activated. Canceling this event will prevent the
273
+ # node from being activated.
274
+ #
275
+ # ```typescript
276
+ # document.querySelector("select-panel").addEventListener(
277
+ # "treeViewBeforeNodeActivated",
278
+ # (event: CustomEvent<TreeViewNodeInfo>) => {
279
+ # event.preventDefault() // Cancel the event to prevent activation (eg. expanding/collapsing)
280
+ # }
281
+ # )
282
+ # ```
283
+ #
284
+ # _Item checking/unchecking_
285
+ #
286
+ # The `tree-view` element fires a `treeViewNodeChecked` event whenever a node is checked or unchecked.
287
+ #
288
+ # The `treeViewBeforeNodeChecked` event fires before a node is checked or unchecked. Canceling this event will
289
+ # prevent the check/uncheck operation.
290
+ #
291
+ # ```typescript
292
+ # document.querySelector("select-panel").addEventListener(
293
+ # "treeViewBeforeNodeChecked",
294
+ # (event: CustomEvent<TreeViewNodeInfo[]>) => {
295
+ # event.preventDefault() // Cancel the event to prevent activation (eg. expanding/collapsing)
296
+ # }
297
+ # )
298
+ # ```
299
+ #
300
+ # Because checking or unchecking a sub-tree results in the checking or unchecking of all its children recursively,
301
+ # both the `treeViewNodeChecked` and `treeViewBeforeNodeChecked` events provide an array of `TreeViewNodeInfo`
302
+ # objects, which contain entries for every modified node in the tree.
303
+ class TreeView < Primer::Component
304
+ # @!parse
305
+ # # Adds an leaf node to the tree. Leaf nodes are nodes that do not have children.
306
+ # #
307
+ # # @param component_klass [Class] The class to use instead of the default <%= link_to_component(Primer::OpenProject::TreeView::LeafNode) %>
308
+ # # @param system_arguments [Hash] These arguments are forwarded to <%= link_to_component(Primer::OpenProject::TreeView::LeafNode) %>, or whatever class is passed as the `component_klass` argument.
309
+ # def with_leaf(**system_arguments, &block)
310
+ # end
311
+
312
+ # @!parse
313
+ # # Adds a sub-tree node to the tree. Sub-trees are nodes that have children, which can be both leaf nodes and other sub-trees.
314
+ # #
315
+ # # @param component_klass [Class] The class to use instead of the default <%= link_to_component(Primer::OpenProject::TreeView::SubTreeNode) %>
316
+ # # @param system_arguments [Hash] These arguments are forwarded to <%= link_to_component(Primer::OpenProject::TreeView::SubTreeNode) %>, or whatever class is passed as the `component_klass` argument.
317
+ # def with_sub_tree(**system_arguments, &block)
318
+ # end
319
+
320
+ renders_many :nodes, types: {
321
+ leaf: {
322
+ renders: lambda { |component_klass: LeafNode, label:, **system_arguments|
323
+ component_klass.new(
324
+ **system_arguments,
325
+ path: [label],
326
+ label: label
327
+ )
328
+ },
329
+
330
+ as: :leaf
331
+ },
332
+
333
+ sub_tree: {
334
+ renders: lambda { |component_klass: SubTreeNode, label:, **system_arguments|
335
+ component_klass.new(
336
+ **system_arguments,
337
+ path: [label],
338
+ label: label
339
+ )
340
+ },
341
+
342
+ as: :sub_tree
343
+ }
344
+ }
345
+
346
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>.
347
+ def initialize(**system_arguments)
348
+ @system_arguments = deny_tag_argument(**system_arguments)
349
+
350
+ @system_arguments[:tag] = :ul
351
+ @system_arguments[:role] = :tree
352
+ @system_arguments[:classes] = class_names(
353
+ @system_arguments.delete(:classes),
354
+ "TreeViewRootUlStyles"
355
+ )
356
+ end
357
+
358
+ private
359
+
360
+ def before_render
361
+ if (first_node = nodes.first)
362
+ first_node.merge_system_arguments!(tabindex: 0)
363
+ end
364
+ end
365
+ end
366
+ end
367
+ end
@@ -33,3 +33,7 @@ import './open_project/danger_dialog_form_helper';
33
33
  import './open_project/collapsible';
34
34
  import './open_project/border_box/collapsible_header';
35
35
  import './open_project/collapsible_section';
36
+ import './open_project/tree_view/tree_view';
37
+ import './open_project/tree_view/tree_view_icon_pair_element';
38
+ import './open_project/tree_view/tree_view_sub_tree_node_element';
39
+ import './open_project/tree_view/tree_view_include_fragment_element';