primer_view_components 0.1.4 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/app/assets/javascripts/primer_view_components.js +1 -1
  4. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  5. data/app/assets/styles/primer_view_components.css +2 -2
  6. data/app/assets/styles/primer_view_components.css.map +1 -1
  7. data/app/components/primer/alpha/action_list/heading.html.erb +1 -1
  8. data/app/components/primer/alpha/action_list/heading.rb +5 -3
  9. data/app/components/primer/alpha/action_list/item.html.erb +9 -0
  10. data/app/components/primer/alpha/action_list/item.rb +32 -11
  11. data/app/components/primer/alpha/action_list.css +1 -1
  12. data/app/components/primer/alpha/action_list.css.json +4 -41
  13. data/app/components/primer/alpha/action_list.css.map +1 -1
  14. data/app/components/primer/alpha/action_list.pcss +19 -20
  15. data/app/components/primer/alpha/action_list.rb +54 -6
  16. data/app/components/primer/alpha/action_menu/action_menu_element.d.ts +23 -0
  17. data/app/components/primer/alpha/action_menu/action_menu_element.js +165 -0
  18. data/app/components/primer/alpha/action_menu/action_menu_element.ts +168 -0
  19. data/app/components/primer/alpha/action_menu/list.rb +91 -0
  20. data/app/components/primer/alpha/action_menu.html.erb +26 -0
  21. data/app/components/primer/alpha/action_menu.rb +361 -0
  22. data/app/components/primer/alpha/auto_complete.css.json +0 -11
  23. data/app/components/primer/alpha/banner.css.json +0 -14
  24. data/app/components/primer/alpha/button_marketing.css.json +0 -10
  25. data/app/components/primer/alpha/dialog.css +1 -1
  26. data/app/components/primer/alpha/dialog.css.json +0 -65
  27. data/app/components/primer/alpha/dialog.css.map +1 -1
  28. data/app/components/primer/alpha/dialog.pcss +0 -4
  29. data/app/components/primer/alpha/dialog.rb +6 -2
  30. data/app/components/primer/alpha/dropdown/menu.rb +1 -1
  31. data/app/components/primer/alpha/dropdown.css.json +0 -21
  32. data/app/components/primer/alpha/layout.css.json +0 -27
  33. data/app/components/primer/alpha/menu.css.json +0 -11
  34. data/app/components/primer/alpha/modal_dialog.js +12 -0
  35. data/app/components/primer/alpha/modal_dialog.ts +17 -0
  36. data/app/components/primer/alpha/nav_list/item.rb +5 -0
  37. data/app/components/primer/alpha/overlay.css +1 -1
  38. data/app/components/primer/alpha/overlay.css.json +0 -3
  39. data/app/components/primer/alpha/overlay.css.map +1 -1
  40. data/app/components/primer/alpha/overlay.pcss +1 -0
  41. data/app/components/primer/alpha/overlay.rb +19 -19
  42. data/app/components/primer/alpha/segmented_control.css.json +0 -15
  43. data/app/components/primer/alpha/tab_nav.css.json +0 -10
  44. data/app/components/primer/alpha/text_field.css.json +0 -38
  45. data/app/components/primer/alpha/toggle_switch.css.json +0 -16
  46. data/app/components/primer/alpha/underline_nav.css.json +0 -13
  47. data/app/components/primer/beta/auto_complete/auto_complete.html.erb +1 -1
  48. data/app/components/primer/beta/auto_complete.rb +19 -1
  49. data/app/components/primer/beta/avatar.css.json +0 -14
  50. data/app/components/primer/beta/avatar_stack.css.json +0 -9
  51. data/app/components/primer/beta/blankslate.css.json +0 -12
  52. data/app/components/primer/beta/border_box.css.json +0 -32
  53. data/app/components/primer/beta/border_box.rb +3 -3
  54. data/app/components/primer/beta/breadcrumbs.css.json +0 -4
  55. data/app/components/primer/beta/button.css +1 -1
  56. data/app/components/primer/beta/button.css.json +0 -24
  57. data/app/components/primer/beta/button.css.map +1 -1
  58. data/app/components/primer/beta/button.pcss +5 -7
  59. data/app/components/primer/beta/counter.css.json +0 -6
  60. data/app/components/primer/beta/flash.css.json +0 -15
  61. data/app/components/primer/beta/label.css.json +0 -20
  62. data/app/components/primer/beta/link.css.json +0 -8
  63. data/app/components/primer/beta/popover.css.json +0 -18
  64. data/app/components/primer/beta/progress_bar.css.json +0 -6
  65. data/app/components/primer/beta/state.css.json +0 -10
  66. data/app/components/primer/beta/subhead.css.json +0 -8
  67. data/app/components/primer/beta/timeline_item.css.json +0 -9
  68. data/app/components/primer/beta/truncate.css.json +0 -6
  69. data/app/components/primer/focus_group.d.ts +19 -0
  70. data/app/components/primer/focus_group.js +144 -0
  71. data/app/components/primer/focus_group.ts +137 -0
  72. data/app/components/primer/icon_button.rb +1 -1
  73. data/app/components/primer/primer.d.ts +2 -0
  74. data/app/components/primer/primer.js +2 -0
  75. data/app/components/primer/primer.ts +2 -0
  76. data/app/components/primer/truncate.css.json +0 -7
  77. data/app/lib/primer/css/layout.css.json +0 -263
  78. data/app/lib/primer/css/utilities.css.json +0 -1636
  79. data/lib/primer/static/generate_arguments.rb +55 -0
  80. data/lib/primer/static/generate_audited_at.rb +17 -0
  81. data/lib/primer/static/generate_constants.rb +19 -0
  82. data/lib/primer/static/generate_info_arch.rb +156 -0
  83. data/lib/primer/static/generate_previews.rb +45 -0
  84. data/lib/primer/static/generate_statuses.rb +17 -0
  85. data/lib/primer/static.rb +72 -0
  86. data/lib/primer/view_components/linters/disallow_component_css_counter.rb +43 -4
  87. data/lib/primer/view_components/version.rb +1 -1
  88. data/lib/primer/view_components.rb +0 -48
  89. data/lib/primer/yard/component_manifest.rb +1 -0
  90. data/lib/primer/yard/component_ref.rb +14 -0
  91. data/lib/primer/yard/docs_helper.rb +3 -0
  92. data/lib/primer/yard/info_arch_docs_helper.rb +31 -0
  93. data/lib/primer/yard/legacy_gatsby_backend.rb +3 -35
  94. data/lib/primer/yard/registry.rb +2 -1
  95. data/lib/primer/yard.rb +1 -0
  96. data/lib/tasks/docs.rake +10 -12
  97. data/lib/tasks/static.rake +20 -28
  98. data/previews/primer/alpha/action_list_preview.rb +4 -1
  99. data/previews/primer/alpha/action_menu_preview/align_end.html.erb +6 -0
  100. data/previews/primer/alpha/action_menu_preview/content_labels.html.erb +9 -0
  101. data/previews/primer/alpha/action_menu_preview/opens_dialog.html.erb +21 -0
  102. data/previews/primer/alpha/action_menu_preview.rb +245 -0
  103. data/previews/primer/alpha/dialog_preview/body_has_scrollbar_overflow.html.erb +2 -2
  104. data/previews/primer/alpha/dialog_preview/custom_header.html.erb +3 -3
  105. data/previews/primer/alpha/dialog_preview/nested_dialog.html.erb +4 -4
  106. data/previews/primer/alpha/dialog_preview/test.html.erb +3 -3
  107. data/previews/primer/alpha/dialog_preview/with_footer.html.erb +3 -3
  108. data/previews/primer/alpha/dialog_preview/with_form.html.erb +1 -1
  109. data/previews/primer/alpha/dialog_preview/with_text_input.html.erb +2 -2
  110. data/previews/primer/alpha/dialog_preview.rb +7 -2
  111. data/previews/primer/beta/auto_complete_item_preview.rb +1 -0
  112. data/previews/primer/beta/auto_complete_preview.rb +36 -23
  113. data/static/arguments.json +3085 -1405
  114. data/static/audited_at.json +2 -0
  115. data/static/classes.json +576 -311
  116. data/static/constants.json +53 -2
  117. data/static/info_arch.json +8888 -0
  118. data/static/previews.json +226 -101
  119. data/static/statuses.json +2 -0
  120. metadata +28 -6
@@ -0,0 +1,165 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _ActionMenuElement_instances, _ActionMenuElement_abortController, _ActionMenuElement_originalLabel, _ActionMenuElement_setDynamicLabel, _ActionMenuElement_isEnterKeydown, _ActionMenuElement_firstItem_get;
13
+ import '@github/include-fragment-element';
14
+ const popoverSelector = (() => {
15
+ try {
16
+ document.querySelector(':open');
17
+ return ':open';
18
+ }
19
+ catch (_a) {
20
+ return '.\\:open';
21
+ }
22
+ })();
23
+ const menuItemSelectors = ['[role="menuitem"]', '[role="menuitemcheckbox"]', '[role="menuitemradio"]'];
24
+ export class ActionMenuElement extends HTMLElement {
25
+ constructor() {
26
+ super(...arguments);
27
+ _ActionMenuElement_instances.add(this);
28
+ _ActionMenuElement_abortController.set(this, void 0);
29
+ _ActionMenuElement_originalLabel.set(this, '');
30
+ }
31
+ get selectVariant() {
32
+ return this.getAttribute('data-select-variant');
33
+ }
34
+ set selectVariant(variant) {
35
+ if (variant) {
36
+ this.setAttribute('data-select-variant', variant);
37
+ }
38
+ else {
39
+ this.removeAttribute('variant');
40
+ }
41
+ }
42
+ get dynamicLabelPrefix() {
43
+ const prefix = this.getAttribute('data-dynamic-label-prefix');
44
+ if (!prefix)
45
+ return '';
46
+ return `${prefix}:`;
47
+ }
48
+ set dynamicLabelPrefix(value) {
49
+ this.setAttribute('data-dynamic-label', value);
50
+ }
51
+ get dynamicLabel() {
52
+ return this.hasAttribute('data-dynamic-label');
53
+ }
54
+ set dynamicLabel(value) {
55
+ this.toggleAttribute('data-dynamic-label', value);
56
+ }
57
+ get popoverElement() {
58
+ return this.querySelector('[popover]');
59
+ }
60
+ get invokerElement() {
61
+ var _a;
62
+ const id = (_a = this.querySelector('[role=menu]')) === null || _a === void 0 ? void 0 : _a.id;
63
+ if (!id)
64
+ return null;
65
+ for (const el of this.querySelectorAll(`[aria-controls]`)) {
66
+ if (el.getAttribute('aria-controls') === id)
67
+ return el;
68
+ }
69
+ return null;
70
+ }
71
+ get invokerLabel() {
72
+ if (!this.invokerElement)
73
+ return null;
74
+ return this.invokerElement.querySelector('.Button-label');
75
+ }
76
+ connectedCallback() {
77
+ const { signal } = (__classPrivateFieldSet(this, _ActionMenuElement_abortController, new AbortController(), "f"));
78
+ this.addEventListener('keydown', this, { signal });
79
+ this.addEventListener('click', this, { signal });
80
+ this.addEventListener('mouseover', this, { signal });
81
+ this.addEventListener('focusout', this, { signal });
82
+ __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_setDynamicLabel).call(this);
83
+ }
84
+ disconnectedCallback() {
85
+ __classPrivateFieldGet(this, _ActionMenuElement_abortController, "f").abort();
86
+ }
87
+ handleEvent(event) {
88
+ var _a, _b, _c, _d;
89
+ if (event.target === this.invokerElement && __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isEnterKeydown).call(this, event)) {
90
+ if (__classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get)) {
91
+ event.preventDefault();
92
+ (_a = this.popoverElement) === null || _a === void 0 ? void 0 : _a.showPopover();
93
+ __classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get).focus();
94
+ return;
95
+ }
96
+ }
97
+ if (!((_b = this.popoverElement) === null || _b === void 0 ? void 0 : _b.matches(popoverSelector)))
98
+ return;
99
+ if (event.type === 'focusout' && !this.contains(event.relatedTarget)) {
100
+ (_c = this.popoverElement) === null || _c === void 0 ? void 0 : _c.hidePopover();
101
+ }
102
+ else if (__classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isEnterKeydown).call(this, event) || (event instanceof MouseEvent && event.type === 'click')) {
103
+ const item = (_d = event.target.closest(menuItemSelectors.join(','))) === null || _d === void 0 ? void 0 : _d.closest('li');
104
+ if (!item)
105
+ return;
106
+ const ariaChecked = item.getAttribute('aria-checked');
107
+ const checked = ariaChecked !== 'true';
108
+ item.setAttribute('aria-checked', `${checked}`);
109
+ if (this.selectVariant === 'single') {
110
+ const selector = menuItemSelectors.map(s => `li[aria-checked] ${s}`).join(',');
111
+ for (const checkedItemContent of this.querySelectorAll(selector)) {
112
+ const checkedItem = checkedItemContent.closest('li');
113
+ if (checkedItem !== item) {
114
+ checkedItem.setAttribute('aria-checked', 'false');
115
+ }
116
+ }
117
+ __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_setDynamicLabel).call(this);
118
+ }
119
+ if (event instanceof KeyboardEvent && event.target instanceof HTMLButtonElement) {
120
+ // prevent buttons from being clicked twice
121
+ event.preventDefault();
122
+ }
123
+ // Hide popover after current event loop to prevent changes in focus from
124
+ // altering the target of the event. Not doing this specifically affects
125
+ // <a> tags. It causes the event to be sent to the currently focused element
126
+ // instead of the anchor, which effectively prevents navigation, i.e. it
127
+ // appears as if hitting enter does nothing. Curiously, clicking instead
128
+ // works fine.
129
+ if (this.selectVariant !== 'multiple') {
130
+ setTimeout(() => { var _a; return (_a = this.popoverElement) === null || _a === void 0 ? void 0 : _a.hidePopover(); });
131
+ }
132
+ }
133
+ }
134
+ }
135
+ _ActionMenuElement_abortController = new WeakMap(), _ActionMenuElement_originalLabel = new WeakMap(), _ActionMenuElement_instances = new WeakSet(), _ActionMenuElement_setDynamicLabel = function _ActionMenuElement_setDynamicLabel() {
136
+ if (!this.dynamicLabel)
137
+ return;
138
+ const invokerLabel = this.invokerLabel;
139
+ if (!invokerLabel)
140
+ return;
141
+ const itemLabel = this.querySelector('[aria-checked=true] .ActionListItem-label');
142
+ if (itemLabel && this.dynamicLabel) {
143
+ __classPrivateFieldSet(this, _ActionMenuElement_originalLabel, __classPrivateFieldGet(this, _ActionMenuElement_originalLabel, "f") || (invokerLabel.textContent || ''), "f");
144
+ const prefixSpan = document.createElement('span');
145
+ prefixSpan.classList.add('color-fg-muted');
146
+ const contentSpan = document.createElement('span');
147
+ prefixSpan.textContent = this.dynamicLabelPrefix;
148
+ contentSpan.textContent = itemLabel.textContent || '';
149
+ invokerLabel.replaceChildren(prefixSpan, contentSpan);
150
+ }
151
+ else {
152
+ invokerLabel.textContent = __classPrivateFieldGet(this, _ActionMenuElement_originalLabel, "f");
153
+ }
154
+ }, _ActionMenuElement_isEnterKeydown = function _ActionMenuElement_isEnterKeydown(event) {
155
+ return (event instanceof KeyboardEvent &&
156
+ event.type === 'keydown' &&
157
+ !(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
158
+ event.key === 'Enter');
159
+ }, _ActionMenuElement_firstItem_get = function _ActionMenuElement_firstItem_get() {
160
+ return this.querySelector(menuItemSelectors.join(','));
161
+ };
162
+ if (!window.customElements.get('action-menu')) {
163
+ window.ActionMenuElement = ActionMenuElement;
164
+ window.customElements.define('action-menu', ActionMenuElement);
165
+ }
@@ -0,0 +1,168 @@
1
+ import '@github/include-fragment-element'
2
+
3
+ const popoverSelector = (() => {
4
+ try {
5
+ document.querySelector(':open')
6
+ return ':open'
7
+ } catch {
8
+ return '.\\:open'
9
+ }
10
+ })()
11
+
12
+ type SelectVariant = 'single' | 'multiple' | null
13
+
14
+ const menuItemSelectors = ['[role="menuitem"]', '[role="menuitemcheckbox"]', '[role="menuitemradio"]']
15
+
16
+ export class ActionMenuElement extends HTMLElement {
17
+ #abortController: AbortController
18
+ #originalLabel = ''
19
+
20
+ get selectVariant(): SelectVariant {
21
+ return this.getAttribute('data-select-variant') as SelectVariant
22
+ }
23
+
24
+ set selectVariant(variant: SelectVariant) {
25
+ if (variant) {
26
+ this.setAttribute('data-select-variant', variant)
27
+ } else {
28
+ this.removeAttribute('variant')
29
+ }
30
+ }
31
+
32
+ get dynamicLabelPrefix(): string {
33
+ const prefix = this.getAttribute('data-dynamic-label-prefix')
34
+ if (!prefix) return ''
35
+ return `${prefix}:`
36
+ }
37
+
38
+ set dynamicLabelPrefix(value: string) {
39
+ this.setAttribute('data-dynamic-label', value)
40
+ }
41
+
42
+ get dynamicLabel(): boolean {
43
+ return this.hasAttribute('data-dynamic-label')
44
+ }
45
+
46
+ set dynamicLabel(value: boolean) {
47
+ this.toggleAttribute('data-dynamic-label', value)
48
+ }
49
+
50
+ get popoverElement(): HTMLElement | null {
51
+ return this.querySelector<HTMLElement>('[popover]')
52
+ }
53
+
54
+ get invokerElement(): HTMLElement | null {
55
+ const id = this.querySelector('[role=menu]')?.id
56
+ if (!id) return null
57
+ for (const el of this.querySelectorAll(`[aria-controls]`)) {
58
+ if (el.getAttribute('aria-controls') === id) return el as HTMLElement
59
+ }
60
+ return null
61
+ }
62
+
63
+ get invokerLabel(): HTMLElement | null {
64
+ if (!this.invokerElement) return null
65
+ return this.invokerElement.querySelector('.Button-label')
66
+ }
67
+
68
+ connectedCallback() {
69
+ const {signal} = (this.#abortController = new AbortController())
70
+ this.addEventListener('keydown', this, {signal})
71
+ this.addEventListener('click', this, {signal})
72
+ this.addEventListener('mouseover', this, {signal})
73
+ this.addEventListener('focusout', this, {signal})
74
+ this.#setDynamicLabel()
75
+ }
76
+
77
+ disconnectedCallback() {
78
+ this.#abortController.abort()
79
+ }
80
+
81
+ handleEvent(event: Event) {
82
+ if (event.target === this.invokerElement && this.#isEnterKeydown(event)) {
83
+ if (this.#firstItem) {
84
+ event.preventDefault()
85
+ this.popoverElement?.showPopover()
86
+ this.#firstItem.focus()
87
+ return
88
+ }
89
+ }
90
+
91
+ if (!this.popoverElement?.matches(popoverSelector)) return
92
+
93
+ if (event.type === 'focusout' && !this.contains((event as FocusEvent).relatedTarget as Node)) {
94
+ this.popoverElement?.hidePopover()
95
+ } else if (this.#isEnterKeydown(event) || (event instanceof MouseEvent && event.type === 'click')) {
96
+ const item = (event.target as Element).closest(menuItemSelectors.join(','))?.closest('li')
97
+ if (!item) return
98
+ const ariaChecked = item.getAttribute('aria-checked')
99
+ const checked = ariaChecked !== 'true'
100
+ item.setAttribute('aria-checked', `${checked}`)
101
+ if (this.selectVariant === 'single') {
102
+ const selector = menuItemSelectors.map(s => `li[aria-checked] ${s}`).join(',')
103
+ for (const checkedItemContent of this.querySelectorAll(selector)) {
104
+ const checkedItem = checkedItemContent.closest('li')!
105
+ if (checkedItem !== item) {
106
+ checkedItem.setAttribute('aria-checked', 'false')
107
+ }
108
+ }
109
+ this.#setDynamicLabel()
110
+ }
111
+ if (event instanceof KeyboardEvent && event.target instanceof HTMLButtonElement) {
112
+ // prevent buttons from being clicked twice
113
+ event.preventDefault()
114
+ }
115
+ // Hide popover after current event loop to prevent changes in focus from
116
+ // altering the target of the event. Not doing this specifically affects
117
+ // <a> tags. It causes the event to be sent to the currently focused element
118
+ // instead of the anchor, which effectively prevents navigation, i.e. it
119
+ // appears as if hitting enter does nothing. Curiously, clicking instead
120
+ // works fine.
121
+ if (this.selectVariant !== 'multiple') {
122
+ setTimeout(() => this.popoverElement?.hidePopover())
123
+ }
124
+ }
125
+ }
126
+
127
+ #setDynamicLabel() {
128
+ if (!this.dynamicLabel) return
129
+ const invokerLabel = this.invokerLabel
130
+ if (!invokerLabel) return
131
+ const itemLabel = this.querySelector('[aria-checked=true] .ActionListItem-label')
132
+ if (itemLabel && this.dynamicLabel) {
133
+ this.#originalLabel ||= invokerLabel.textContent || ''
134
+ const prefixSpan = document.createElement('span')
135
+ prefixSpan.classList.add('color-fg-muted')
136
+ const contentSpan = document.createElement('span')
137
+ prefixSpan.textContent = this.dynamicLabelPrefix
138
+ contentSpan.textContent = itemLabel.textContent || ''
139
+ invokerLabel.replaceChildren(prefixSpan, contentSpan)
140
+ } else {
141
+ invokerLabel.textContent = this.#originalLabel
142
+ }
143
+ }
144
+
145
+ #isEnterKeydown(event: Event): boolean {
146
+ return (
147
+ event instanceof KeyboardEvent &&
148
+ event.type === 'keydown' &&
149
+ !(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
150
+ event.key === 'Enter'
151
+ )
152
+ }
153
+
154
+ get #firstItem(): HTMLElement | null {
155
+ return this.querySelector(menuItemSelectors.join(','))
156
+ }
157
+ }
158
+
159
+ if (!window.customElements.get('action-menu')) {
160
+ window.ActionMenuElement = ActionMenuElement
161
+ window.customElements.define('action-menu', ActionMenuElement)
162
+ }
163
+
164
+ declare global {
165
+ interface Window {
166
+ ActionMenuElement: typeof ActionMenuElement
167
+ }
168
+ }
@@ -0,0 +1,91 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Primer
5
+ module Alpha
6
+ class ActionMenu
7
+ # This component is part of <%= link_to_component(Primer::Alpha::ActionMenu) %> and should not be
8
+ # used as a standalone component.
9
+ class List < Primer::Alpha::ActionList
10
+ DEFAULT_ITEM_TAG = :button
11
+ ITEM_TAG_OPTIONS = [:a, :button, :"clipboard-copy", DEFAULT_ITEM_TAG].freeze
12
+
13
+ # Adds a new item to the list.
14
+ #
15
+ # @param system_arguments [Hash] The same arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
16
+ def with_item(**system_arguments, &block)
17
+ content_arguments = system_arguments.delete(:content_arguments) || {}
18
+
19
+ content_arguments[:tag] =
20
+ if system_arguments[:tag] && ITEM_TAG_OPTIONS.include?(system_arguments[:tag])
21
+ system_arguments[:tag]
22
+ elsif system_arguments[:href] && !system_arguments[:disabled]
23
+ :a
24
+ else
25
+ DEFAULT_ITEM_TAG
26
+ end
27
+
28
+ # disallow setting item's tag
29
+ system_arguments.delete(:tag)
30
+
31
+ # rubocop:disable Style/IfUnlessModifier
32
+ if content_arguments[:tag] == :a
33
+ content_arguments[:href] ||= system_arguments.delete(:href)
34
+ end
35
+ # rubocop:enable Style/IfUnlessModifier
36
+
37
+ content_arguments[:tabindex] = -1
38
+ system_arguments[:autofocus] = "" if system_arguments[:autofocus]
39
+
40
+ if system_arguments[:disabled]
41
+ content_arguments[:aria] = merge_aria(
42
+ content_arguments,
43
+ { aria: { disabled: true } }
44
+ )
45
+
46
+ system_arguments[:aria] = merge_aria(
47
+ system_arguments,
48
+ { aria: { disabled: true } }
49
+ )
50
+
51
+ content_arguments[:disabled] = "" if content_arguments[:tag] == :button
52
+ end
53
+
54
+ super(**system_arguments, content_arguments: content_arguments) do |item|
55
+ # Prevent double renders by using the capture method on the component
56
+ # that originally received the block.
57
+ #
58
+ # Handle blocks that originate from C code such as `&:method` by checking
59
+ # source_location. Such blocks don't allow access to their receiver.
60
+ if block&.source_location
61
+ block_context = block.binding.receiver
62
+
63
+ if block_context.class < ActionView::Base
64
+ block_context.capture(item, &block)
65
+ else
66
+ capture(item, &block)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ # @param menu_id [String] ID of the parent menu.
73
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
74
+ def initialize(menu_id:, **system_arguments, &block)
75
+ @menu_id = menu_id
76
+
77
+ system_arguments[:aria] = merge_aria(
78
+ system_arguments,
79
+ { aria: { labelledby: "#{@menu_id}-button" } }
80
+ )
81
+
82
+ system_arguments[:role] = :menu
83
+ system_arguments[:scheme] = :inset
84
+ system_arguments[:id] = "#{@menu_id}-list"
85
+
86
+ super(**system_arguments, &block)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,26 @@
1
+ <%= render Primer::BaseComponent.new(**@system_arguments) do %>
2
+ <focus-group direction="vertical" mnemonics retain>
3
+ <%= render(@overlay) do |overlay| %>
4
+ <% if @src.present? %>
5
+ <include-fragment src="<%= @src %>" loading="<%= preload? ? "eager" : "lazy" %>" data-target="action-menu.includeFragment">
6
+ <%= render(Primer::Alpha::ActionMenu::List.new(id: "#{@menu_id}-list", menu_id: @menu_id)) do |list| %>
7
+ <% list.with_item(
8
+ aria: { disabled: true },
9
+ content_arguments: {
10
+ display: :flex,
11
+ align_items: :center,
12
+ justify_content: :center,
13
+ text_align: :center,
14
+ autofocus: true
15
+ }
16
+ ) do %>
17
+ <%= render Primer::Beta::Spinner.new(aria: { label: "Loading content..." }) %>
18
+ <% end %>
19
+ <% end %>
20
+ </include-fragment>
21
+ <% else %>
22
+ <%= render(@list) %>
23
+ <% end %>
24
+ <% end %>
25
+ </focus-group>
26
+ <% end %>