primer_view_components 0.43.5 → 0.44.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/app/assets/javascripts/components/primer/alpha/tree_view/tree_view.d.ts +39 -0
  4. data/app/assets/javascripts/components/primer/alpha/tree_view/tree_view_icon_pair_element.d.ts +15 -0
  5. data/app/assets/javascripts/components/primer/alpha/tree_view/tree_view_include_fragment_element.d.ts +9 -0
  6. data/app/assets/javascripts/components/primer/alpha/tree_view/tree_view_roving_tab_index.d.ts +3 -0
  7. data/app/assets/javascripts/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.d.ts +42 -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/file_tree_view/directory_node.html.erb +5 -0
  15. data/app/components/primer/alpha/file_tree_view/directory_node.rb +24 -0
  16. data/app/components/primer/alpha/file_tree_view/file_node.html.erb +2 -0
  17. data/app/components/primer/alpha/file_tree_view/file_node.rb +14 -0
  18. data/app/components/primer/alpha/file_tree_view.rb +15 -0
  19. data/app/components/primer/alpha/skeleton_box.css +1 -0
  20. data/app/components/primer/alpha/skeleton_box.css.json +6 -0
  21. data/app/components/primer/alpha/skeleton_box.css.map +1 -0
  22. data/app/components/primer/alpha/skeleton_box.html.erb +1 -0
  23. data/app/components/primer/alpha/skeleton_box.pcss +30 -0
  24. data/app/components/primer/alpha/skeleton_box.rb +29 -0
  25. data/app/components/primer/alpha/tree_view/icon.html.erb +1 -0
  26. data/app/components/primer/alpha/tree_view/icon.rb +22 -0
  27. data/app/components/primer/alpha/tree_view/icon_pair.html.erb +13 -0
  28. data/app/components/primer/alpha/tree_view/icon_pair.rb +42 -0
  29. data/app/components/primer/alpha/tree_view/leading_action.html.erb +3 -0
  30. data/app/components/primer/alpha/tree_view/leading_action.rb +18 -0
  31. data/app/components/primer/alpha/tree_view/leaf_node.html.erb +18 -0
  32. data/app/components/primer/alpha/tree_view/leaf_node.rb +96 -0
  33. data/app/components/primer/alpha/tree_view/loading_failure_message.html.erb +13 -0
  34. data/app/components/primer/alpha/tree_view/loading_failure_message.rb +31 -0
  35. data/app/components/primer/alpha/tree_view/node.html.erb +32 -0
  36. data/app/components/primer/alpha/tree_view/node.rb +194 -0
  37. data/app/components/primer/alpha/tree_view/skeleton_loader.html.erb +23 -0
  38. data/app/components/primer/alpha/tree_view/skeleton_loader.rb +36 -0
  39. data/app/components/primer/alpha/tree_view/spinner_loader.html.erb +20 -0
  40. data/app/components/primer/alpha/tree_view/spinner_loader.rb +33 -0
  41. data/app/components/primer/alpha/tree_view/sub_tree.html.erb +21 -0
  42. data/app/components/primer/alpha/tree_view/sub_tree.rb +113 -0
  43. data/app/components/primer/alpha/tree_view/sub_tree_container.html.erb +3 -0
  44. data/app/components/primer/alpha/tree_view/sub_tree_container.rb +39 -0
  45. data/app/components/primer/alpha/tree_view/sub_tree_node.html.erb +49 -0
  46. data/app/components/primer/alpha/tree_view/sub_tree_node.rb +188 -0
  47. data/app/components/primer/alpha/tree_view/tree_view.d.ts +39 -0
  48. data/app/components/primer/alpha/tree_view/tree_view.js +363 -0
  49. data/app/components/primer/alpha/tree_view/tree_view.ts +396 -0
  50. data/app/components/primer/alpha/tree_view/tree_view_icon_pair_element.d.ts +15 -0
  51. data/app/components/primer/alpha/tree_view/tree_view_icon_pair_element.js +62 -0
  52. data/app/components/primer/alpha/tree_view/tree_view_icon_pair_element.ts +56 -0
  53. data/app/components/primer/alpha/tree_view/tree_view_include_fragment_element.d.ts +9 -0
  54. data/app/components/primer/alpha/tree_view/tree_view_include_fragment_element.js +28 -0
  55. data/app/components/primer/alpha/tree_view/tree_view_include_fragment_element.ts +28 -0
  56. data/app/components/primer/alpha/tree_view/tree_view_roving_tab_index.d.ts +3 -0
  57. data/app/components/primer/alpha/tree_view/tree_view_roving_tab_index.js +130 -0
  58. data/app/components/primer/alpha/tree_view/tree_view_roving_tab_index.ts +161 -0
  59. data/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.d.ts +42 -0
  60. data/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.js +418 -0
  61. data/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.ts +470 -0
  62. data/app/components/primer/alpha/tree_view/visual.html.erb +14 -0
  63. data/app/components/primer/alpha/tree_view/visual.rb +27 -0
  64. data/app/components/primer/alpha/tree_view.css +1 -0
  65. data/app/components/primer/alpha/tree_view.css.json +52 -0
  66. data/app/components/primer/alpha/tree_view.css.map +1 -0
  67. data/app/components/primer/alpha/tree_view.html.erb +12 -0
  68. data/app/components/primer/alpha/tree_view.pcss +373 -0
  69. data/app/components/primer/alpha/tree_view.rb +439 -0
  70. data/app/components/primer/beta/breadcrumbs.css +1 -1
  71. data/app/components/primer/beta/breadcrumbs.css.json +0 -1
  72. data/app/components/primer/beta/breadcrumbs.css.map +1 -1
  73. data/app/components/primer/beta/breadcrumbs.pcss +2 -8
  74. data/app/components/primer/beta/progress_bar.css +1 -1
  75. data/app/components/primer/beta/progress_bar.css.map +1 -1
  76. data/app/components/primer/beta/progress_bar.pcss +3 -2
  77. data/app/components/primer/beta/relative_time.rb +3 -0
  78. data/app/components/primer/beta/spinner.html.erb +1 -1
  79. data/app/components/primer/beta/spinner.rb +2 -0
  80. data/app/components/primer/primer.d.ts +4 -0
  81. data/app/components/primer/primer.js +4 -0
  82. data/app/components/primer/primer.pcss +2 -0
  83. data/app/components/primer/primer.ts +4 -0
  84. data/app/components/primer/shared_events.d.ts +15 -0
  85. data/app/components/primer/shared_events.ts +19 -0
  86. data/app/controllers/primer/view_components/tree_view_items.json +293 -0
  87. data/app/controllers/primer/view_components/tree_view_items_controller.rb +55 -0
  88. data/app/forms/check_box_with_nested_form.rb +10 -10
  89. data/app/forms/radio_button_with_nested_form.rb +16 -16
  90. data/app/views/primer/view_components/tree_view_items/async_alpha.html_fragment.erb +23 -0
  91. data/app/views/primer/view_components/tree_view_items/index.html_fragment.erb +24 -0
  92. data/config/routes.rb +2 -0
  93. data/lib/primer/view_components/version.rb +2 -2
  94. data/previews/primer/alpha/file_tree_view_preview/default.html.erb +16 -0
  95. data/previews/primer/alpha/file_tree_view_preview/playground.html.erb +4 -0
  96. data/previews/primer/alpha/file_tree_view_preview.rb +69 -0
  97. data/previews/primer/alpha/skeleton_box_preview.rb +20 -0
  98. data/previews/primer/alpha/tree_view_preview/async_alpha.html.erb +12 -0
  99. data/previews/primer/alpha/tree_view_preview/buttons.html.erb +10 -0
  100. data/previews/primer/alpha/tree_view_preview/default.html.erb +24 -0
  101. data/previews/primer/alpha/tree_view_preview/empty.html.erb +10 -0
  102. data/previews/primer/alpha/tree_view_preview/form_input.html.erb +14 -0
  103. data/previews/primer/alpha/tree_view_preview/leaf_node_playground.html.erb +15 -0
  104. data/previews/primer/alpha/tree_view_preview/links.html.erb +17 -0
  105. data/previews/primer/alpha/tree_view_preview/loading_failure.html.erb +36 -0
  106. data/previews/primer/alpha/tree_view_preview/loading_skeleton.html.erb +12 -0
  107. data/previews/primer/alpha/tree_view_preview/loading_spinner.html.erb +12 -0
  108. data/previews/primer/alpha/tree_view_preview/playground.html.erb +4 -0
  109. data/previews/primer/alpha/tree_view_preview.rb +208 -0
  110. data/static/arguments.json +456 -0
  111. data/static/audited_at.json +17 -0
  112. data/static/classes.json +15 -0
  113. data/static/constants.json +101 -0
  114. data/static/info_arch.json +1410 -56
  115. data/static/previews.json +232 -0
  116. data/static/statuses.json +17 -0
  117. metadata +89 -8
@@ -0,0 +1,470 @@
1
+ import {controller, target} from '@github/catalyst'
2
+ import {TreeViewIconPairElement} from './tree_view_icon_pair_element'
3
+ import {observeMutationsUntilConditionMet} from '../../utils'
4
+ import {TreeViewIncludeFragmentElement} from './tree_view_include_fragment_element'
5
+ import {TreeViewElement} from './tree_view'
6
+ import type {TreeViewNodeInfo} from '../../shared_events'
7
+
8
+ type LoadingState = 'loading' | 'error' | 'success'
9
+
10
+ export type SelectStrategy = 'self' | 'descendants' | 'mixed_descendants'
11
+
12
+ @controller
13
+ export class TreeViewSubTreeNodeElement extends HTMLElement {
14
+ @target node: HTMLElement
15
+ @target subTree: HTMLElement
16
+ @target iconPair: TreeViewIconPairElement
17
+ @target toggleButton: HTMLElement
18
+ @target expandedToggleIcon: HTMLElement
19
+ @target collapsedToggleIcon: HTMLElement
20
+ @target includeFragment: TreeViewIncludeFragmentElement
21
+ @target loadingIndicator: HTMLElement
22
+ @target loadingFailureMessage: HTMLElement
23
+ @target retryButton: HTMLButtonElement
24
+
25
+ #expanded: boolean | null = null
26
+ #loadingState: LoadingState = 'success'
27
+ #abortController: AbortController
28
+ #activeElementIsLoader: boolean = false
29
+
30
+ connectedCallback() {
31
+ observeMutationsUntilConditionMet(
32
+ this,
33
+ () => Boolean(this.node) && Boolean(this.subTree),
34
+ () => {
35
+ this.#update()
36
+ },
37
+ )
38
+
39
+ const {signal} = (this.#abortController = new AbortController())
40
+ this.addEventListener('click', this, {signal})
41
+ this.addEventListener('keydown', this, {signal})
42
+
43
+ observeMutationsUntilConditionMet(
44
+ this,
45
+ () => Boolean(this.includeFragment),
46
+ () => {
47
+ this.includeFragment.addEventListener('loadstart', this, {signal})
48
+ this.includeFragment.addEventListener('error', this, {signal})
49
+ this.includeFragment.addEventListener('include-fragment-replace', this, {signal})
50
+ this.includeFragment.addEventListener(
51
+ 'include-fragment-replaced',
52
+ (e: Event) => {
53
+ this.#handleIncludeFragmentEvent(e)
54
+ },
55
+ {signal},
56
+ )
57
+ },
58
+ )
59
+
60
+ observeMutationsUntilConditionMet(
61
+ this,
62
+ () => Boolean(this.retryButton),
63
+ () => {
64
+ this.retryButton.addEventListener(
65
+ 'click',
66
+ event => {
67
+ this.#handleRetryButtonEvent(event)
68
+ },
69
+ {signal},
70
+ )
71
+ },
72
+ )
73
+
74
+ const checkedMutationObserver = new MutationObserver(() => {
75
+ if (this.selectStrategy !== 'mixed_descendants') return
76
+
77
+ let checkType = 'unknown'
78
+
79
+ for (const node of this.eachDirectDescendantNode()) {
80
+ switch (`${checkType} ${node.getAttribute('aria-checked') || 'false'}`) {
81
+ case 'unknown mixed':
82
+ case 'false mixed':
83
+ case 'true mixed':
84
+ case 'false true':
85
+ case 'true false':
86
+ checkType = 'mixed'
87
+ break
88
+
89
+ case 'unknown false':
90
+ checkType = 'false'
91
+ break
92
+
93
+ case 'unknown true':
94
+ checkType = 'true'
95
+ }
96
+ }
97
+
98
+ if (checkType !== 'unknown' && this.node?.getAttribute('aria-checked') !== checkType) {
99
+ this.node?.setAttribute('aria-checked', checkType)
100
+ }
101
+ })
102
+
103
+ checkedMutationObserver.observe(this, {
104
+ childList: true,
105
+ subtree: true,
106
+ attributeFilter: ['aria-checked'],
107
+ })
108
+ }
109
+
110
+ get expanded(): boolean {
111
+ if (this.#expanded === null) {
112
+ this.#expanded = this.node.getAttribute('aria-expanded') === 'true'
113
+ }
114
+
115
+ return this.#expanded
116
+ }
117
+
118
+ set expanded(newValue: boolean) {
119
+ this.#expanded = newValue
120
+ this.#update()
121
+ }
122
+
123
+ get loadingState(): LoadingState {
124
+ return this.#loadingState
125
+ }
126
+
127
+ set loadingState(newState: LoadingState) {
128
+ this.#loadingState = newState
129
+ this.#update()
130
+ }
131
+
132
+ get selectStrategy(): SelectStrategy {
133
+ return (this.node.getAttribute('data-select-strategy') || 'descendants') as SelectStrategy
134
+ }
135
+
136
+ disconnectedCallback() {
137
+ this.#abortController.abort()
138
+ }
139
+
140
+ handleEvent(event: Event) {
141
+ if (event.target === this.toggleButton) {
142
+ this.#handleToggleEvent(event)
143
+ } else if (event.target === this.includeFragment) {
144
+ this.#handleIncludeFragmentEvent(event)
145
+ } else if (event instanceof KeyboardEvent) {
146
+ this.#handleKeyboardEvent(event)
147
+ } else if (
148
+ (event.target as Element).closest('[role=treeitem]') === this.node &&
149
+ event.type === 'click' &&
150
+ this.#checkboxElement
151
+ ) {
152
+ this.#handleCheckboxEvent(event)
153
+ }
154
+ }
155
+
156
+ expand() {
157
+ const alreadyExpanded = this.expanded
158
+
159
+ this.expanded = true
160
+
161
+ if (!alreadyExpanded && this.treeView) {
162
+ this.treeView.dispatchEvent(
163
+ new CustomEvent('treeViewNodeExpanded', {
164
+ bubbles: true,
165
+ detail: this.treeView?.infoFromNode(this.node),
166
+ }),
167
+ )
168
+ }
169
+ }
170
+
171
+ collapse() {
172
+ const alreadyCollapsed = !this.expanded
173
+
174
+ this.expanded = false
175
+
176
+ if (!alreadyCollapsed && this.treeView) {
177
+ // Prevent issue where currently focusable node is stuck inside a collapsed
178
+ // sub-tree and no node in the entire tree can be focused
179
+ const previousNode = this.subTree.querySelector("[tabindex='0']")
180
+ previousNode?.setAttribute('tabindex', '-1')
181
+ this.node.setAttribute('tabindex', '0')
182
+
183
+ this.treeView.dispatchEvent(
184
+ new CustomEvent('treeViewNodeCollapsed', {
185
+ bubbles: true,
186
+ detail: this.treeView?.infoFromNode(this.node),
187
+ }),
188
+ )
189
+ }
190
+ }
191
+
192
+ toggle() {
193
+ if (this.expanded) {
194
+ this.collapse()
195
+ } else {
196
+ this.expand()
197
+ }
198
+ }
199
+
200
+ get nodes(): NodeListOf<Element> {
201
+ return this.querySelectorAll(':scope > [role=treeitem]')
202
+ }
203
+
204
+ *eachDirectDescendantNode(): Generator<Element> {
205
+ for (const leaf of this.subTree.querySelectorAll(':scope > li > .TreeViewItemContainer > [role=treeitem]')) {
206
+ yield leaf
207
+ }
208
+
209
+ for (const subTree of this.subTree.querySelectorAll(
210
+ ':scope > tree-view-sub-tree-node > li > .TreeViewItemContainer > [role=treeitem]',
211
+ )) {
212
+ yield subTree
213
+ }
214
+ }
215
+
216
+ *eachDescendantNode(): Generator<Element> {
217
+ for (const node of this.subTree.querySelectorAll('[role=treeitem]')) {
218
+ yield node
219
+ }
220
+ }
221
+
222
+ *eachAncestorSubTreeNode(): Generator<TreeViewSubTreeNodeElement> {
223
+ if (!this.treeView) return
224
+
225
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
226
+ let current: TreeViewSubTreeNodeElement | null = this
227
+
228
+ while (current && this.treeView.contains(current)) {
229
+ yield current
230
+
231
+ current = current.parentElement?.closest('tree-view-sub-tree-node') as TreeViewSubTreeNodeElement | null
232
+ }
233
+ }
234
+
235
+ get isEmpty(): boolean {
236
+ return this.nodes.length === 0
237
+ }
238
+
239
+ get treeView(): TreeViewElement | null {
240
+ return this.closest('tree-view')
241
+ }
242
+
243
+ #handleToggleEvent(event: Event) {
244
+ if (event.type === 'click') {
245
+ this.toggle()
246
+ // eslint-disable-next-line no-restricted-syntax
247
+ event.stopPropagation()
248
+ }
249
+ }
250
+
251
+ #handleIncludeFragmentEvent(event: Event) {
252
+ switch (event.type) {
253
+ // the request has started
254
+ case 'loadstart':
255
+ this.loadingState = 'loading'
256
+ break
257
+
258
+ // the request failed
259
+ case 'error':
260
+ this.loadingState = 'error'
261
+ break
262
+
263
+ // request succeeded but element has not yet been replaced
264
+ case 'include-fragment-replace':
265
+ this.#activeElementIsLoader = document.activeElement === this.loadingIndicator.closest('[role=treeitem]')
266
+ this.loadingState = 'success'
267
+ break
268
+
269
+ case 'include-fragment-replaced':
270
+ // Make sure to expand the new sub-tree, otherwise it looks like nothing happened. This prevents
271
+ // having to remember to pass `SubTree.new(expanded: true)` in the controller.
272
+ this.expanded = true
273
+
274
+ if (this.#activeElementIsLoader) {
275
+ const firstItem = this.querySelector('[role=group] > :first-child') as HTMLElement | null
276
+ if (!firstItem) return
277
+
278
+ const content = firstItem.querySelector('[role=treeitem]') as HTMLElement | null
279
+ if (!content) return
280
+
281
+ content.focus()
282
+ }
283
+
284
+ this.#activeElementIsLoader = false
285
+ break
286
+ }
287
+ }
288
+
289
+ #handleRetryButtonEvent(event: Event) {
290
+ if (event.type === 'click') {
291
+ this.loadingState = 'loading'
292
+ this.includeFragment.refetch()
293
+ }
294
+ }
295
+
296
+ #handleKeyboardEvent(event: KeyboardEvent) {
297
+ const node = (event.target as HTMLElement).closest('[role=treeitem]')
298
+ if (!node || this.treeView?.getNodeType(node) !== 'sub-tree') {
299
+ return
300
+ }
301
+
302
+ switch (event.key) {
303
+ case 'Enter':
304
+ if (this.treeView?.getNodeDisabledValue(node)) {
305
+ event.preventDefault()
306
+ break
307
+ }
308
+
309
+ // eslint-disable-next-line no-restricted-syntax
310
+ event.stopPropagation()
311
+
312
+ if (this.#checkboxElement) {
313
+ this.toggleChecked()
314
+ } else if (!this.treeView?.nodeHasNativeAction(node)) {
315
+ // toggle only if this node isn't eg. an anchor or button
316
+ this.toggle()
317
+ }
318
+
319
+ break
320
+
321
+ case 'ArrowRight':
322
+ // eslint-disable-next-line no-restricted-syntax
323
+ event.stopPropagation()
324
+ this.expand()
325
+ break
326
+
327
+ case 'ArrowLeft':
328
+ // eslint-disable-next-line no-restricted-syntax
329
+ event.stopPropagation()
330
+ this.collapse()
331
+ break
332
+
333
+ case ' ':
334
+ if (this.treeView?.getNodeDisabledValue(node)) {
335
+ event.preventDefault()
336
+ break
337
+ }
338
+
339
+ if (this.#checkboxElement) {
340
+ // eslint-disable-next-line no-restricted-syntax
341
+ event.stopPropagation()
342
+ event.preventDefault()
343
+
344
+ this.toggleChecked()
345
+ } else {
346
+ if (node instanceof HTMLAnchorElement) {
347
+ // simulate click on space for anchors (buttons already handle this natively)
348
+ node.click()
349
+ } else if (!this.treeView?.nodeHasNativeAction(node)) {
350
+ this.toggle()
351
+ }
352
+ }
353
+
354
+ break
355
+ }
356
+ }
357
+
358
+ #handleCheckboxEvent(event: Event) {
359
+ if (this.treeView?.getNodeDisabledValue(this.node)) {
360
+ event.preventDefault()
361
+ return
362
+ }
363
+
364
+ if (event.type !== 'click') return
365
+
366
+ this.toggleChecked()
367
+
368
+ // prevent receiving this event twice
369
+ // eslint-disable-next-line no-restricted-syntax
370
+ event.stopPropagation()
371
+ }
372
+
373
+ toggleChecked() {
374
+ const checkValue = this.treeView?.getNodeCheckedValue(this.node) || 'false'
375
+ const newCheckValue = checkValue === 'false' ? 'true' : 'false'
376
+ const nodeInfos: TreeViewNodeInfo[] = []
377
+ const rootInfo = this.treeView?.infoFromNode(this.node, newCheckValue)
378
+ if (rootInfo) nodeInfos.push(rootInfo)
379
+
380
+ if (this.selectStrategy === 'descendants' || this.selectStrategy === 'mixed_descendants') {
381
+ for (const node of this.eachDescendantNode()) {
382
+ const info = this.treeView?.infoFromNode(node, newCheckValue)
383
+ if (info) nodeInfos.push(info)
384
+ }
385
+ }
386
+
387
+ const checkSuccess = this.dispatchEvent(
388
+ new CustomEvent('treeViewBeforeNodeChecked', {
389
+ bubbles: true,
390
+ cancelable: true,
391
+ detail: nodeInfos,
392
+ }),
393
+ )
394
+
395
+ if (!checkSuccess) return
396
+
397
+ for (const nodeInfo of nodeInfos) {
398
+ nodeInfo.node.setAttribute('aria-checked', newCheckValue)
399
+ }
400
+
401
+ this.dispatchEvent(
402
+ new CustomEvent('treeViewNodeChecked', {
403
+ bubbles: true,
404
+ cancelable: true,
405
+ detail: nodeInfos,
406
+ }),
407
+ )
408
+ }
409
+
410
+ #update() {
411
+ if (this.expanded) {
412
+ if (this.subTree) this.subTree.hidden = false
413
+ this.node.setAttribute('aria-expanded', 'true')
414
+ this.treeView?.expandAncestorsForNode(this)
415
+
416
+ if (this.iconPair) {
417
+ this.iconPair.showExpanded()
418
+ }
419
+
420
+ if (this.expandedToggleIcon && this.collapsedToggleIcon) {
421
+ this.expandedToggleIcon.removeAttribute('hidden')
422
+ this.collapsedToggleIcon.setAttribute('hidden', 'hidden')
423
+ }
424
+ } else {
425
+ if (this.subTree) this.subTree.hidden = true
426
+ this.node.setAttribute('aria-expanded', 'false')
427
+
428
+ if (this.iconPair) {
429
+ this.iconPair.showCollapsed()
430
+ }
431
+
432
+ if (this.expandedToggleIcon && this.collapsedToggleIcon) {
433
+ this.expandedToggleIcon.setAttribute('hidden', 'hidden')
434
+ this.collapsedToggleIcon.removeAttribute('hidden')
435
+ }
436
+ }
437
+
438
+ switch (this.loadingState) {
439
+ case 'loading':
440
+ if (this.loadingFailureMessage) this.loadingFailureMessage.hidden = true
441
+ if (this.loadingIndicator) this.loadingIndicator.hidden = false
442
+ break
443
+
444
+ case 'error':
445
+ if (this.loadingIndicator) this.loadingIndicator.hidden = true
446
+ if (this.loadingFailureMessage) this.loadingFailureMessage.hidden = false
447
+ break
448
+
449
+ // success/init case
450
+ default:
451
+ if (this.loadingIndicator) this.loadingIndicator.hidden = true
452
+ if (this.loadingFailureMessage) this.loadingFailureMessage.hidden = true
453
+ }
454
+ }
455
+
456
+ get #checkboxElement(): HTMLElement | null {
457
+ return this.querySelector('.TreeViewItemCheckbox')
458
+ }
459
+ }
460
+
461
+ if (!window.customElements.get('tree-view-sub-tree-node')) {
462
+ window.TreeViewSubTreeNodeElement = TreeViewSubTreeNodeElement
463
+ window.customElements.define('tree-view-sub-tree-node', TreeViewSubTreeNodeElement)
464
+ }
465
+
466
+ declare global {
467
+ interface Window {
468
+ TreeViewSubTreeNodeElement: typeof TreeViewSubTreeNodeElement
469
+ }
470
+ }
@@ -0,0 +1,14 @@
1
+ <%= render(
2
+ Primer::BaseComponent.new(
3
+ tag: :div,
4
+ classes: "sr-only TreeViewVisuallyHidden",
5
+ aria: { hidden: true },
6
+ id: @id,
7
+ test_selector: "tree-view-visual-label"
8
+ )
9
+ ) do %>
10
+ <%= @label %>
11
+ <% end %>
12
+ <div class="TreeViewItemVisual" aria-hidden="true">
13
+ <%= render(@visual) %>
14
+ </div>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module Alpha
5
+ class TreeView
6
+ # A `TreeView` visual, either leading or trailing.
7
+ #
8
+ # This component is part of the <%= link_to_component(Primer::Alpha::TreeView) %> component and should
9
+ # not be used directly.
10
+ class Visual < Primer::Component
11
+ # @param id [String] This visual's HTML ID.
12
+ # @param visual [ViewComponent::Base] A renderable component like an instance of <%= link_to_component(Primer::Beta::Octicon) %> to render as the visual.
13
+ # @param label [String] Text describing this visual that will be visible only to screen readers.
14
+ def initialize(id:, visual:, label: nil)
15
+ @id = id
16
+ @visual = visual
17
+ @label = label
18
+ end
19
+
20
+ def render_in(_view_context, &block)
21
+ block&.call(@visual)
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ .TreeViewRootUlStyles{list-style:none;margin:0;padding:0}.TreeViewRootUlStyles .TreeViewItem{outline:none}:is(.TreeViewRootUlStyles .TreeViewItem):focus-visible>div{box-shadow:var(--boxShadow-thick) var(--fgColor-accent)}@media (forced-colors:active){:is(.TreeViewRootUlStyles .TreeViewItem):focus-visible>div{outline:2px solid HighlightText;outline-offset:-2}}[data-has-leading-action]:is(.TreeViewRootUlStyles .TreeViewItem){--has-leading-action:1}.TreeViewRootUlStyles .TreeViewItemContainer{--level:1;--toggle-width:1rem;--min-item-height:2rem;border-radius:var(--borderRadius-medium);color:var(--fgColor-default);display:grid;font-size:var(--text-body-size-medium);grid-template-areas:"spacer leadingAction toggle content";grid-template-columns:var(--spacer-width) var(--leading-action-width) var(--toggle-width) 1fr;position:relative;width:100%;--leading-action-width:calc(var(--has-leading-action, 0)*1.5rem);--spacer-width:calc((var(--level) - 1)*(var(--toggle-width)/2))}:is(.TreeViewRootUlStyles .TreeViewItemContainer):hover{background-color:var(--control-transparent-bgColor-hover)}@media (forced-colors:active){:is(.TreeViewRootUlStyles .TreeViewItemContainer):hover{outline:2px solid #0000;outline-offset:-2px}}@media (pointer:coarse){.TreeViewRootUlStyles .TreeViewItemContainer{--toggle-width:1.5rem;--min-item-height:2.75rem}}:is(.TreeViewRootUlStyles .TreeViewItemContainer):has(.TreeViewFailureMessage):hover{background-color:initial;cursor:default}@media (forced-colors:active){:is(.TreeViewRootUlStyles .TreeViewItemContainer):has(.TreeViewFailureMessage):hover{outline:none}}:is(.TreeViewRootUlStyles .TreeViewItemContainer):has([role=treeitem]:focus-visible){box-shadow:var(--boxShadow-thick) var(--fgColor-accent)}.TreeViewRootUlStyles:where([data-omit-spacer=true]) .TreeViewItemContainer{grid-template-columns:0 0 0 1fr}.TreeViewRootUlStyles .TreeViewItem>.TreeViewItemContainer:has(.TreeViewItemContent[aria-current=true]){background-color:var(--control-transparent-bgColor-selected)}:is(.TreeViewRootUlStyles .TreeViewItem>.TreeViewItemContainer:has(.TreeViewItemContent[aria-current=true])):after{background-color:var(--fgColor-accent);border-radius:var(--borderRadius-medium);content:"";height:1.5rem;left:calc(var(--base-size-8)*-1);position:absolute;top:calc(50% - var(--base-size-12));width:.25rem}@media (forced-colors:active){:is(.TreeViewRootUlStyles .TreeViewItem>.TreeViewItemContainer:has(.TreeViewItemContent[aria-current=true])):after{background-color:HighlightText}}.TreeViewRootUlStyles .TreeViewItemToggle{align-items:flex-start;color:var(--fgColor-muted);cursor:pointer;display:flex;grid-area:toggle;height:100%;justify-content:center;padding-top:calc(var(--min-item-height)/2 - var(--base-size-12)/2)}.TreeViewRootUlStyles .TreeViewItemToggleHover:hover{background-color:var(--control-transparent-bgColor-hover)}.TreeViewRootUlStyles .TreeViewItemToggleEnd{border-bottom-left-radius:var(--borderRadius-medium);border-top-left-radius:var(--borderRadius-medium)}.TreeViewRootUlStyles a.TreeViewItemContent:hover,.TreeViewRootUlStyles button.TreeViewItemContent:hover{-webkit-text-decoration:underline;text-decoration:underline;text-decoration-color:var(--control-fgColor-rest)}.TreeViewRootUlStyles :has(.TreeViewItemContent[aria-disabled=true]){cursor:not-allowed}.TreeViewRootUlStyles .TreeViewItemContent{cursor:pointer;display:flex;gap:var(--stack-gap-condensed);grid-area:content;height:100%;line-height:var(--custom-line-height,var(--text-body-lineHeight-medium,1.4285));outline:none;padding:0 var(--base-size-8);padding-bottom:calc((var(--min-item-height) - var(--custom-line-height, 1.3rem))/2);padding-top:calc((var(--min-item-height) - var(--custom-line-height, 1.3rem))/2)}.TreeViewRootUlStyles .TreeViewItemContent,:is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemCheckbox{background-color:initial;border:none;text-align:left;touch-action:manipulation;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}:is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemCheckbox{border-radius:var(--borderRadius-medium);color:var(--control-fgColor-rest);position:relative;transition:background 33.333ms linear}[aria-checked=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox{background:var(--control-checked-bgColor-rest);border-color:var(--control-checked-borderColor-rest);transition:background-color,border-color 80ms cubic-bezier(.32,0,.67,0) 0s}:is([aria-checked=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox):before{animation:checkmarkIn 80ms cubic-bezier(.65,0,.35,1) 80ms forwards;transition:visibility 0s linear 0s;visibility:visible}[aria-checked=mixed]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox{background:var(--control-checked-bgColor-rest);border-color:var(--control-checked-borderColor-rest);transition:background-color,border-color 80ms cubic-bezier(.32,0,.67,0) 0s}:is([aria-checked=mixed]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox):before{animation:checkmarkIn 80ms cubic-bezier(.65,0,.35,1) 80ms forwards;clip-path:none;mask-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMCIgaGVpZ2h0PSIyIiBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMTAgMiI+PHBhdGggZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMCAxYTEgMSAwIDAgMSAxLTFoOGExIDEgMCAxIDEgMCAySDFhMSAxIDAgMCAxLTEtMSIgY2xpcC1ydWxlPSJldmVub2RkIi8+PC9zdmc+");visibility:visible}[aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent){pointer-events:none}[aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemContentText{color:var(--control-fgColor-disabled)}:is([aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemVisual) svg,[aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemVisual{fill:var(--control-fgColor-disabled)}@media (hover:hover){:is([aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox):hover{cursor:not-allowed}[aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent):hover{background-color:initial;cursor:not-allowed}}.TreeViewRootUlStyles .TreeViewItemContentText{color:var(--control-fgColor-rest);flex:1 1 auto;width:0}.TreeViewRootUlStyles:where([data-truncate-text=true]) .TreeViewItemContentText{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.TreeViewRootUlStyles:where([data-truncate-text=false]) .TreeViewItemContentText{word-break:break-word}.TreeViewRootUlStyles .TreeViewItemVisual{align-items:center;color:var(--fgColor-muted);display:flex;height:var(--custom-line-height,1.3rem)}.TreeViewRootUlStyles .TreeViewItemLeadingAction{color:var(--fgColor-muted);display:flex;grid-area:leadingAction}:is(.TreeViewRootUlStyles .TreeViewItemLeadingAction)>button{flex-shrink:1}.TreeViewRootUlStyles .TreeViewItemLevelLine{border-color:var(--borderColor-muted);border-right:var(--borderWidth-thin) solid;height:100%;width:100%}@media (hover:hover){.TreeViewRootUlStyles .TreeViewItemLevelLine{border-color:#0000}.TreeViewRootUlStyles:focus-within .TreeViewItemLevelLine,.TreeViewRootUlStyles:hover .TreeViewItemLevelLine{border-color:var(--borderColor-muted)}}.TreeViewRootUlStyles .TreeViewVisuallyHidden{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border-width:0;white-space:nowrap}.TreeViewSkeletonItemContainerStyle{align-items:center;column-gap:.5rem;display:flex;height:2rem}@media (pointer:coarse){.TreeViewSkeletonItemContainerStyle{height:2.75rem}}.TreeViewSkeletonItemContainerStyle:nth-of-type(5n+1){--tree-item-loading-width:67%}.TreeViewSkeletonItemContainerStyle:nth-of-type(5n+2){--tree-item-loading-width:47%}.TreeViewSkeletonItemContainerStyle:nth-of-type(5n+3){--tree-item-loading-width:73%}.TreeViewSkeletonItemContainerStyle:nth-of-type(5n+4){--tree-item-loading-width:64%}.TreeViewSkeletonItemContainerStyle:nth-of-type(5n+5){--tree-item-loading-width:50%}.TreeItemSkeletonTextStyles{width:var(--tree-item-loading-width,67%)}.TreeViewFailureMessage{align-items:center;display:grid;gap:.5rem;grid-template-columns:auto 1fr;width:100%}
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "alpha/tree_view",
3
+ "selectors": [
4
+ ".TreeViewRootUlStyles",
5
+ ".TreeViewRootUlStyles .TreeViewItem",
6
+ ":is(.TreeViewRootUlStyles .TreeViewItem):focus-visible>div",
7
+ "[data-has-leading-action]:is(.TreeViewRootUlStyles .TreeViewItem)",
8
+ ".TreeViewRootUlStyles .TreeViewItemContainer",
9
+ ":is(.TreeViewRootUlStyles .TreeViewItemContainer):hover",
10
+ ":is(.TreeViewRootUlStyles .TreeViewItemContainer):has(.TreeViewFailureMessage):hover",
11
+ ":is(.TreeViewRootUlStyles .TreeViewItemContainer):has([role=treeitem]:focus-visible)",
12
+ ".TreeViewRootUlStyles:where([data-omit-spacer=true]) .TreeViewItemContainer",
13
+ ".TreeViewRootUlStyles .TreeViewItem>.TreeViewItemContainer:has(.TreeViewItemContent[aria-current=true])",
14
+ ":is(.TreeViewRootUlStyles .TreeViewItem>.TreeViewItemContainer:has(.TreeViewItemContent[aria-current=true])):after",
15
+ ".TreeViewRootUlStyles .TreeViewItemToggle",
16
+ ".TreeViewRootUlStyles .TreeViewItemToggleHover:hover",
17
+ ".TreeViewRootUlStyles .TreeViewItemToggleEnd",
18
+ ".TreeViewRootUlStyles a.TreeViewItemContent:hover",
19
+ ".TreeViewRootUlStyles button.TreeViewItemContent:hover",
20
+ ".TreeViewRootUlStyles :has(.TreeViewItemContent[aria-disabled=true])",
21
+ ".TreeViewRootUlStyles .TreeViewItemContent",
22
+ ":is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemCheckbox",
23
+ "[aria-checked=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox",
24
+ ":is([aria-checked=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox):before",
25
+ "[aria-checked=mixed]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox",
26
+ ":is([aria-checked=mixed]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox):before",
27
+ "[aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent)",
28
+ "[aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemContentText",
29
+ ":is([aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemVisual) svg",
30
+ "[aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .TreeViewItemVisual",
31
+ ":is([aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent) .FormControl-checkbox):hover",
32
+ "[aria-disabled=true]:is(.TreeViewRootUlStyles .TreeViewItemContent):hover",
33
+ ".TreeViewRootUlStyles .TreeViewItemContentText",
34
+ ".TreeViewRootUlStyles:where([data-truncate-text=true]) .TreeViewItemContentText",
35
+ ".TreeViewRootUlStyles:where([data-truncate-text=false]) .TreeViewItemContentText",
36
+ ".TreeViewRootUlStyles .TreeViewItemVisual",
37
+ ".TreeViewRootUlStyles .TreeViewItemLeadingAction",
38
+ ":is(.TreeViewRootUlStyles .TreeViewItemLeadingAction)>button",
39
+ ".TreeViewRootUlStyles .TreeViewItemLevelLine",
40
+ ".TreeViewRootUlStyles:focus-within .TreeViewItemLevelLine",
41
+ ".TreeViewRootUlStyles:hover .TreeViewItemLevelLine",
42
+ ".TreeViewRootUlStyles .TreeViewVisuallyHidden",
43
+ ".TreeViewSkeletonItemContainerStyle",
44
+ ".TreeViewSkeletonItemContainerStyle:nth-of-type(5n+1)",
45
+ ".TreeViewSkeletonItemContainerStyle:nth-of-type(5n+2)",
46
+ ".TreeViewSkeletonItemContainerStyle:nth-of-type(5n+3)",
47
+ ".TreeViewSkeletonItemContainerStyle:nth-of-type(5n+4)",
48
+ ".TreeViewSkeletonItemContainerStyle:nth-of-type(5n+5)",
49
+ ".TreeItemSkeletonTextStyles",
50
+ ".TreeViewFailureMessage"
51
+ ]
52
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["tree_view.pcss"],"names":[],"mappings":"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","file":"tree_view.css","sourcesContent":["/* stylelint-disable selector-max-type -- Copied from primer/react */\n\n.TreeViewRootUlStyles {\n padding: 0;\n margin: 0;\n list-style: none;\n\n /*\n * WARNING: This is a performance optimization.\n *\n * We define styles for the tree items at the root level of the tree\n * to avoid recomputing the styles for each item when the tree updates.\n * We're sacrificing maintainability for performance because TreeView\n * needs to be performant enough to handle large trees (thousands of items).\n *\n * This is intended to be a temporary solution until we can improve the\n * performance of our styling patterns.\n *\n * Do NOT copy this pattern without understanding the tradeoffs.\n */\n & .TreeViewItem {\n outline: none;\n\n &:focus-visible > div {\n box-shadow: var(--boxShadow-thick) var(--fgColor-accent);\n\n @media (forced-colors: active) {\n outline: 2px solid HighlightText;\n /* stylelint-disable-next-line declaration-property-value-no-unknown -- Copied from primer/react */\n outline-offset: -2;\n }\n }\n\n &[data-has-leading-action] {\n --has-leading-action: 1;\n }\n }\n\n & .TreeViewItemContainer {\n --level: 1;\n --toggle-width: 1rem;\n --min-item-height: 2rem;\n\n position: relative;\n display: grid;\n width: 100%;\n font-size: var(--text-body-size-medium);\n color: var(--fgColor-default);\n border-radius: var(--borderRadius-medium);\n grid-template-columns: var(--spacer-width) var(--leading-action-width) var(--toggle-width) 1fr;\n grid-template-areas: 'spacer leadingAction toggle content';\n\n --leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem);\n --spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2));\n\n &:hover {\n background-color: var(--control-transparent-bgColor-hover);\n\n @media (forced-colors: active) {\n outline: 2px solid transparent;\n outline-offset: -2px;\n }\n }\n\n @media (pointer: coarse) {\n --toggle-width: 1.5rem;\n --min-item-height: 2.75rem;\n }\n\n &:has(.TreeViewFailureMessage):hover {\n cursor: default;\n background-color: transparent;\n\n @media (forced-colors: active) {\n outline: none;\n }\n }\n\n &:has([role='treeitem']:focus-visible) {\n box-shadow: var(--boxShadow-thick) var(--fgColor-accent);\n }\n }\n\n &:where([data-omit-spacer='true']) .TreeViewItemContainer {\n grid-template-columns: 0 0 0 1fr;\n }\n\n /* stylelint-disable-next-line selector-max-specificity */\n & .TreeViewItem > .TreeViewItemContainer:has(.TreeViewItemContent[aria-current='true']) {\n background-color: var(--control-transparent-bgColor-selected);\n\n /* Current item indicator */\n /* stylelint-disable-next-line selector-max-specificity -- Copied from primer/react */\n &::after {\n position: absolute;\n top: calc(50% - var(--base-size-12));\n left: calc(-1 * var(--base-size-8));\n width: 0.25rem;\n height: 1.5rem;\n content: '';\n\n /*\n * Use fgColor accent for consistency across all themes. Using the \"correct\" variable,\n * --bgColor-accent-emphasis, causes vrt failures for dark high contrast mode\n */\n /* stylelint-disable-next-line primer/colors */\n background-color: var(--fgColor-accent);\n border-radius: var(--borderRadius-medium);\n\n @media (forced-colors: active) {\n background-color: HighlightText;\n }\n }\n }\n\n & .TreeViewItemToggle {\n display: flex;\n height: 100%;\n\n /* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap\n across more lines. */\n /* stylelint-disable-next-line primer/spacing */\n padding-top: calc(var(--min-item-height) / 2 - var(--base-size-12) / 2);\n color: var(--fgColor-muted);\n grid-area: toggle;\n justify-content: center;\n align-items: flex-start;\n cursor: pointer;\n }\n\n & .TreeViewItemToggleHover:hover {\n background-color: var(--control-transparent-bgColor-hover);\n }\n\n & .TreeViewItemToggleEnd {\n border-top-left-radius: var(--borderRadius-medium);\n border-bottom-left-radius: var(--borderRadius-medium);\n }\n\n /* stylelint-disable-next-line selector-no-qualifying-type */\n & a.TreeViewItemContent:hover, button.TreeViewItemContent:hover {\n text-decoration: underline;\n text-decoration-color: var(--control-fgColor-rest);\n }\n\n & :has(.TreeViewItemContent[aria-disabled=\"true\"]) {\n cursor: not-allowed;\n }\n\n & .TreeViewItemContent {\n display: flex;\n height: 100%;\n padding: 0 var(--base-size-8);\n outline: none;\n text-align: left;\n user-select: none;\n background-color: transparent;\n border: none;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n cursor: pointer;\n\n /* The dynamic top and bottom padding to maintain the minimum item height for single line items */\n /* stylelint-disable-next-line primer/spacing */\n padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);\n /* stylelint-disable-next-line primer/spacing */\n padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);\n line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285));\n grid-area: content;\n gap: var(--stack-gap-condensed);\n\n & .TreeViewItemCheckbox {\n position: relative;\n color: var(--control-fgColor-rest);\n text-align: left;\n user-select: none;\n background-color: transparent;\n border: none;\n border-radius: var(--borderRadius-medium);\n transition: background 33.333ms linear;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n\n &[aria-checked='true'] {\n & .FormControl-checkbox {\n background: var(--control-checked-bgColor-rest);\n border-color: var(--control-checked-borderColor-rest);\n transition: background-color, border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; /* unchecked -> checked */\n\n /* stylelint-disable-next-line max-nesting-depth, selector-max-specificity -- Copied from primer/react */\n &::before {\n visibility: visible;\n transition: visibility 0s linear 0s;\n animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms;\n }\n }\n }\n\n &[aria-checked='mixed'] {\n & .FormControl-checkbox {\n background: var(--control-checked-bgColor-rest);\n border-color: var(--control-checked-borderColor-rest);\n transition: background-color, border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; /* unchecked -> checked */\n\n /* stylelint-disable-next-line max-nesting-depth, selector-max-specificity -- Copied from primer/react */\n &::before {\n visibility: visible;\n mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMiIgdmlld0JveD0iMCAwIDEwIDIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxQzAgMC40NDc3MTUgMC40NDc3MTUgMCAxIDBIOUM5LjU1MjI5IDAgMTAgMC40NDc3MTUgMTAgMUMxMCAxLjU1MjI4IDkuNTUyMjkgMiA5IDJIMUMwLjQ0NzcxNSAyIDAgMS41NTIyOCAwIDFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K');\n animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms;\n clip-path: none;\n }\n }\n }\n\n &[aria-disabled='true'] {\n pointer-events: none;\n\n & .TreeViewItemContentText {\n color: var(--control-fgColor-disabled);\n }\n\n & .TreeViewItemVisual {\n fill: var(--control-fgColor-disabled);\n\n /* stylelint-disable-next-line max-nesting-depth, selector-max-compound-selectors, selector-max-specificity */\n & svg {\n fill: var(--control-fgColor-disabled);\n }\n }\n\n & .FormControl-checkbox {\n /* stylelint-disable-next-line max-nesting-depth */\n @media (hover: hover) {\n /* stylelint-disable-next-line max-nesting-depth, selector-max-specificity */\n &:hover {\n cursor: not-allowed;\n }\n }\n }\n\n @media (hover: hover) {\n /* stylelint-disable-next-line max-nesting-depth */\n &:hover {\n cursor: not-allowed;\n background-color: transparent;\n }\n }\n }\n }\n\n & .TreeViewItemContentText {\n color: var(--control-fgColor-rest);\n flex: 1 1 auto;\n width: 0;\n }\n\n &:where([data-truncate-text='true']) .TreeViewItemContentText {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n &:where([data-truncate-text='false']) .TreeViewItemContentText {\n /* stylelint-disable-next-line declaration-property-value-keyword-no-deprecated -- Copied from primer/react */\n word-break: break-word;\n }\n\n & .TreeViewItemVisual {\n display: flex;\n\n /* The visual icons should appear vertically centered for single-line items, but remain at the top for items that wrap\n across more lines. */\n height: var(--custom-line-height, 1.3rem);\n color: var(--fgColor-muted);\n align-items: center;\n }\n\n & .TreeViewItemLeadingAction {\n display: flex;\n color: var(--fgColor-muted);\n grid-area: leadingAction;\n\n & > button {\n flex-shrink: 1;\n }\n }\n\n & .TreeViewItemLevelLine {\n width: 100%;\n height: 100%;\n\n /*\n * On devices without hover, the nesting indicator lines\n * appear at all times.\n */\n border-color: var(--borderColor-muted);\n border-right: var(--borderWidth-thin) solid;\n }\n\n /*\n * On devices with :hover support, the nesting indicator lines\n * fade in when the user mouses over the entire component,\n * or when there's focus inside the component. This makes\n * sure the component remains simple when not in use.\n */\n @media (hover: hover) {\n .TreeViewItemLevelLine {\n border-color: transparent;\n }\n\n &:hover .TreeViewItemLevelLine,\n &:focus-within .TreeViewItemLevelLine {\n border-color: var(--borderColor-muted);\n }\n }\n\n & .TreeViewVisuallyHidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n /* stylelint-disable-next-line primer/spacing */\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n }\n}\n\n.TreeViewSkeletonItemContainerStyle {\n display: flex;\n align-items: center;\n column-gap: 0.5rem;\n height: 2rem;\n\n @media (pointer: coarse) {\n height: 2.75rem;\n }\n\n &:nth-of-type(5n + 1) {\n --tree-item-loading-width: 67%;\n }\n\n &:nth-of-type(5n + 2) {\n --tree-item-loading-width: 47%;\n }\n\n &:nth-of-type(5n + 3) {\n --tree-item-loading-width: 73%;\n }\n\n &:nth-of-type(5n + 4) {\n --tree-item-loading-width: 64%;\n }\n\n &:nth-of-type(5n + 5) {\n --tree-item-loading-width: 50%;\n }\n}\n\n.TreeItemSkeletonTextStyles {\n width: var(--tree-item-loading-width, 67%);\n}\n\n.TreeViewFailureMessage {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 0.5rem;\n width: 100%;\n align-items: center;\n}\n"]}
@@ -0,0 +1,12 @@
1
+ <tree-view>
2
+ <% if acts_as_form_input? %>
3
+ <%= @form_arguments[:builder].hidden_field(@form_arguments[:name], multiple: true, skip_default_ids: true, form: "", data: { target: "tree-view.formInputPrototype" }) %>
4
+ <div data-target="tree-view.formInputContainer"></div>
5
+ <% end %>
6
+
7
+ <%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
8
+ <% nodes.each do |node| %>
9
+ <%= node %>
10
+ <% end %>
11
+ <% end %>
12
+ </tree-view>