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,130 @@
1
+ import { FocusKeys, focusZone } from '@primer/behaviors';
2
+ // This code was adapted from the roving tab index implementation in primer/react, see:
3
+ // https://github.com/primer/react/blob/f9785343716435f43e3d82482b057a17bd345c25/packages/react/src/TreeView/useRovingTabIndex.ts
4
+ export function useRovingTabIndex(containerEl) {
5
+ // TODO: Initialize focus to the aria-current item if it exists
6
+ focusZone(containerEl, {
7
+ bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd | FocusKeys.Backspace,
8
+ getNextFocusable: (_direction, from, event) => {
9
+ if (!(from instanceof HTMLElement))
10
+ return;
11
+ // Skip elements within a modal dialog
12
+ // This need to be in a try/catch to avoid errors in
13
+ // non-supported browsers
14
+ try {
15
+ if (from.closest('dialog:modal')) {
16
+ return;
17
+ }
18
+ }
19
+ catch {
20
+ // Don't return
21
+ }
22
+ return getNextFocusableElement(from, event) ?? from;
23
+ },
24
+ focusInStrategy: () => {
25
+ let currentItem = containerEl.querySelector('[aria-current]');
26
+ currentItem = currentItem?.checkVisibility() ? currentItem : null;
27
+ const firstItem = containerEl.querySelector('[role="treeitem"]');
28
+ // Focus the aria-current item if it exists
29
+ if (currentItem instanceof HTMLElement) {
30
+ return currentItem;
31
+ }
32
+ // Otherwise, focus the activeElement if it's a treeitem
33
+ if (document.activeElement instanceof HTMLElement &&
34
+ containerEl.contains(document.activeElement) &&
35
+ document.activeElement.getAttribute('role') === 'treeitem') {
36
+ return document.activeElement;
37
+ }
38
+ // Otherwise, focus the first treeitem
39
+ return firstItem instanceof HTMLElement ? firstItem : undefined;
40
+ },
41
+ });
42
+ }
43
+ // DOM utilities used for focus management
44
+ function getNextFocusableElement(activeElement, event) {
45
+ const elementState = getElementState(activeElement);
46
+ // Reference: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboard-interaction-24
47
+ switch (`${elementState} ${event.key}`) {
48
+ case 'open ArrowRight':
49
+ // Focus first child node
50
+ return getFirstChildElement(activeElement);
51
+ case 'open ArrowLeft':
52
+ // Close node; don't change focus
53
+ return;
54
+ case 'closed ArrowRight':
55
+ // Open node; don't change focus
56
+ return;
57
+ case 'closed ArrowLeft':
58
+ // Focus parent element
59
+ return getParentElement(activeElement);
60
+ case 'end ArrowRight':
61
+ // Do nothing
62
+ return;
63
+ case 'end ArrowLeft':
64
+ // Focus parent element
65
+ return getParentElement(activeElement);
66
+ }
67
+ // ArrowUp and ArrowDown behavior is the same regardless of element state
68
+ switch (event.key) {
69
+ case 'ArrowUp':
70
+ // Focus previous visible element
71
+ return getVisibleElement(activeElement, 'previous');
72
+ case 'ArrowDown':
73
+ // Focus next visible element
74
+ return getVisibleElement(activeElement, 'next');
75
+ case 'Backspace':
76
+ return getParentElement(activeElement);
77
+ }
78
+ }
79
+ export function getElementState(element) {
80
+ if (element.getAttribute('role') !== 'treeitem') {
81
+ throw new Error('Element is not a treeitem');
82
+ }
83
+ switch (element.getAttribute('aria-expanded')) {
84
+ case 'true':
85
+ return 'open';
86
+ case 'false':
87
+ return 'closed';
88
+ default:
89
+ return 'end';
90
+ }
91
+ }
92
+ function getVisibleElement(element, direction) {
93
+ const root = element.closest('[role=tree]');
94
+ if (!root)
95
+ return;
96
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, node => {
97
+ if (!(node instanceof HTMLElement))
98
+ return NodeFilter.FILTER_SKIP;
99
+ return node.getAttribute('role') === 'treeitem' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
100
+ });
101
+ let current = walker.firstChild();
102
+ while (current !== element) {
103
+ current = walker.nextNode();
104
+ }
105
+ let next = direction === 'next' ? walker.nextNode() : walker.previousNode();
106
+ // If next element is nested inside a collapsed subtree, continue iterating
107
+ while (next instanceof HTMLElement && collapsedParent(next, root)) {
108
+ next = direction === 'next' ? walker.nextNode() : walker.previousNode();
109
+ }
110
+ return next instanceof HTMLElement ? next : undefined;
111
+ }
112
+ function collapsedParent(node, root) {
113
+ for (const ancestor of root.querySelectorAll('[role=treeitem][aria-expanded=false]')) {
114
+ if (node === ancestor)
115
+ continue;
116
+ if (ancestor.closest('li')?.contains(node)) {
117
+ return ancestor;
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ function getFirstChildElement(element) {
123
+ const firstChild = element.querySelector('[role=treeitem]');
124
+ return firstChild instanceof HTMLElement ? firstChild : undefined;
125
+ }
126
+ function getParentElement(element) {
127
+ const group = element.closest('[role=group]');
128
+ const parent = group?.closest('[role=treeitem]');
129
+ return parent instanceof HTMLElement ? parent : undefined;
130
+ }
@@ -0,0 +1,161 @@
1
+ import {TreeViewElement} from './tree_view'
2
+ import {FocusKeys, focusZone} from '@primer/behaviors'
3
+
4
+ // This code was adapted from the roving tab index implementation in primer/react, see:
5
+ // https://github.com/primer/react/blob/f9785343716435f43e3d82482b057a17bd345c25/packages/react/src/TreeView/useRovingTabIndex.ts
6
+ export function useRovingTabIndex(containerEl: TreeViewElement) {
7
+ // TODO: Initialize focus to the aria-current item if it exists
8
+ focusZone(containerEl, {
9
+ bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd | FocusKeys.Backspace,
10
+ getNextFocusable: (_direction, from, event) => {
11
+ if (!(from instanceof HTMLElement)) return
12
+
13
+ // Skip elements within a modal dialog
14
+ // This need to be in a try/catch to avoid errors in
15
+ // non-supported browsers
16
+ try {
17
+ if (from.closest('dialog:modal')) {
18
+ return
19
+ }
20
+ } catch {
21
+ // Don't return
22
+ }
23
+
24
+ return getNextFocusableElement(from, event) ?? from
25
+ },
26
+ focusInStrategy: () => {
27
+ let currentItem = containerEl.querySelector('[aria-current]')
28
+ currentItem = currentItem?.checkVisibility() ? currentItem : null
29
+
30
+ const firstItem = containerEl.querySelector('[role="treeitem"]')
31
+
32
+ // Focus the aria-current item if it exists
33
+ if (currentItem instanceof HTMLElement) {
34
+ return currentItem
35
+ }
36
+
37
+ // Otherwise, focus the activeElement if it's a treeitem
38
+ if (
39
+ document.activeElement instanceof HTMLElement &&
40
+ containerEl.contains(document.activeElement) &&
41
+ document.activeElement.getAttribute('role') === 'treeitem'
42
+ ) {
43
+ return document.activeElement
44
+ }
45
+
46
+ // Otherwise, focus the first treeitem
47
+ return firstItem instanceof HTMLElement ? firstItem : undefined
48
+ },
49
+ })
50
+ }
51
+
52
+ // DOM utilities used for focus management
53
+
54
+ function getNextFocusableElement(activeElement: HTMLElement, event: KeyboardEvent): HTMLElement | undefined {
55
+ const elementState = getElementState(activeElement)
56
+
57
+ // Reference: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboard-interaction-24
58
+ switch (`${elementState} ${event.key}`) {
59
+ case 'open ArrowRight':
60
+ // Focus first child node
61
+ return getFirstChildElement(activeElement)
62
+
63
+ case 'open ArrowLeft':
64
+ // Close node; don't change focus
65
+ return
66
+
67
+ case 'closed ArrowRight':
68
+ // Open node; don't change focus
69
+ return
70
+
71
+ case 'closed ArrowLeft':
72
+ // Focus parent element
73
+ return getParentElement(activeElement)
74
+
75
+ case 'end ArrowRight':
76
+ // Do nothing
77
+ return
78
+
79
+ case 'end ArrowLeft':
80
+ // Focus parent element
81
+ return getParentElement(activeElement)
82
+ }
83
+
84
+ // ArrowUp and ArrowDown behavior is the same regardless of element state
85
+ switch (event.key) {
86
+ case 'ArrowUp':
87
+ // Focus previous visible element
88
+ return getVisibleElement(activeElement, 'previous')
89
+
90
+ case 'ArrowDown':
91
+ // Focus next visible element
92
+ return getVisibleElement(activeElement, 'next')
93
+
94
+ case 'Backspace':
95
+ return getParentElement(activeElement)
96
+ }
97
+ }
98
+
99
+ export function getElementState(element: HTMLElement): 'open' | 'closed' | 'end' {
100
+ if (element.getAttribute('role') !== 'treeitem') {
101
+ throw new Error('Element is not a treeitem')
102
+ }
103
+
104
+ switch (element.getAttribute('aria-expanded')) {
105
+ case 'true':
106
+ return 'open'
107
+ case 'false':
108
+ return 'closed'
109
+ default:
110
+ return 'end'
111
+ }
112
+ }
113
+
114
+ function getVisibleElement(element: HTMLElement, direction: 'next' | 'previous'): HTMLElement | undefined {
115
+ const root = element.closest('[role=tree]')
116
+
117
+ if (!root) return
118
+
119
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, node => {
120
+ if (!(node instanceof HTMLElement)) return NodeFilter.FILTER_SKIP
121
+ return node.getAttribute('role') === 'treeitem' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
122
+ })
123
+
124
+ let current = walker.firstChild()
125
+
126
+ while (current !== element) {
127
+ current = walker.nextNode()
128
+ }
129
+
130
+ let next = direction === 'next' ? walker.nextNode() : walker.previousNode()
131
+
132
+ // If next element is nested inside a collapsed subtree, continue iterating
133
+ while (next instanceof HTMLElement && collapsedParent(next, root)) {
134
+ next = direction === 'next' ? walker.nextNode() : walker.previousNode()
135
+ }
136
+
137
+ return next instanceof HTMLElement ? next : undefined
138
+ }
139
+
140
+ function collapsedParent(node: Element, root: Element): Element | null {
141
+ for (const ancestor of root.querySelectorAll('[role=treeitem][aria-expanded=false]')) {
142
+ if (node === ancestor) continue
143
+
144
+ if (ancestor.closest('li')?.contains(node)) {
145
+ return ancestor
146
+ }
147
+ }
148
+
149
+ return null
150
+ }
151
+
152
+ function getFirstChildElement(element: HTMLElement): HTMLElement | undefined {
153
+ const firstChild = element.querySelector('[role=treeitem]')
154
+ return firstChild instanceof HTMLElement ? firstChild : undefined
155
+ }
156
+
157
+ function getParentElement(element: HTMLElement): HTMLElement | undefined {
158
+ const group = element.closest('[role=group]')
159
+ const parent = group?.closest('[role=treeitem]')
160
+ return parent instanceof HTMLElement ? parent : undefined
161
+ }
@@ -0,0 +1,42 @@
1
+ import { TreeViewIconPairElement } from './tree_view_icon_pair_element';
2
+ import { TreeViewIncludeFragmentElement } from './tree_view_include_fragment_element';
3
+ import { TreeViewElement } from './tree_view';
4
+ type LoadingState = 'loading' | 'error' | 'success';
5
+ export type SelectStrategy = 'self' | 'descendants' | 'mixed_descendants';
6
+ export declare class TreeViewSubTreeNodeElement extends HTMLElement {
7
+ #private;
8
+ node: HTMLElement;
9
+ subTree: HTMLElement;
10
+ iconPair: TreeViewIconPairElement;
11
+ toggleButton: HTMLElement;
12
+ expandedToggleIcon: HTMLElement;
13
+ collapsedToggleIcon: HTMLElement;
14
+ includeFragment: TreeViewIncludeFragmentElement;
15
+ loadingIndicator: HTMLElement;
16
+ loadingFailureMessage: HTMLElement;
17
+ retryButton: HTMLButtonElement;
18
+ connectedCallback(): void;
19
+ get expanded(): boolean;
20
+ set expanded(newValue: boolean);
21
+ get loadingState(): LoadingState;
22
+ set loadingState(newState: LoadingState);
23
+ get selectStrategy(): SelectStrategy;
24
+ disconnectedCallback(): void;
25
+ handleEvent(event: Event): void;
26
+ expand(): void;
27
+ collapse(): void;
28
+ toggle(): void;
29
+ get nodes(): NodeListOf<Element>;
30
+ eachDirectDescendantNode(): Generator<Element>;
31
+ eachDescendantNode(): Generator<Element>;
32
+ eachAncestorSubTreeNode(): Generator<TreeViewSubTreeNodeElement>;
33
+ get isEmpty(): boolean;
34
+ get treeView(): TreeViewElement | null;
35
+ toggleChecked(): void;
36
+ }
37
+ declare global {
38
+ interface Window {
39
+ TreeViewSubTreeNodeElement: typeof TreeViewSubTreeNodeElement;
40
+ }
41
+ }
42
+ export {};