primer_view_components 0.36.4 → 0.37.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -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/button.html.erb +1 -1
  16. data/app/components/primer/beta/button.rb +2 -1
  17. data/app/components/primer/beta/details.html.erb +8 -6
  18. data/app/components/primer/beta/details.rb +42 -0
  19. data/app/components/primer/beta/details_toggle_element.d.ts +39 -0
  20. data/app/components/primer/beta/details_toggle_element.js +60 -0
  21. data/app/components/primer/beta/details_toggle_element.ts +57 -0
  22. data/app/components/primer/beta/markdown.rb +1 -0
  23. data/app/components/primer/beta/nav_list.rb +1 -1
  24. data/app/components/primer/primer.d.ts +1 -0
  25. data/app/components/primer/primer.js +1 -0
  26. data/app/components/primer/primer.ts +1 -0
  27. data/app/lib/primer/forms/action_menu.html.erb +1 -1
  28. data/app/lib/primer/forms/action_menu.rb +5 -0
  29. data/lib/primer/view_components/version.rb +2 -2
  30. data/lib/primer/yard/component_manifest.rb +11 -10
  31. data/lib/primer/yard/lookbook_pages_backend.rb +8 -0
  32. data/previews/primer/alpha/action_menu_preview/multiple_select_form.html.erb +1 -1
  33. data/previews/primer/alpha/form_control_preview/playground.html.erb +14 -6
  34. data/previews/primer/alpha/overlay_preview.rb +0 -31
  35. data/previews/primer/alpha/select_preview.rb +6 -6
  36. data/previews/primer/alpha/text_field_preview.rb +22 -22
  37. data/previews/primer/alpha/toggle_switch_preview.rb +4 -0
  38. data/previews/primer/alpha/tooltip_preview.rb +1 -1
  39. data/previews/primer/beta/details_preview.rb +12 -0
  40. data/previews/primer/beta/markdown_preview.rb +9 -9
  41. data/previews/primer/beta/relative_time_preview.rb +20 -10
  42. data/static/arguments.json +18 -0
  43. data/static/constants.json +4 -0
  44. data/static/info_arch.json +140 -70
  45. data/static/previews.json +27 -52
  46. metadata +6 -4
  47. data/previews/primer/alpha/overlay_preview/in_an_action_menu.html.erb +0 -13
  48. 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) {
@@ -9,8 +9,8 @@
9
9
  <% if trailing_visual %>
10
10
  <span class="Button-visual Button-trailingVisual">
11
11
  <% if @trailing_visual_counter %>
12
+ <span class="d-flex" aria-hidden="true"><%= trailing_visual %></span>
12
13
  <span class="sr-only">(<%= trailing_visual %>)</span>
13
- <%= trailing_visual %>
14
14
  <% else %>
15
15
  <%= trailing_visual %>
16
16
  <% end %>
@@ -70,7 +70,7 @@ module Primer
70
70
  label: Primer::Beta::Label,
71
71
  counter: lambda { |**system_arguments|
72
72
  @trailing_visual_counter = true
73
- Primer::Beta::Counter.new("aria-hidden": true, **system_arguments)
73
+ Primer::Beta::Counter.new(**system_arguments)
74
74
  }
75
75
  }
76
76
 
@@ -108,6 +108,7 @@ module Primer
108
108
  # @param align_content [Symbol] <%= one_of(Primer::Beta::Button::ALIGN_CONTENT_OPTIONS) %>
109
109
  # @param tag [Symbol] (Primer::Beta::BaseButton::DEFAULT_TAG) <%= one_of(Primer::Beta::BaseButton::TAG_OPTIONS) %>
110
110
  # @param type [Symbol] (Primer::Beta::BaseButton::DEFAULT_TYPE) <%= one_of(Primer::Beta::BaseButton::TYPE_OPTIONS) %>
111
+ # @param inactive [Boolean] Whether the button looks visually disabled, but can still accept all the same interactions as an enabled button.
111
112
  # @param disabled [Boolean] Whether or not the button is disabled. If true, this option forces `tag:` to `:button`.
112
113
  # @param label_wrap [Boolean] Whether or not the button label text wraps and the button height expands.
113
114
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
@@ -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 = 4
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