primer_view_components 0.1.4 → 0.1.5

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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -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 +31 -10
  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 +22 -0
  17. data/app/components/primer/alpha/action_menu/action_menu_element.js +139 -0
  18. data/app/components/primer/alpha/action_menu/action_menu_element.ts +137 -0
  19. data/app/components/primer/alpha/action_menu/list.rb +81 -0
  20. data/app/components/primer/alpha/action_menu.html.erb +26 -0
  21. data/app/components/primer/alpha/action_menu.rb +322 -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.json +0 -63
  26. data/app/components/primer/alpha/dialog.rb +6 -2
  27. data/app/components/primer/alpha/dropdown.css.json +0 -21
  28. data/app/components/primer/alpha/layout.css.json +0 -27
  29. data/app/components/primer/alpha/menu.css.json +0 -11
  30. data/app/components/primer/alpha/nav_list/item.rb +5 -0
  31. data/app/components/primer/alpha/overlay.css +1 -1
  32. data/app/components/primer/alpha/overlay.css.json +0 -3
  33. data/app/components/primer/alpha/overlay.css.map +1 -1
  34. data/app/components/primer/alpha/overlay.pcss +1 -0
  35. data/app/components/primer/alpha/overlay.rb +14 -18
  36. data/app/components/primer/alpha/segmented_control.css.json +0 -15
  37. data/app/components/primer/alpha/tab_nav.css.json +0 -10
  38. data/app/components/primer/alpha/text_field.css.json +0 -38
  39. data/app/components/primer/alpha/toggle_switch.css.json +0 -16
  40. data/app/components/primer/alpha/underline_nav.css.json +0 -13
  41. data/app/components/primer/beta/avatar.css.json +0 -14
  42. data/app/components/primer/beta/avatar_stack.css.json +0 -9
  43. data/app/components/primer/beta/blankslate.css.json +0 -12
  44. data/app/components/primer/beta/border_box.css.json +0 -32
  45. data/app/components/primer/beta/border_box.rb +3 -3
  46. data/app/components/primer/beta/breadcrumbs.css.json +0 -4
  47. data/app/components/primer/beta/button.css +1 -1
  48. data/app/components/primer/beta/button.css.json +0 -22
  49. data/app/components/primer/beta/button.css.map +1 -1
  50. data/app/components/primer/beta/button.pcss +3 -3
  51. data/app/components/primer/beta/counter.css.json +0 -6
  52. data/app/components/primer/beta/flash.css.json +0 -15
  53. data/app/components/primer/beta/label.css.json +0 -20
  54. data/app/components/primer/beta/link.css.json +0 -8
  55. data/app/components/primer/beta/popover.css.json +0 -18
  56. data/app/components/primer/beta/progress_bar.css.json +0 -6
  57. data/app/components/primer/beta/state.css.json +0 -10
  58. data/app/components/primer/beta/subhead.css.json +0 -8
  59. data/app/components/primer/beta/timeline_item.css.json +0 -9
  60. data/app/components/primer/beta/truncate.css.json +0 -6
  61. data/app/components/primer/focus_group.d.ts +19 -0
  62. data/app/components/primer/focus_group.js +144 -0
  63. data/app/components/primer/focus_group.ts +137 -0
  64. data/app/components/primer/icon_button.rb +1 -1
  65. data/app/components/primer/primer.d.ts +2 -0
  66. data/app/components/primer/primer.js +2 -0
  67. data/app/components/primer/primer.ts +2 -0
  68. data/app/components/primer/truncate.css.json +0 -7
  69. data/app/lib/primer/css/layout.css.json +0 -263
  70. data/app/lib/primer/css/utilities.css.json +0 -1636
  71. data/lib/primer/static/generate_arguments.rb +55 -0
  72. data/lib/primer/static/generate_audited_at.rb +17 -0
  73. data/lib/primer/static/generate_constants.rb +19 -0
  74. data/lib/primer/static/generate_info_arch.rb +156 -0
  75. data/lib/primer/static/generate_previews.rb +45 -0
  76. data/lib/primer/static/generate_statuses.rb +17 -0
  77. data/lib/primer/static.rb +72 -0
  78. data/lib/primer/view_components/linters/disallow_component_css_counter.rb +43 -4
  79. data/lib/primer/view_components/version.rb +1 -1
  80. data/lib/primer/view_components.rb +0 -48
  81. data/lib/primer/yard/component_manifest.rb +1 -0
  82. data/lib/primer/yard/component_ref.rb +14 -0
  83. data/lib/primer/yard/docs_helper.rb +3 -0
  84. data/lib/primer/yard/info_arch_docs_helper.rb +31 -0
  85. data/lib/primer/yard/legacy_gatsby_backend.rb +3 -35
  86. data/lib/primer/yard/registry.rb +2 -1
  87. data/lib/primer/yard.rb +1 -0
  88. data/lib/tasks/docs.rake +10 -12
  89. data/lib/tasks/static.rake +20 -28
  90. data/previews/primer/alpha/action_list_preview.rb +4 -1
  91. data/previews/primer/alpha/action_menu_preview/align_end.html.erb +6 -0
  92. data/previews/primer/alpha/action_menu_preview/opens_dialog.html.erb +21 -0
  93. data/previews/primer/alpha/action_menu_preview.rb +238 -0
  94. data/previews/primer/alpha/dialog_preview/body_has_scrollbar_overflow.html.erb +2 -2
  95. data/previews/primer/alpha/dialog_preview/custom_header.html.erb +3 -3
  96. data/previews/primer/alpha/dialog_preview/nested_dialog.html.erb +4 -4
  97. data/previews/primer/alpha/dialog_preview/test.html.erb +3 -3
  98. data/previews/primer/alpha/dialog_preview/with_footer.html.erb +3 -3
  99. data/previews/primer/alpha/dialog_preview/with_form.html.erb +1 -1
  100. data/previews/primer/alpha/dialog_preview/with_text_input.html.erb +2 -2
  101. data/previews/primer/alpha/dialog_preview.rb +7 -2
  102. data/previews/primer/beta/auto_complete_item_preview.rb +1 -0
  103. data/static/arguments.json +3078 -1404
  104. data/static/audited_at.json +2 -0
  105. data/static/classes.json +576 -311
  106. data/static/constants.json +42 -2
  107. data/static/info_arch.json +8859 -0
  108. data/static/previews.json +221 -101
  109. data/static/statuses.json +2 -0
  110. metadata +23 -2
@@ -0,0 +1,139 @@
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;
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"]', '[role="none"]'];
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
+ connectedCallback() {
72
+ const { signal } = (__classPrivateFieldSet(this, _ActionMenuElement_abortController, new AbortController(), "f"));
73
+ this.addEventListener('keydown', this, { signal });
74
+ this.addEventListener('click', this, { signal });
75
+ this.addEventListener('mouseover', this, { signal });
76
+ this.addEventListener('focusout', this, { signal });
77
+ __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_setDynamicLabel).call(this);
78
+ }
79
+ disconnectedCallback() {
80
+ __classPrivateFieldGet(this, _ActionMenuElement_abortController, "f").abort();
81
+ }
82
+ handleEvent(event) {
83
+ var _a, _b, _c, _d;
84
+ if (!((_a = this.popoverElement) === null || _a === void 0 ? void 0 : _a.matches(popoverSelector)))
85
+ return;
86
+ if (event.type === 'focusout' && !this.contains(event.relatedTarget)) {
87
+ (_b = this.popoverElement) === null || _b === void 0 ? void 0 : _b.hidePopover();
88
+ }
89
+ else if ((event instanceof KeyboardEvent &&
90
+ event.type === 'keydown' &&
91
+ !(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
92
+ event.key === 'Enter') ||
93
+ (event instanceof MouseEvent && event.type === 'click')) {
94
+ const item = (_c = event.target.closest(menuItemSelectors.join(','))) === null || _c === void 0 ? void 0 : _c.closest('li');
95
+ if (!item)
96
+ return;
97
+ const ariaChecked = item.getAttribute('aria-checked');
98
+ const checked = ariaChecked !== 'true';
99
+ item.setAttribute('aria-checked', `${checked}`);
100
+ if (this.selectVariant === 'single') {
101
+ const selector = menuItemSelectors.map(s => `li[aria-checked] ${s}`).join(',');
102
+ for (const checkedItemContent of this.querySelectorAll(selector)) {
103
+ const checkedItem = checkedItemContent.closest('li');
104
+ if (checkedItem !== item) {
105
+ checkedItem.setAttribute('aria-checked', 'false');
106
+ }
107
+ }
108
+ __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_setDynamicLabel).call(this);
109
+ }
110
+ event.preventDefault();
111
+ (_d = this.popoverElement) === null || _d === void 0 ? void 0 : _d.hidePopover();
112
+ }
113
+ }
114
+ }
115
+ _ActionMenuElement_abortController = new WeakMap(), _ActionMenuElement_originalLabel = new WeakMap(), _ActionMenuElement_instances = new WeakSet(), _ActionMenuElement_setDynamicLabel = function _ActionMenuElement_setDynamicLabel() {
116
+ if (!this.dynamicLabel)
117
+ return;
118
+ const invoker = this.invokerElement;
119
+ if (!invoker)
120
+ return;
121
+ const selector = menuItemSelectors.map(s => `${s}[aria-checked=true]`).join(',');
122
+ const item = this.querySelector(selector);
123
+ if (item && this.dynamicLabel) {
124
+ __classPrivateFieldSet(this, _ActionMenuElement_originalLabel, __classPrivateFieldGet(this, _ActionMenuElement_originalLabel, "f") || (invoker.textContent || ''), "f");
125
+ const prefixSpan = document.createElement('span');
126
+ prefixSpan.classList.add('color-fg-muted');
127
+ const contentSpan = document.createElement('span');
128
+ prefixSpan.textContent = this.dynamicLabelPrefix;
129
+ contentSpan.textContent = item.textContent || '';
130
+ invoker.replaceChildren(prefixSpan, contentSpan);
131
+ }
132
+ else {
133
+ invoker.textContent = __classPrivateFieldGet(this, _ActionMenuElement_originalLabel, "f");
134
+ }
135
+ };
136
+ if (!window.customElements.get('action-menu')) {
137
+ window.ActionMenuElement = ActionMenuElement;
138
+ window.customElements.define('action-menu', ActionMenuElement);
139
+ }
@@ -0,0 +1,137 @@
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"]', '[role="none"]']
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
+ connectedCallback() {
64
+ const {signal} = (this.#abortController = new AbortController())
65
+ this.addEventListener('keydown', this, {signal})
66
+ this.addEventListener('click', this, {signal})
67
+ this.addEventListener('mouseover', this, {signal})
68
+ this.addEventListener('focusout', this, {signal})
69
+ this.#setDynamicLabel()
70
+ }
71
+
72
+ disconnectedCallback() {
73
+ this.#abortController.abort()
74
+ }
75
+
76
+ handleEvent(event: Event) {
77
+ if (!this.popoverElement?.matches(popoverSelector)) return
78
+
79
+ if (event.type === 'focusout' && !this.contains((event as FocusEvent).relatedTarget as Node)) {
80
+ this.popoverElement?.hidePopover()
81
+ } else if (
82
+ (event instanceof KeyboardEvent &&
83
+ event.type === 'keydown' &&
84
+ !(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) &&
85
+ event.key === 'Enter') ||
86
+ (event instanceof MouseEvent && event.type === 'click')
87
+ ) {
88
+ const item = (event.target as Element).closest(menuItemSelectors.join(','))?.closest('li')
89
+ if (!item) return
90
+ const ariaChecked = item.getAttribute('aria-checked')
91
+ const checked = ariaChecked !== 'true'
92
+ item.setAttribute('aria-checked', `${checked}`)
93
+ if (this.selectVariant === 'single') {
94
+ const selector = menuItemSelectors.map(s => `li[aria-checked] ${s}`).join(',')
95
+ for (const checkedItemContent of this.querySelectorAll(selector)) {
96
+ const checkedItem = checkedItemContent.closest('li')!
97
+ if (checkedItem !== item) {
98
+ checkedItem.setAttribute('aria-checked', 'false')
99
+ }
100
+ }
101
+ this.#setDynamicLabel()
102
+ }
103
+ event.preventDefault()
104
+ this.popoverElement?.hidePopover()
105
+ }
106
+ }
107
+
108
+ #setDynamicLabel() {
109
+ if (!this.dynamicLabel) return
110
+ const invoker = this.invokerElement
111
+ if (!invoker) return
112
+ const selector = menuItemSelectors.map(s => `${s}[aria-checked=true]`).join(',')
113
+ const item = this.querySelector(selector)
114
+ if (item && this.dynamicLabel) {
115
+ this.#originalLabel ||= invoker.textContent || ''
116
+ const prefixSpan = document.createElement('span')
117
+ prefixSpan.classList.add('color-fg-muted')
118
+ const contentSpan = document.createElement('span')
119
+ prefixSpan.textContent = this.dynamicLabelPrefix
120
+ contentSpan.textContent = item.textContent || ''
121
+ invoker.replaceChildren(prefixSpan, contentSpan)
122
+ } else {
123
+ invoker.textContent = this.#originalLabel
124
+ }
125
+ }
126
+ }
127
+
128
+ if (!window.customElements.get('action-menu')) {
129
+ window.ActionMenuElement = ActionMenuElement
130
+ window.customElements.define('action-menu', ActionMenuElement)
131
+ }
132
+
133
+ declare global {
134
+ interface Window {
135
+ ActionMenuElement: typeof ActionMenuElement
136
+ }
137
+ }
@@ -0,0 +1,81 @@
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 = :span
11
+ ITEM_TAG_OPTIONS = [:a, :button, :"clipboard-copy", DEFAULT_ITEM_TAG].freeze
12
+ ITEM_ACTION_OPTIONS = [:classes, :onclick, :href, :value].freeze
13
+
14
+ # Adds a new item to the list.
15
+ #
16
+ # @param system_arguments [Hash] The same arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
17
+ def with_item(**system_arguments, &block)
18
+ content_arguments = system_arguments.delete(:content_arguments) || {}
19
+
20
+ content_arguments[:tag] =
21
+ if system_arguments[:tag] && ITEM_TAG_OPTIONS.include?(system_arguments[:tag])
22
+ system_arguments[:tag]
23
+ elsif system_arguments[:href] && !system_arguments[:disabled]
24
+ :a
25
+ else
26
+ DEFAULT_ITEM_TAG
27
+ end
28
+
29
+ # disallow setting item's tag
30
+ system_arguments.delete(:tag)
31
+
32
+ # rubocop:disable Style/IfUnlessModifier
33
+ if content_arguments[:tag] == :a
34
+ content_arguments[:href] = system_arguments.delete(:href)
35
+ end
36
+ # rubocop:enable Style/IfUnlessModifier
37
+
38
+ system_arguments[:tabindex] = -1
39
+ system_arguments[:autofocus] = "" if system_arguments[:autofocus]
40
+
41
+ if system_arguments[:disabled]
42
+ content_arguments[:aria] = merge_aria(
43
+ content_arguments,
44
+ { aria: { disabled: true } }
45
+ )
46
+
47
+ system_arguments[:aria] = merge_aria(
48
+ system_arguments,
49
+ { aria: { disabled: true } }
50
+ )
51
+
52
+ content_arguments[:disabled] = "" if content_arguments[:tag] == :button
53
+ end
54
+
55
+ super(
56
+ **system_arguments,
57
+ content_arguments: content_arguments,
58
+ &block
59
+ )
60
+ end
61
+
62
+ # @param menu_id [String] ID of the parent menu.
63
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
64
+ def initialize(menu_id:, **system_arguments, &block)
65
+ @menu_id = menu_id
66
+
67
+ system_arguments[:aria] = merge_aria(
68
+ system_arguments,
69
+ { aria: { labelledby: "#{@menu_id}-button" } }
70
+ )
71
+
72
+ system_arguments[:role] = :menu
73
+ system_arguments[:scheme] = :inset
74
+ system_arguments[:id] = "#{@menu_id}-list"
75
+
76
+ super(**system_arguments, &block)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ 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 %>