primer_view_components 0.0.45 → 0.0.49

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 (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