primer_view_components 0.36.5 → 0.37.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/app/assets/javascripts/components/primer/beta/details_toggle_element.d.ts +39 -0
  4. data/app/assets/javascripts/components/primer/primer.d.ts +1 -0
  5. data/app/assets/javascripts/primer_view_components.js +1 -1
  6. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  7. data/app/components/primer/alpha/action_menu/action_menu_element.js +13 -3
  8. data/app/components/primer/alpha/action_menu/action_menu_element.ts +14 -2
  9. data/app/components/primer/alpha/dropdown.rb +8 -0
  10. data/app/components/primer/alpha/form_control.rb +47 -7
  11. data/app/components/primer/alpha/toggle_switch.html.erb +1 -1
  12. data/app/components/primer/alpha/toggle_switch.js +1 -0
  13. data/app/components/primer/alpha/toggle_switch.rb +14 -2
  14. data/app/components/primer/alpha/toggle_switch.ts +1 -0
  15. data/app/components/primer/beta/details.html.erb +8 -6
  16. data/app/components/primer/beta/details.rb +42 -0
  17. data/app/components/primer/beta/details_toggle_element.d.ts +39 -0
  18. data/app/components/primer/beta/details_toggle_element.js +60 -0
  19. data/app/components/primer/beta/details_toggle_element.ts +57 -0
  20. data/app/components/primer/beta/markdown.rb +1 -0
  21. data/app/components/primer/beta/nav_list.rb +1 -1
  22. data/app/components/primer/primer.d.ts +1 -0
  23. data/app/components/primer/primer.js +1 -0
  24. data/app/components/primer/primer.ts +1 -0
  25. data/app/lib/primer/forms/action_menu.html.erb +1 -1
  26. data/app/lib/primer/forms/action_menu.rb +5 -0
  27. data/lib/primer/view_components/version.rb +2 -2
  28. data/lib/primer/yard/component_manifest.rb +11 -10
  29. data/lib/primer/yard/lookbook_pages_backend.rb +8 -0
  30. data/previews/primer/alpha/action_menu_preview/multiple_select_form.html.erb +1 -1
  31. data/previews/primer/alpha/form_control_preview/playground.html.erb +14 -6
  32. data/previews/primer/alpha/overlay_preview.rb +0 -31
  33. data/previews/primer/alpha/select_preview.rb +6 -6
  34. data/previews/primer/alpha/text_field_preview.rb +22 -22
  35. data/previews/primer/alpha/toggle_switch_preview.rb +4 -0
  36. data/previews/primer/beta/details_preview.rb +12 -0
  37. data/previews/primer/beta/markdown_preview.rb +9 -9
  38. data/previews/primer/beta/relative_time_preview.rb +20 -10
  39. data/static/arguments.json +12 -0
  40. data/static/constants.json +4 -0
  41. data/static/info_arch.json +134 -70
  42. data/static/previews.json +27 -52
  43. metadata +6 -4
  44. data/previews/primer/alpha/overlay_preview/in_an_action_menu.html.erb +0 -13
  45. data/previews/primer/alpha/overlay_preview/overlay_with_header_filter.html.erb +0 -16
@@ -129,6 +129,11 @@ let ActionMenuElement = class ActionMenuElement extends HTMLElement {
129
129
  }
130
130
  }), "f");
131
131
  observeMutationsUntilConditionMet(this, () => Boolean(this.invokerElement), () => __classPrivateFieldGet(this, _ActionMenuElement_intersectionObserver, "f").observe(this.invokerElement));
132
+ // If there's no include fragment, then no async fetching will occur and we can
133
+ // mark the component as ready.
134
+ if (!this.includeFragment) {
135
+ this.setAttribute('data-ready', 'true');
136
+ }
132
137
  }
133
138
  disconnectedCallback() {
134
139
  __classPrivateFieldGet(this, _ActionMenuElement_abortController, "f").abort();
@@ -137,7 +142,9 @@ let ActionMenuElement = class ActionMenuElement extends HTMLElement {
137
142
  const targetIsInvoker = this.invokerElement?.contains(event.target);
138
143
  const eventIsActivation = __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_isActivation).call(this, event);
139
144
  if (event.type === 'toggle' && event.newState === 'open') {
140
- __classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get)?.focus();
145
+ window.requestAnimationFrame(() => {
146
+ __classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get)?.focus();
147
+ });
141
148
  }
142
149
  if (targetIsInvoker && event.type === 'mousedown') {
143
150
  __classPrivateFieldSet(this, _ActionMenuElement_invokerBeingClicked, true, "f");
@@ -392,9 +399,10 @@ _ActionMenuElement_handleItemActivated = function _ActionMenuElement_handleItemA
392
399
  }));
393
400
  };
394
401
  _ActionMenuElement_handleIncludeFragmentReplaced = function _ActionMenuElement_handleIncludeFragmentReplaced() {
395
- if (__classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get))
396
- __classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get).focus();
402
+ __classPrivateFieldGet(this, _ActionMenuElement_instances, "a", _ActionMenuElement_firstItem_get)?.focus();
397
403
  __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_softDisableItems).call(this);
404
+ // async items have loaded, so component is ready
405
+ this.setAttribute('data-ready', 'true');
398
406
  };
399
407
  _ActionMenuElement_handleFocusOut = function _ActionMenuElement_handleFocusOut() {
400
408
  __classPrivateFieldGet(this, _ActionMenuElement_instances, "m", _ActionMenuElement_hide).call(this);
@@ -409,6 +417,8 @@ _ActionMenuElement_isOpen = function _ActionMenuElement_isOpen() {
409
417
  return this.popoverElement?.matches(':popover-open');
410
418
  };
411
419
  _ActionMenuElement_setDynamicLabel = function _ActionMenuElement_setDynamicLabel() {
420
+ if (this.selectVariant !== 'single')
421
+ return;
412
422
  if (!this.dynamicLabel)
413
423
  return;
414
424
  const invokerLabel = this.invokerLabel;
@@ -143,6 +143,12 @@ export class ActionMenuElement extends HTMLElement {
143
143
  () => Boolean(this.invokerElement),
144
144
  () => this.#intersectionObserver.observe(this.invokerElement!),
145
145
  )
146
+
147
+ // If there's no include fragment, then no async fetching will occur and we can
148
+ // mark the component as ready.
149
+ if (!this.includeFragment) {
150
+ this.setAttribute('data-ready', 'true')
151
+ }
146
152
  }
147
153
 
148
154
  disconnectedCallback() {
@@ -199,7 +205,9 @@ export class ActionMenuElement extends HTMLElement {
199
205
  const eventIsActivation = this.#isActivation(event)
200
206
 
201
207
  if (event.type === 'toggle' && (event as ToggleEvent).newState === 'open') {
202
- this.#firstItem?.focus()
208
+ window.requestAnimationFrame(() => {
209
+ this.#firstItem?.focus()
210
+ })
203
211
  }
204
212
 
205
213
  if (targetIsInvoker && event.type === 'mousedown') {
@@ -365,8 +373,11 @@ export class ActionMenuElement extends HTMLElement {
365
373
  }
366
374
 
367
375
  #handleIncludeFragmentReplaced() {
368
- if (this.#firstItem) this.#firstItem.focus()
376
+ this.#firstItem?.focus()
369
377
  this.#softDisableItems()
378
+
379
+ // async items have loaded, so component is ready
380
+ this.setAttribute('data-ready', 'true')
370
381
  }
371
382
 
372
383
  // Close when focus leaves menu
@@ -387,6 +398,7 @@ export class ActionMenuElement extends HTMLElement {
387
398
  }
388
399
 
389
400
  #setDynamicLabel() {
401
+ if (this.selectVariant !== 'single') return
390
402
  if (!this.dynamicLabel) return
391
403
  const invokerLabel = this.invokerLabel
392
404
  if (!invokerLabel) return
@@ -7,12 +7,20 @@ module Primer
7
7
  class Dropdown < Primer::Component
8
8
  status :alpha
9
9
 
10
+ ARIA_LABEL_OPEN_DEFAULT = "Close"
11
+ ARIA_LABEL_CLOSED_DEFAULT = "Open"
12
+
10
13
  # Required trigger for the dropdown. Has the same arguments as <%= link_to_component(Primer::ButtonComponent) %>,
11
14
  # but it is locked as a `summary` tag.
15
+ #
16
+ # @param aria_label_open [String] Defaults to "Close". Value to announce when menu is open.
17
+ # @param aria_label_closed [String] Defaults to "Open". Value to announce when menu is closed.
12
18
  renders_one :button, lambda { |**system_arguments|
13
19
  @button_arguments = system_arguments
14
20
  @button_arguments[:button] = true
15
21
  @button_arguments[:dropdown] = @with_caret
22
+ @button_arguments[:aria_label_open] = system_arguments[:aria_label_open] || ARIA_LABEL_OPEN_DEFAULT
23
+ @button_arguments[:aria_label_closed] = system_arguments[:aria_label_closed] || ARIA_LABEL_CLOSED_DEFAULT
16
24
 
17
25
  Primer::Content.new
18
26
  }
@@ -3,8 +3,39 @@
3
3
  module Primer
4
4
  module Alpha
5
5
  # Wraps an input (or arbitrary content) with a label above and a caption and validation message beneath.
6
+ #
6
7
  # NOTE: This `FormControl` component is designed for wrapping inputs that aren't supported by the Primer
7
8
  # forms framework.
9
+ #
10
+ # @accessibility
11
+ # Because `FormControl` does not manage the actual `<input>` element, it cannot semantically connect
12
+ # the input and its associated label. For this and other reasons, consumers are highly encouraged to
13
+ # use Primer's pre-made form components like `TextField`, etc, ideally via the Primer forms framework.
14
+ #
15
+ # Users of the `FormControl` component will need to manually connect the label using the `for:`
16
+ # attribute, eg:
17
+ #
18
+ # ```erb
19
+ # <%= form_with(url: "/path/somewhere") do |f| %>
20
+ # <%= render(Primer::Alpha::FormControl.new(label_arguments: { for: "bar" })) do |component| %>
21
+ # <% component.with_input do |input_arguments| %>
22
+ # <%= f.text_field(:bar, **input_arguments) %>
23
+ # <% end %>
24
+ # <% end %>
25
+ # <% end %>
26
+ # ```
27
+ #
28
+ # Note that the name of the field, `:bar`, is passed to both the Rails `#text_field` method _and_
29
+ # as part of the `label_arguments` passed to the `FormControl` constructor.
30
+ #
31
+ # Similarly, `FormControl` cannot automatically connect the `<input>` element to the caption and
32
+ # validation message elements. The component attempts to mitigate this by including the correct
33
+ # `aria-describedby` attribute in the hash it yields to the block passed to `#with_input`. In the
34
+ # example above, `input_arguments[:aria][:describedby]` contains the HTML IDs for both the caption
35
+ # and validation message elements, and can be passed directly to Rails' form helper methods. If the
36
+ # input being wrapped is not generated by a Rails form helper, care must be taken to set
37
+ # `aria-describedby` manually on the input element.
38
+ #
8
39
  class FormControl < Primer::Component
9
40
  # Describes the field and what sorts of input it expects. Displayed below the input.
10
41
  # Note that this slot takes precedence over the `caption:` argument in the constructor.
@@ -16,14 +47,16 @@ module Primer
16
47
  # @param required [Boolean] Default `false`. When set to `true`, causes an asterisk (*) to appear next to the field's label indicating it is a required field. Note that this option explicitly does _not_ add a `required` HTML attribute. Doing so would enable native browser validations, which are inaccessible and inconsistent with the Primer design system.
17
48
  # @param visually_hide_label [Boolean] When set to `true`, hides the label. Although the label will be hidden visually, it will still be visible to screen readers.
18
49
  # @param full_width [Boolean] When set to `true`, the form control will take up all the horizontal space allowed by its container.
50
+ # @param label_arguments [Hash] HTML attributes to attach to the `<label>` element that labels the input.
19
51
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
20
- def initialize(label:, caption: nil, validation_message: nil, required: false, visually_hide_label: false, full_width: false, **system_arguments)
52
+ def initialize(label:, caption: nil, validation_message: nil, required: false, visually_hide_label: false, full_width: false, label_arguments: {}, **system_arguments)
21
53
  @label = label
22
54
  @init_caption = caption
23
55
  @validation_message = validation_message
24
56
  @required = required
25
57
  @visually_hide_label = visually_hide_label
26
58
  @full_width = full_width
59
+ @label_arguments = label_arguments
27
60
  @system_arguments = system_arguments
28
61
 
29
62
  @system_arguments[:classes] = class_names(
@@ -32,12 +65,11 @@ module Primer
32
65
  "FormControl--fullWidth" => full_width?
33
66
  )
34
67
 
35
- @label_arguments = {
36
- classes: class_names(
37
- "FormControl-label",
38
- visually_hide_label? ? "sr-only" : nil
39
- )
40
- }
68
+ @label_arguments[:classes] = class_names(
69
+ @label_arguments.delete(:classes),
70
+ "FormControl-label",
71
+ visually_hide_label? ? "sr-only" : nil
72
+ )
41
73
 
42
74
  base_id = self.class.generate_id
43
75
  @validation_id = "validation-#{base_id}"
@@ -58,14 +90,20 @@ module Primer
58
90
  @input_block = block
59
91
  end
60
92
 
93
+ # Whether or not this input is marked as required.
94
+ # @returns Boolean
61
95
  def required?
62
96
  @required
63
97
  end
64
98
 
99
+ # Whether or not to hide the label visually. The label will still be visible to screen readers.
100
+ # @returns Boolean
65
101
  def visually_hide_label?
66
102
  @visually_hide_label
67
103
  end
68
104
 
105
+ # Whether or not the form control should take up all the horizontal space allowed by its container.
106
+ # @returns Boolean
69
107
  def full_width?
70
108
  @full_width
71
109
  end
@@ -85,6 +123,8 @@ module Primer
85
123
  memo << @caption_id if @init_caption || caption?
86
124
  end
87
125
 
126
+ @input_arguments[:aria][:required] = "true" if required?
127
+
88
128
  return if ids.empty?
89
129
 
90
130
  @input_arguments[:aria][:describedby] = ids.join(" ")
@@ -8,7 +8,7 @@
8
8
  <%= render(Primer::Box.new(classes: "ToggleSwitch-statusOff").with_content("Off")) %>
9
9
  <% end %>
10
10
 
11
- <%= render(Primer::BaseComponent.new(tag: :button, classes: "ToggleSwitch-track", disabled: disabled?, data: { target: "toggle-switch.switch", action: "click:toggle-switch#toggle" }, **@aria_arguments)) do %>
11
+ <%= render(Primer::BaseComponent.new(tag: :button, classes: "ToggleSwitch-track", disabled: disabled?, data: { target: "toggle-switch.switch", action: "click:toggle-switch#toggle" }, **@button_arguments)) do %>
12
12
  <%= render(Primer::Box.new(classes: "ToggleSwitch-icons", aria: { hidden: true })) do %>
13
13
  <%= render(Primer::Box.new(classes: "ToggleSwitch-lineIcon")) do %>
14
14
  <%= render(Primer::BaseComponent.new(
@@ -132,6 +132,7 @@ let ToggleSwitchElement = class ToggleSwitchElement extends HTMLElement {
132
132
  let response;
133
133
  const requestHeaders = {
134
134
  'Requested-With': 'XMLHttpRequest',
135
+ 'X-Requested-With': 'XMLHttpRequest',
135
136
  };
136
137
  if (this.turbo) {
137
138
  requestHeaders['Accept'] = 'text/vnd.turbo-stream.html';
@@ -27,8 +27,19 @@ module Primer
27
27
  # @param size [Symbol] What size toggle switch to render. <%= one_of(Primer::Alpha::ToggleSwitch::SIZE_OPTIONS) %>
28
28
  # @param status_label_position [Symbol] Which side of the toggle switch to render the status label. <%= one_of(Primer::Alpha::ToggleSwitch::STATUS_LABEL_POSITION_OPTIONS) %>
29
29
  # @param turbo [Boolean] Whether or not to request a turbo stream and render the response as such.
30
+ # @param autofocus [Boolean] Whether switch should be autofocused when rendered.
30
31
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
31
- def initialize(src: nil, csrf_token: nil, checked: false, enabled: true, size: SIZE_DEFAULT, status_label_position: STATUS_LABEL_POSITION_DEFAULT, turbo: false, **system_arguments)
32
+ def initialize(
33
+ src: nil,
34
+ csrf_token: nil,
35
+ checked: false,
36
+ enabled: true,
37
+ size: SIZE_DEFAULT,
38
+ status_label_position: STATUS_LABEL_POSITION_DEFAULT,
39
+ turbo: false,
40
+ autofocus: nil,
41
+ **system_arguments
42
+ )
32
43
  @src = src
33
44
  @csrf_token = csrf_token
34
45
  @checked = checked
@@ -50,12 +61,13 @@ module Primer
50
61
  SIZE_MAPPINGS[@size]
51
62
  )
52
63
 
53
- @aria_arguments = {
64
+ @button_arguments = {
54
65
  aria: merge_aria(
55
66
  @system_arguments,
56
67
  aria: { pressed: on? }
57
68
  )
58
69
  }
70
+ @button_arguments[:autofocus] = true if autofocus
59
71
 
60
72
  @system_arguments[:src] = @src if @src
61
73
  end
@@ -161,6 +161,7 @@ class ToggleSwitchElement extends HTMLElement {
161
161
 
162
162
  const requestHeaders: {[key: string]: string} = {
163
163
  'Requested-With': 'XMLHttpRequest',
164
+ 'X-Requested-With': 'XMLHttpRequest',
164
165
  }
165
166
 
166
167
  if (this.turbo) {
@@ -1,8 +1,10 @@
1
- <% if disabled? %>
2
- <%= summary %>
3
- <% else %>
4
- <%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
1
+ <details-toggle>
2
+ <% if disabled? %>
5
3
  <%= summary %>
6
- <%= body %>
4
+ <% else %>
5
+ <%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
6
+ <%= summary %>
7
+ <%= body %>
8
+ <% end %>
7
9
  <% end %>
8
- <% end %>
10
+ </details-toggle>
@@ -14,14 +14,47 @@ module Primer
14
14
  :default => "details-overlay",
15
15
  :dark => "details-overlay details-overlay-dark"
16
16
  }.freeze
17
+ ARIA_LABEL_OPEN_DEFAULT = "Collapse"
18
+ ARIA_LABEL_CLOSED_DEFAULT = "Expand"
17
19
 
18
20
  attr_reader :disabled
19
21
  alias disabled? disabled
20
22
 
23
+ attr_reader :open
24
+ alias open? open
25
+
26
+ # Use the Summary slot as the target for toggling the Details content open/closed.
27
+ #
28
+ # @param button [Boolean] Whether or not to render the summary element as a button.
29
+ # @param aria_label_open [String] Defaults to "Collapse". Value to announce when details element is open.
30
+ # @param aria_label_closed [String] Defaults to "Expand". Value to announce when details element is closed.
21
31
  renders_one :summary, lambda { |button: true, **system_arguments|
22
32
  system_arguments[:tag] = :summary
23
33
  system_arguments[:role] = "button"
24
34
 
35
+ aria_label_closed = system_arguments[:aria_label_closed] || ARIA_LABEL_CLOSED_DEFAULT
36
+ aria_label_open = system_arguments[:aria_label_open] || ARIA_LABEL_OPEN_DEFAULT
37
+
38
+ system_arguments[:data] = merge_data(
39
+ system_arguments, {
40
+ data: {
41
+ target: "details-toggle.summaryTarget",
42
+ action: "click:details-toggle#toggle",
43
+ aria_label_closed: aria_label_closed,
44
+ aria_label_open: aria_label_open,
45
+ }
46
+ }
47
+ )
48
+
49
+ system_arguments[:aria] = merge_aria(
50
+ system_arguments, {
51
+ aria: {
52
+ label: open? ? aria_label_open : aria_label_closed,
53
+ expanded: open?,
54
+ }
55
+ }
56
+ )
57
+
25
58
  if disabled?
26
59
  # rubocop:disable Primer/ComponentNameMigration
27
60
  Primer::ButtonComponent.new(**system_arguments, disabled: true)
@@ -57,6 +90,15 @@ module Primer
57
90
  OVERLAY_MAPPINGS[fetch_or_fallback(OVERLAY_MAPPINGS.keys, overlay, NO_OVERLAY)],
58
91
  "details-reset" => reset
59
92
  )
93
+ @system_arguments[:data] = merge_data(
94
+ @system_arguments, {
95
+ data: {
96
+ target: "details-toggle.detailsTarget",
97
+ }
98
+ }
99
+ )
100
+ # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details#open
101
+ @open = !!@system_arguments[:open]
60
102
  @disabled = disabled
61
103
  @summary_info = nil
62
104
  end
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A companion Catalyst element for the Details view component. This element
3
+ * ensures the <details> and <summary> elements markup is properly accessible by
4
+ * updating the aria-label and aria-expanded attributes on click.
5
+ *
6
+ * aria-label values default to "Expand" and "Collapse". To override those
7
+ * values, use the `data-aria-label-open` and `data-aria-label-closed`
8
+ * attributes on the summary target.
9
+ *
10
+ * @example
11
+ * ```html
12
+ * <details-toggle>
13
+ * <details open=true data-target="details-toggle.detailsTarget">
14
+ * <summary
15
+ * aria-expanded="true"
16
+ * aria-label="Collapse me"
17
+ * data-target="details-toggle.summaryTarget"
18
+ * data-action="click:details-toggle#toggle"
19
+ * data-aria-label-closed="Expand me"
20
+ * data-aria-label-open="Collapse me"
21
+ * >
22
+ * Click me
23
+ * </summary>
24
+ * <div>Contents</div>
25
+ * </details>
26
+ * </details-toggle>
27
+ * ```
28
+ */
29
+ declare class DetailsToggleElement extends HTMLElement {
30
+ detailsTarget: HTMLDetailsElement;
31
+ summaryTarget: HTMLElement;
32
+ toggle(): void;
33
+ }
34
+ declare global {
35
+ interface Window {
36
+ DetailsToggleElement: typeof DetailsToggleElement;
37
+ }
38
+ }
39
+ export {};
@@ -0,0 +1,60 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { controller, target } from '@github/catalyst';
8
+ /**
9
+ * A companion Catalyst element for the Details view component. This element
10
+ * ensures the <details> and <summary> elements markup is properly accessible by
11
+ * updating the aria-label and aria-expanded attributes on click.
12
+ *
13
+ * aria-label values default to "Expand" and "Collapse". To override those
14
+ * values, use the `data-aria-label-open` and `data-aria-label-closed`
15
+ * attributes on the summary target.
16
+ *
17
+ * @example
18
+ * ```html
19
+ * <details-toggle>
20
+ * <details open=true data-target="details-toggle.detailsTarget">
21
+ * <summary
22
+ * aria-expanded="true"
23
+ * aria-label="Collapse me"
24
+ * data-target="details-toggle.summaryTarget"
25
+ * data-action="click:details-toggle#toggle"
26
+ * data-aria-label-closed="Expand me"
27
+ * data-aria-label-open="Collapse me"
28
+ * >
29
+ * Click me
30
+ * </summary>
31
+ * <div>Contents</div>
32
+ * </details>
33
+ * </details-toggle>
34
+ * ```
35
+ */
36
+ let DetailsToggleElement = class DetailsToggleElement extends HTMLElement {
37
+ toggle() {
38
+ const detailsIsOpen = this.detailsTarget.hasAttribute('open');
39
+ if (detailsIsOpen) {
40
+ const ariaLabelClosed = this.summaryTarget.getAttribute('data-aria-label-closed') || 'Expand';
41
+ this.summaryTarget.setAttribute('aria-label', ariaLabelClosed);
42
+ this.summaryTarget.setAttribute('aria-expanded', 'false');
43
+ }
44
+ else {
45
+ const ariaLabelOpen = this.summaryTarget.getAttribute('data-aria-label-open') || 'Collapse';
46
+ this.summaryTarget.setAttribute('aria-label', ariaLabelOpen);
47
+ this.summaryTarget.setAttribute('aria-expanded', 'true');
48
+ }
49
+ }
50
+ };
51
+ __decorate([
52
+ target
53
+ ], DetailsToggleElement.prototype, "detailsTarget", void 0);
54
+ __decorate([
55
+ target
56
+ ], DetailsToggleElement.prototype, "summaryTarget", void 0);
57
+ DetailsToggleElement = __decorate([
58
+ controller
59
+ ], DetailsToggleElement);
60
+ window.DetailsToggleElement = DetailsToggleElement;
@@ -0,0 +1,57 @@
1
+ import {controller, target} from '@github/catalyst'
2
+
3
+ /**
4
+ * A companion Catalyst element for the Details view component. This element
5
+ * ensures the <details> and <summary> elements markup is properly accessible by
6
+ * updating the aria-label and aria-expanded attributes on click.
7
+ *
8
+ * aria-label values default to "Expand" and "Collapse". To override those
9
+ * values, use the `data-aria-label-open` and `data-aria-label-closed`
10
+ * attributes on the summary target.
11
+ *
12
+ * @example
13
+ * ```html
14
+ * <details-toggle>
15
+ * <details open=true data-target="details-toggle.detailsTarget">
16
+ * <summary
17
+ * aria-expanded="true"
18
+ * aria-label="Collapse me"
19
+ * data-target="details-toggle.summaryTarget"
20
+ * data-action="click:details-toggle#toggle"
21
+ * data-aria-label-closed="Expand me"
22
+ * data-aria-label-open="Collapse me"
23
+ * >
24
+ * Click me
25
+ * </summary>
26
+ * <div>Contents</div>
27
+ * </details>
28
+ * </details-toggle>
29
+ * ```
30
+ */
31
+
32
+ @controller
33
+ class DetailsToggleElement extends HTMLElement {
34
+ @target detailsTarget!: HTMLDetailsElement
35
+ @target summaryTarget!: HTMLElement
36
+
37
+ toggle() {
38
+ const detailsIsOpen = this.detailsTarget.hasAttribute('open')
39
+ if (detailsIsOpen) {
40
+ const ariaLabelClosed = this.summaryTarget.getAttribute('data-aria-label-closed') || 'Expand'
41
+ this.summaryTarget.setAttribute('aria-label', ariaLabelClosed)
42
+ this.summaryTarget.setAttribute('aria-expanded', 'false')
43
+ } else {
44
+ const ariaLabelOpen = this.summaryTarget.getAttribute('data-aria-label-open') || 'Collapse'
45
+ this.summaryTarget.setAttribute('aria-label', ariaLabelOpen)
46
+ this.summaryTarget.setAttribute('aria-expanded', 'true')
47
+ }
48
+ }
49
+ }
50
+
51
+ declare global {
52
+ interface Window {
53
+ DetailsToggleElement: typeof DetailsToggleElement
54
+ }
55
+ }
56
+
57
+ window.DetailsToggleElement = DetailsToggleElement
@@ -3,6 +3,7 @@
3
3
  module Primer
4
4
  module Beta
5
5
  # Use `Markdown` to wrap markdown content.
6
+ # @accessibility This component is purely presentational. Consumers should handle accessibility expectations, such as ensuring that an overflowing, scrollable code block or table is keyboard accessible.
6
7
  class Markdown < Primer::Component
7
8
  status :beta
8
9
 
@@ -168,7 +168,7 @@ module Primer
168
168
  end
169
169
  end
170
170
 
171
- # Lists that contain top-level items (i.e. items outside of a group) should be wrapped in a <ul>
171
+ # Lists that contain top-level items (i.e. items outside of a group) should be wrapped in a `<ul>`
172
172
  def render_outer_list?
173
173
  items.any? { |item| !group?(item) }
174
174
  end
@@ -25,3 +25,4 @@ import '../../lib/primer/forms/primer_text_field';
25
25
  import '../../lib/primer/forms/toggle_switch_input';
26
26
  import './alpha/action_menu/action_menu_element';
27
27
  import './alpha/select_panel_element';
28
+ import './beta/details_toggle_element';
@@ -25,3 +25,4 @@ import '../../lib/primer/forms/primer_text_field';
25
25
  import '../../lib/primer/forms/toggle_switch_input';
26
26
  import './alpha/action_menu/action_menu_element';
27
27
  import './alpha/select_panel_element';
28
+ import './beta/details_toggle_element';
@@ -25,3 +25,4 @@ import '../../lib/primer/forms/primer_text_field'
25
25
  import '../../lib/primer/forms/toggle_switch_input'
26
26
  import './alpha/action_menu/action_menu_element'
27
27
  import './alpha/select_panel_element'
28
+ import './beta/details_toggle_element'
@@ -1,6 +1,6 @@
1
1
  <%= render(FormControl.new(input: @input)) do %>
2
2
  <%= render(Primer::Alpha::ActionMenu.new(**@input.input_arguments)) do |menu| %>
3
- <% menu.with_show_button { "Select..." } %>
3
+ <% menu.with_show_button("aria-describedby": @label_id) { "Select..." } %>
4
4
  <% @input.block.call(menu) if @input.block %>
5
5
  <% end %>
6
6
  <% end %>
@@ -8,6 +8,7 @@ module Primer
8
8
 
9
9
  def initialize(input:)
10
10
  @input = input
11
+ @input.label_arguments[:id] = label_id
11
12
 
12
13
  @input.input_arguments[:form_arguments] = {
13
14
  name: @input.name,
@@ -20,6 +21,10 @@ module Primer
20
21
  @input.input_arguments[:dynamic_label] = true
21
22
  end
22
23
  end
24
+
25
+ def label_id
26
+ @label_id ||= "label-#{@input.base_id}"
27
+ end
23
28
  end
24
29
  end
25
30
  end
@@ -5,8 +5,8 @@ module Primer
5
5
  module ViewComponents
6
6
  module VERSION
7
7
  MAJOR = 0
8
- MINOR = 36
9
- PATCH = 5
8
+ MINOR = 37
9
+ PATCH = 0
10
10
 
11
11
  STRING = [MAJOR, MINOR, PATCH].join(".")
12
12
  end
@@ -86,16 +86,17 @@ module Primer
86
86
  Primer::Alpha::ActionList::Item => { examples: false },
87
87
 
88
88
  # Forms
89
- Primer::Alpha::TextField => { form_component: true },
90
- Primer::Alpha::TextArea => { form_component: true, published: false },
91
- Primer::Alpha::Select => { form_component: true, published: false },
92
- Primer::Alpha::MultiInput => { form_component: true, js: true, published: false },
93
- Primer::Alpha::RadioButton => { form_component: true, published: false },
94
- Primer::Alpha::RadioButtonGroup => { form_component: true, published: false },
95
- Primer::Alpha::CheckBox => { form_component: true, published: false },
96
- Primer::Alpha::CheckBoxGroup => { form_component: true, published: false },
97
- Primer::Alpha::SubmitButton => { form_component: true, published: false },
98
- Primer::Alpha::FormButton => { form_component: true, published: false }
89
+ Primer::Alpha::FormControl => { form_component: true },
90
+ Primer::Alpha::TextField => { form_component: true, js: true },
91
+ Primer::Alpha::TextArea => { form_component: true },
92
+ Primer::Alpha::Select => { form_component: true },
93
+ Primer::Alpha::MultiInput => { form_component: true, js: true },
94
+ Primer::Alpha::RadioButton => { form_component: true },
95
+ Primer::Alpha::RadioButtonGroup => { form_component: true },
96
+ Primer::Alpha::CheckBox => { form_component: true },
97
+ Primer::Alpha::CheckBoxGroup => { form_component: true },
98
+ Primer::Alpha::SubmitButton => { form_component: true },
99
+ Primer::Alpha::FormButton => { form_component: true }
99
100
  }.freeze
100
101
 
101
102
  include Enumerable
@@ -165,6 +165,12 @@ module Primer
165
165
  class LookbookPagesBackend < Backend
166
166
  attr_reader :registry, :manifest
167
167
 
168
+ IGNORED_COMPONENTS = [
169
+ Primer::Alpha::FormControl
170
+ ]
171
+
172
+ IGNORED_COMPONENTS.freeze
173
+
168
174
  def initialize(registry, manifest)
169
175
  @registry = registry
170
176
  @manifest = manifest
@@ -172,6 +178,8 @@ module Primer
172
178
 
173
179
  def generate
174
180
  each_component do |component_ref|
181
+ next if IGNORED_COMPONENTS.include?(component_ref.klass)
182
+
175
183
  page_for(component_ref).generate
176
184
  end
177
185
  generate_system_args_docs