primer_view_components 0.0.45 → 0.0.49

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +194 -0
  3. data/app/components/primer/{auto_complete_component.d.ts → auto_complete/auto_component.d.ts} +0 -0
  4. data/app/components/primer/{auto_complete_component.js → auto_complete/auto_component.js} +0 -0
  5. data/app/components/primer/base_component.rb +36 -7
  6. data/app/components/primer/beta/auto_complete.rb +159 -0
  7. data/app/components/primer/beta/auto_complete/auto_complete.d.ts +1 -0
  8. data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.html.erb +0 -0
  9. data/app/components/primer/beta/auto_complete/auto_complete.js +1 -0
  10. data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.ts +0 -0
  11. data/app/components/primer/beta/auto_complete/item.rb +44 -0
  12. data/app/components/primer/beta/avatar.rb +77 -0
  13. data/app/components/primer/{avatar_stack_component.html.erb → beta/avatar_stack.html.erb} +0 -0
  14. data/app/components/primer/beta/avatar_stack.rb +92 -0
  15. data/app/components/primer/border_box_component.rb +3 -0
  16. data/app/components/primer/component.rb +9 -1
  17. data/app/components/primer/details_component.rb +12 -8
  18. data/app/components/primer/image_crop.html.erb +4 -4
  19. data/app/components/primer/image_crop.rb +1 -1
  20. data/app/components/primer/markdown.rb +9 -9
  21. data/app/components/primer/menu_component.rb +7 -3
  22. data/app/components/primer/navigation/tab_component.rb +34 -6
  23. data/app/components/primer/popover_component.rb +6 -3
  24. data/app/components/primer/primer.d.ts +1 -1
  25. data/app/components/primer/primer.js +1 -1
  26. data/app/components/primer/primer.ts +1 -1
  27. data/app/components/primer/tab_nav_component.rb +9 -6
  28. data/app/components/primer/timeline_item_component.rb +2 -2
  29. data/app/components/primer/tooltip.rb +1 -1
  30. data/app/components/primer/truncate.rb +6 -1
  31. data/app/components/primer/underline_nav_component.rb +13 -6
  32. data/{app/lib → lib}/primer/classify.rb +12 -39
  33. data/{app/lib → lib}/primer/classify/cache.rb +6 -20
  34. data/{app/lib → lib}/primer/classify/flex.rb +0 -0
  35. data/{app/lib → lib}/primer/classify/functional_background_colors.rb +2 -0
  36. data/{app/lib → lib}/primer/classify/functional_border_colors.rb +2 -0
  37. data/{app/lib → lib}/primer/classify/functional_colors.rb +0 -0
  38. data/{app/lib → lib}/primer/classify/functional_text_colors.rb +2 -0
  39. data/{app/lib → lib}/primer/classify/grid.rb +0 -0
  40. data/{app/lib → lib}/primer/classify/utilities.rb +54 -22
  41. data/{app/lib → lib}/primer/classify/utilities.yml +124 -0
  42. data/lib/primer/view_components.rb +35 -6
  43. data/lib/primer/view_components/constants.rb +55 -0
  44. data/lib/primer/view_components/linters/argument_mappers/base.rb +39 -0
  45. data/lib/primer/view_components/linters/argument_mappers/button.rb +35 -44
  46. data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +25 -0
  47. data/lib/primer/view_components/linters/argument_mappers/label.rb +56 -0
  48. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +1 -2
  49. data/lib/primer/view_components/linters/autocorrectable.rb +30 -0
  50. data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -23
  51. data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +21 -0
  52. data/lib/primer/view_components/linters/helpers.rb +56 -38
  53. data/lib/primer/view_components/linters/label_component_migration_counter.rb +25 -0
  54. data/lib/primer/view_components/statuses.rb +14 -0
  55. data/lib/primer/view_components/version.rb +1 -1
  56. data/lib/rubocop/config/default.yml +12 -0
  57. data/lib/rubocop/cop/primer.rb +4 -0
  58. data/lib/rubocop/cop/primer/no_tag_memoize.rb +42 -0
  59. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +75 -0
  60. data/lib/tasks/constants.rake +12 -0
  61. data/lib/tasks/docs.rake +89 -34
  62. data/lib/tasks/utilities.rake +9 -11
  63. data/lib/yard/docs_helper.rb +12 -3
  64. data/static/arguments.yml +977 -0
  65. data/static/assets/view-components.svg +18 -0
  66. data/static/classes.yml +174 -0
  67. data/static/constants.json +628 -0
  68. data/static/statuses.json +5 -5
  69. metadata +44 -27
  70. data/app/components/primer/auto_complete.rb +0 -156
  71. data/app/components/primer/auto_complete/item.rb +0 -42
  72. data/app/components/primer/avatar_component.rb +0 -75
  73. data/app/components/primer/avatar_stack_component.rb +0 -84
  74. data/app/components/primer/details_menu_component.d.ts +0 -1
  75. data/app/components/primer/details_menu_component.js +0 -1
@@ -0,0 +1 @@
1
+ import '@github/auto-complete-element';
@@ -0,0 +1 @@
1
+ import '@github/auto-complete-element';
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module Beta
5
+ class AutoComplete
6
+ # Use `AutoCompleteItem` to list results of an auto-completed search.
7
+ class Item < Primer::Component
8
+ status :beta
9
+
10
+ # @example Default
11
+ # <%= render(Primer::Beta::AutoComplete::Item.new(selected: true, value: "value")) do |c| %>
12
+ # Selected
13
+ # <% end %>
14
+ # <%= render(Primer::Beta::AutoComplete::Item.new(value: "value")) do |c| %>
15
+ # Not selected
16
+ # <% end %>
17
+ #
18
+ # @param value [String] Value of the item.
19
+ # @param selected [Boolean] Whether the item is selected.
20
+ # @param disabled [Boolean] Whether the item is disabled.
21
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
22
+ def initialize(value:, selected: false, disabled: false, **system_arguments)
23
+ @system_arguments = system_arguments
24
+ @system_arguments[:tag] = :li
25
+ @system_arguments[:role] = :option
26
+ @system_arguments[:"data-autocomplete-value"] = value
27
+
28
+ @system_arguments[:"aria-selected"] = true if selected
29
+ @system_arguments[:"aria-disabled"] = true if disabled
30
+
31
+ @system_arguments[:classes] = class_names(
32
+ "autocomplete-item",
33
+ system_arguments[:classes],
34
+ "disabled" => disabled
35
+ )
36
+ end
37
+
38
+ def call
39
+ render(Primer::BaseComponent.new(**@system_arguments)) { content }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module Beta
5
+ # `Avatar` can be used to represent users and organizations on GitHub.
6
+ #
7
+ # - Use the default round avatar for users, and the `square` argument
8
+ # for organizations or any other non-human avatars.
9
+ # - By default, `Avatar` will render a static `<img>`. To have `Avatar` function as a link, set the `href` which will wrap the `<img>` in a `<a>`.
10
+ # - Set `size` to update the height and width of the `Avatar` in pixels.
11
+ # - To stack multiple avatars together, use <%= link_to_component(Primer::Beta::AvatarStack) %>.
12
+ #
13
+ # @accessibility
14
+ # Images should have text alternatives that describe the information or function represented.
15
+ # If the avatar functions as a link, provide alt text that helps convey the function. For instance,
16
+ # if `Avatar` is a link to a user profile, the alt attribute should be `@kittenuser profile`
17
+ # rather than `@kittenuser`.
18
+ # [Learn more about best image practices (WAI Images)](https://www.w3.org/WAI/tutorials/images/)
19
+ class Avatar < Primer::Component
20
+ status :beta
21
+
22
+ SMALL_THRESHOLD = 24
23
+
24
+ # @example Default
25
+ # <%= render(Primer::Beta::Avatar.new(src: "http://placekitten.com/200/200", alt: "@kittenuser")) %>
26
+ #
27
+ # @example Square
28
+ # <%= render(Primer::Beta::Avatar.new(src: "http://placekitten.com/200/200", alt: "@kittenuser", square: true)) %>
29
+ #
30
+ # @example Link
31
+ # <%= render(Primer::Beta::Avatar.new(href: "#", src: "http://placekitten.com/200/200", alt: "@kittenuser profile")) %>
32
+ #
33
+ # @example With size
34
+ # <%= render(Primer::Beta::Avatar.new(src: "http://placekitten.com/200/200", alt: "@kittenuser", size: 16)) %>
35
+ # <%= render(Primer::Beta::Avatar.new(src: "http://placekitten.com/200/200", alt: "@kittenuser", size: 20)) %>
36
+ # <%= render(Primer::Beta::Avatar.new(src: "http://placekitten.com/200/200", alt: "@kittenuser", size: 24)) %>
37
+ # <%= render(Primer::Beta::Avatar.new(src: "http://placekitten.com/200/200", alt: "@kittenuser", size: 28)) %>
38
+ # <%= render(Primer::Beta::Avatar.new(src: "http://placekitten.com/200/200", alt: "@kittenuser", size: 32)) %>
39
+ # <%= render(Primer::Beta::Avatar.new(src: "http://placekitten.com/200/200", alt: "@kittenuser", size: 36)) %>
40
+ #
41
+ # @param src [String] The source url of the avatar image.
42
+ # @param alt [String] Passed through to alt on img tag.
43
+ # @param size [Integer] Adds the avatar-small class if less than 24.
44
+ # @param square [Boolean] Used to create a square avatar.
45
+ # @param href [String] The URL to link to. If used, component will be wrapped by an `<a>` tag.
46
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
47
+ def initialize(src:, alt:, size: 20, square: false, href: nil, **system_arguments)
48
+ @href = href
49
+ @system_arguments = system_arguments
50
+ @system_arguments[:tag] = :img
51
+ @system_arguments[:src] = src
52
+ @system_arguments[:alt] = alt
53
+ @system_arguments[:size] = size
54
+ @system_arguments[:height] = size
55
+ @system_arguments[:width] = size
56
+
57
+ @system_arguments[:classes] = class_names(
58
+ system_arguments[:classes],
59
+ "avatar",
60
+ "avatar-small" => size < SMALL_THRESHOLD,
61
+ "circle" => !square,
62
+ "lh-0" => href # Addresses an overflow issue with linked avatars
63
+ )
64
+ end
65
+
66
+ def call
67
+ if @href
68
+ render(Primer::LinkComponent.new(href: @href, classes: @system_arguments[:classes])) do
69
+ render(Primer::BaseComponent.new(**@system_arguments.except(:classes))) { content }
70
+ end
71
+ else
72
+ render(Primer::BaseComponent.new(**@system_arguments)) { content }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module Beta
5
+ # Use `AvatarStack` to stack multiple avatars together.
6
+ class AvatarStack < Primer::Component
7
+ status :beta
8
+
9
+ ALIGN_DEFAULT = :left
10
+ ALIGN_OPTIONS = [ALIGN_DEFAULT, :right].freeze
11
+
12
+ DEFAULT_TAG = :div
13
+ TAG_OPTIONS = [DEFAULT_TAG, :span].freeze
14
+
15
+ DEFAULT_BODY_TAG = :div
16
+ BODY_TAG_OPTIONS = [DEFAULT_BODY_TAG, :span].freeze
17
+ # Required list of stacked avatars.
18
+ #
19
+ # @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::Beta::Avatar) %>.
20
+ renders_many :avatars, "Primer::Beta::Avatar"
21
+
22
+ # @example Default
23
+ # <%= render(Primer::Beta::AvatarStack.new) do |c| %>
24
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
25
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
26
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
27
+ # <% end %>
28
+ #
29
+ # @example Align right
30
+ # <%= render(Primer::Beta::AvatarStack.new(align: :right)) do |c| %>
31
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
32
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
33
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
34
+ # <% end %>
35
+ #
36
+ # @example With tooltip
37
+ # <%= render(Primer::Beta::AvatarStack.new(tooltipped: true, body_arguments: { label: 'This is a tooltip!' })) do |c| %>
38
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
39
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
40
+ # <%= c.avatar(src: "http://placekitten.com/200/200", alt: "@kittenuser") %>
41
+ # <% end %>
42
+ #
43
+ # @param tag [Symbol] <%= one_of(Primer::Beta::AvatarStack::TAG_OPTIONS) %>
44
+ # @param align [Symbol] <%= one_of(Primer::Beta::AvatarStack::ALIGN_OPTIONS) %>
45
+ # @param tooltipped [Boolean] Whether to add a tooltip to the stack or not.
46
+ # @param body_arguments [Hash] Parameters to add to the Body. If `tooltipped` is set, has the same arguments as <%= link_to_component(Primer::Tooltip) %>.
47
+ # The default tag is <%= pretty_value(Primer::Beta::AvatarStack::DEFAULT_BODY_TAG) %> but can be changed using `tag:`
48
+ # to <%= one_of(Primer::Beta::AvatarStack::BODY_TAG_OPTIONS, lower: true) %>
49
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
50
+ def initialize(tag: DEFAULT_TAG, align: ALIGN_DEFAULT, tooltipped: false, body_arguments: {}, **system_arguments)
51
+ @align = fetch_or_fallback(ALIGN_OPTIONS, align, ALIGN_DEFAULT)
52
+ @system_arguments = system_arguments
53
+ @tooltipped = tooltipped
54
+ @body_arguments = body_arguments
55
+
56
+ body_tag = @body_arguments[:tag] || DEFAULT_BODY_TAG
57
+ @body_arguments[:tag] = fetch_or_fallback(BODY_TAG_OPTIONS, body_tag, DEFAULT_BODY_TAG)
58
+ @body_arguments[:classes] = class_names(
59
+ "AvatarStack-body",
60
+ @body_arguments[:classes]
61
+ )
62
+
63
+ @system_arguments[:tag] = fetch_or_fallback(TAG_OPTIONS, tag, DEFAULT_TAG)
64
+ @system_arguments[:classes] = class_names(
65
+ "AvatarStack",
66
+ system_arguments[:classes],
67
+ "AvatarStack--right" => @align == :right
68
+ )
69
+ end
70
+
71
+ def body_component
72
+ if @tooltipped
73
+ Primer::Tooltip.new(**@body_arguments)
74
+ else
75
+ Primer::BaseComponent.new(**@body_arguments)
76
+ end
77
+ end
78
+
79
+ def before_render
80
+ @system_arguments[:classes] = class_names(
81
+ @system_arguments[:classes],
82
+ "AvatarStack--two" => avatars.size == 2,
83
+ "AvatarStack--three-plus" => avatars.size > 2
84
+ )
85
+ end
86
+
87
+ def render?
88
+ avatars.any?
89
+ end
90
+ end
91
+ end
92
+ end
@@ -11,6 +11,7 @@ module Primer
11
11
  :condensed => "Box--condensed",
12
12
  :spacious => "Box--spacious"
13
13
  }.freeze
14
+ PADDING_SUGGESTION = "Perhaps you could consider using :padding options of #{PADDING_MAPPINGS.keys.to_sentence}?"
14
15
 
15
16
  # Optional Header.
16
17
  #
@@ -111,6 +112,8 @@ module Primer
111
112
  PADDING_MAPPINGS[fetch_or_fallback(PADDING_MAPPINGS.keys, padding, DEFAULT_PADDING)],
112
113
  system_arguments[:classes]
113
114
  )
115
+
116
+ @system_arguments[:system_arguments_denylist] = { [:p, :pt, :pb, :pr, :pl] => PADDING_SUGGESTION }
114
117
  end
115
118
 
116
119
  def render?
@@ -15,6 +15,10 @@ module Primer
15
15
 
16
16
  private
17
17
 
18
+ def force_system_arguments?
19
+ Rails.application.config.primer_view_components.force_system_arguments
20
+ end
21
+
18
22
  def deprecated_component_warning(new_class: nil, version: nil)
19
23
  return if Rails.env.production? || silence_deprecations?
20
24
 
@@ -25,8 +29,12 @@ module Primer
25
29
  ActiveSupport::Deprecation.warn(message)
26
30
  end
27
31
 
32
+ def aria(val, system_arguments)
33
+ system_arguments[:"aria-#{val}"] || system_arguments.dig(:aria, val.to_sym)
34
+ end
35
+
28
36
  def validate_aria_label
29
- aria_label = @system_arguments[:"aria-label"] || @system_arguments.dig(:aria, :label)
37
+ aria_label = aria("label", @system_arguments)
30
38
  raise ArgumentError, "`aria-label` is required." if aria_label.nil? && !Rails.env.production?
31
39
  end
32
40
 
@@ -5,6 +5,8 @@ module Primer
5
5
  class DetailsComponent < Primer::Component
6
6
  status :beta
7
7
 
8
+ BODY_TAG_DEFAULT = :div
9
+ BODY_TAG_OPTIONS = [:ul, :"details-menu", :"details-dialog", BODY_TAG_DEFAULT].freeze
8
10
  NO_OVERLAY = :none
9
11
  OVERLAY_MAPPINGS = {
10
12
  NO_OVERLAY => "",
@@ -27,21 +29,23 @@ module Primer
27
29
 
28
30
  # Use the Body slot as the main content to be shown when triggered by the Summary.
29
31
  #
32
+ # @param tag [Symbol] (Primer::DetailsComponent::BODY_TAG_DEFAULT) <%= one_of(Primer::DetailsComponent::BODY_TAG_OPTIONS) %>
30
33
  # @param kwargs [Hash] The same arguments as <%= link_to_system_arguments_docs %>.
31
- renders_one :body, lambda { |**system_arguments|
32
- system_arguments[:tag] ||= :div
34
+ renders_one :body, lambda { |tag: BODY_TAG_DEFAULT, **system_arguments|
35
+ system_arguments[:tag] = fetch_or_fallback(BODY_TAG_OPTIONS, tag, BODY_TAG_DEFAULT)
36
+
33
37
  Primer::BaseComponent.new(**system_arguments)
34
38
  }
35
39
 
36
40
  # @example Default
37
41
  #
38
42
  # <%= render Primer::DetailsComponent.new do |c| %>
39
- # component.summary do
40
- # "Summary"
41
- # end
42
- # component.body do
43
- # "Body"
44
- # end
43
+ # <% c.summary do %>
44
+ # Summary
45
+ # <% end %>
46
+ # <% c.body do %>
47
+ # Body
48
+ # <% end %>
45
49
  # <% end %>
46
50
  #
47
51
  # @param overlay [Symbol] Dictates the type of overlay to render with. <%= one_of(Primer::DetailsComponent::OVERLAY_MAPPINGS.keys) %>
@@ -5,8 +5,8 @@
5
5
  <%= render(Primer::SpinnerComponent.new(size: :large, flex: 1, "data-loading-slot": true)) %>
6
6
  <% end %>
7
7
 
8
- <input type="hidden" data-image-crop-input="x" name="cropped_x">
9
- <input type="hidden" data-image-crop-input="y" name="cropped_y">
10
- <input type="hidden" data-image-crop-input="width" name="cropped_width">
11
- <input type="hidden" data-image-crop-input="height" name="cropped_height">
8
+ <input autocomplete="off" type="hidden" data-image-crop-input="x" name="cropped_x">
9
+ <input autocomplete="off" type="hidden" data-image-crop-input="y" name="cropped_y">
10
+ <input autocomplete="off" type="hidden" data-image-crop-input="width" name="cropped_width">
11
+ <input autocomplete="off" type="hidden" data-image-crop-input="height" name="cropped_height">
12
12
  <% end %>
@@ -6,7 +6,7 @@ module Primer
6
6
  # A loading indicator that is shown while the image is loading.
7
7
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
8
8
  renders_one :loading, lambda { |**system_arguments|
9
- system_arguments[:tag] ||= :div
9
+ system_arguments[:tag] = :div
10
10
  system_arguments[:"data-loading-slot"] = true
11
11
 
12
12
  Primer::BaseComponent.new(**system_arguments)
@@ -99,17 +99,17 @@ module Primer
99
99
  # <p>And an unordered task list:</p>
100
100
  #
101
101
  # <ul>
102
- # <li><input type="checkbox" checked /> Create a sample markdown document</li>
103
- # <li><input type="checkbox"/> Add task lists to it</li>
104
- # <li><input type="checkbox"/> Take a vacation</li>
102
+ # <li><input type="checkbox" id="create-markdown" checked /><label for="create-markdown">Create a sample markdown document</label><br></li>
103
+ # <li><input type="checkbox" id="tasks-list" checked /><label for="tasks-list">Add tasks list to it</label><br></li>
104
+ # <li><input type="checkbox" id="take-vacation" checked /><label for="take-vacation">Take a vacation</label><br></li>
105
105
  # </ul>
106
106
  #
107
107
  # <p>And a "mixed" task list:</p>
108
108
  #
109
109
  # <ul>
110
- # <li><input type="checkbox"/> Steal underpants</li>
110
+ # <li><input type="checkbox"id="steal-underpants"/><label for="steal-underpants">Steal underpants</label></li>
111
111
  # <li>?</li>
112
- # <li><input type="checkbox"/> Profit!</li>
112
+ # <li><input type="checkbox"id="profit"/><label for="profit">Profit!</label></li>
113
113
  # </ul>
114
114
  #
115
115
  # And a nested list:
@@ -241,9 +241,9 @@ module Primer
241
241
  #
242
242
  # <pre><code>var foo = "bar";</code></pre>
243
243
  #
244
- # <pre><code>Long, single-line code blocks should not wrap. They should horizontally scroll if they are too long. This line should be long enough to demonstrate this.</code></pre>
244
+ # <pre tabindex="0"><code>Long, single-line code blocks should not wrap. They should horizontally scroll if they are too long. This line should be long enough to demonstrate this.</code></pre>
245
245
  #
246
- # <pre><code>var foo = "The same thing is true for code with syntax highlighting. A single line of code should horizontally scroll if it is really long.";</code></pre>
246
+ # <pre tabindex="0"><code>var foo = "The same thing is true for code with syntax highlighting. A single line of code should horizontally scroll if it is really long.";</code></pre>
247
247
  #
248
248
  # <p>Inline code inside table cells should still be distinguishable.</p>
249
249
  #
@@ -270,11 +270,11 @@ module Primer
270
270
  #
271
271
  # <p>Small images should be shown at their actual size.</p>
272
272
  #
273
- # <p><img src="http://placekitten.com/g/300/200/"/></p>
273
+ # <p><img alt="kitten" src="http://placekitten.com/g/300/200/"/></p>
274
274
  #
275
275
  # <p>Large images should always scale down and fit in the content container.</p>
276
276
  #
277
- # <p><img src="http://placekitten.com/g/1200/800/"/></p>
277
+ # <p><img alt="kitten" src="http://placekitten.com/g/1200/800/"/></p>
278
278
  #
279
279
  # <pre><code>This is the final element on the page and there should be no margin below this.</code></pre>
280
280
  # <% end %>
@@ -3,11 +3,15 @@
3
3
  module Primer
4
4
  # Use `Menu` to create vertical lists of navigational links.
5
5
  class MenuComponent < Primer::Component
6
+ HEADING_TAG_OPTIONS = [:h1, :h2, :h3, :h4, :h5, :h6].freeze
7
+ HEADING_TAG_FALLBACK = :h2
8
+
6
9
  # Optional menu heading
7
10
  #
11
+ # @param tag [Symbol] <%= one_of(Primer::MenuComponent::HEADING_TAG_OPTIONS) %>
8
12
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
9
- renders_one :heading, lambda { |**system_arguments|
10
- system_arguments[:tag] ||= :span
13
+ renders_one :heading, lambda { |tag:, **system_arguments|
14
+ system_arguments[:tag] = fetch_or_fallback(HEADING_TAG_OPTIONS, tag, HEADING_TAG_FALLBACK)
11
15
  system_arguments[:classes] = class_names(
12
16
  "menu-heading",
13
17
  system_arguments[:classes]
@@ -35,7 +39,7 @@ module Primer
35
39
 
36
40
  # @example Default
37
41
  # <%= render(Primer::MenuComponent.new) do |c| %>
38
- # <% c.heading do %>
42
+ # <% c.heading(tag: :h2) do %>
39
43
  # Heading
40
44
  # <% end %>
41
45
  # <% c.item(selected: true, href: "#url") do %>
@@ -18,10 +18,23 @@ module Primer
18
18
  #
19
19
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
20
20
  renders_one :panel, lambda { |**system_arguments|
21
- system_arguments[:tag] ||= :div
21
+ return unless @with_panel
22
+
23
+ system_arguments[:id] = @panel_id
24
+ system_arguments[:tag] = :div
22
25
  system_arguments[:role] ||= :tabpanel
26
+ system_arguments[:tabindex] = 0
23
27
  system_arguments[:hidden] = true unless @selected
24
28
 
29
+ label_present = aria("label", system_arguments) || aria("labelledby", system_arguments)
30
+ unless label_present
31
+ if @id.present?
32
+ system_arguments[:"aria-labelledby"] = @id
33
+ elsif !Rails.env.production?
34
+ raise ArgumentError, "Panels must be labelled. Either set a unique `id` on the tab, or set an `aria-label` directly on the panel"
35
+ end
36
+ end
37
+
25
38
  Primer::BaseComponent.new(**system_arguments)
26
39
  }
27
40
 
@@ -86,22 +99,26 @@ module Primer
86
99
  # @param list [Boolean] Whether the Tab is an item in a `<ul>` list.
87
100
  # @param selected [Boolean] Whether the Tab is selected or not.
88
101
  # @param with_panel [Boolean] Whether the Tab has an associated panel.
102
+ # @param panel_id [String] Only applies if `with_panel` is `true`. Unique id of panel.
89
103
  # @param icon_classes [Boolean] Classes that must always be applied to icons.
90
104
  # @param wrapper_arguments [Hash] <%= link_to_system_arguments_docs %> to be used in the `<li>` wrapper when the tab is an item in a list.
91
105
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
92
- def initialize(list: false, selected: false, with_panel: false, icon_classes: "", wrapper_arguments: {}, **system_arguments)
106
+ def initialize(list: false, selected: false, with_panel: false, panel_id: "", icon_classes: "", wrapper_arguments: {}, **system_arguments)
93
107
  @selected = selected
94
108
  @icon_classes = icon_classes
95
109
  @list = list
110
+ @with_panel = with_panel
96
111
 
97
112
  @system_arguments = system_arguments
113
+ @id = @system_arguments[:id]
98
114
 
99
- if with_panel
100
- @system_arguments[:tag] ||= :button
115
+ if with_panel || @system_arguments[:tag] == :button
116
+ @system_arguments[:tag] = :button
101
117
  @system_arguments[:type] = :button
102
118
  @system_arguments[:role] = :tab
119
+ panel_id(panel_id)
103
120
  else
104
- @system_arguments[:tag] ||= :a
121
+ @system_arguments[:tag] = :a
105
122
  end
106
123
 
107
124
  @wrapper_arguments = wrapper_arguments
@@ -111,7 +128,7 @@ module Primer
111
128
  return unless @selected
112
129
 
113
130
  if @system_arguments[:tag] == :a
114
- aria_current = @system_arguments[:"aria-current"] || @system_arguments.dig(:aria, :current) || DEFAULT_ARIA_CURRENT_FOR_ANCHOR
131
+ aria_current = aria("current", system_arguments) || DEFAULT_ARIA_CURRENT_FOR_ANCHOR
115
132
  @system_arguments[:"aria-current"] = fetch_or_fallback(ARIA_CURRENT_OPTIONS_FOR_ANCHOR, aria_current, DEFAULT_ARIA_CURRENT_FOR_ANCHOR)
116
133
  else
117
134
  @system_arguments[:"aria-selected"] = true
@@ -128,6 +145,17 @@ module Primer
128
145
  yield
129
146
  end
130
147
  end
148
+
149
+ private
150
+
151
+ def panel_id(panel_id)
152
+ if panel_id.blank?
153
+ raise ArgumentError, "`panel_id` is required" unless Rails.env.production?
154
+ else
155
+ @panel_id = panel_id
156
+ @system_arguments[:"aria-controls"] = @panel_id
157
+ end
158
+ end
131
159
  end
132
160
  end
133
161
  end