primer_view_components 0.1.0 → 0.1.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/app/assets/javascripts/primer_view_components.js +1 -1
  4. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  5. data/app/assets/styles/primer_view_components.css +1 -1
  6. data/app/assets/styles/primer_view_components.css.map +1 -1
  7. data/app/components/primer/alpha/action_list/divider.rb +2 -2
  8. data/app/components/primer/alpha/action_list/heading.html.erb +1 -1
  9. data/app/components/primer/alpha/action_list/heading.rb +10 -4
  10. data/app/components/primer/alpha/action_list/item.rb +3 -3
  11. data/app/components/primer/alpha/action_list.html.erb +6 -8
  12. data/app/components/primer/alpha/action_list.rb +5 -10
  13. data/app/components/primer/alpha/nav_list/{section.rb → group.rb} +5 -5
  14. data/app/components/primer/alpha/nav_list/item.html.erb +1 -1
  15. data/app/components/primer/alpha/nav_list/item.rb +15 -1
  16. data/app/components/primer/alpha/nav_list.d.ts +1 -0
  17. data/app/components/primer/alpha/nav_list.html.erb +8 -8
  18. data/app/components/primer/alpha/nav_list.js +21 -0
  19. data/app/components/primer/alpha/nav_list.rb +28 -32
  20. data/app/components/primer/alpha/nav_list.ts +23 -0
  21. data/app/components/primer/alpha/navigation/tab.rb +168 -0
  22. data/app/components/primer/alpha/overlay.rb +19 -6
  23. data/app/components/primer/alpha/tab_nav.rb +10 -3
  24. data/app/components/primer/alpha/tab_panels.rb +2 -2
  25. data/app/components/primer/alpha/underline_nav.css +1 -1
  26. data/app/components/primer/alpha/underline_nav.css.map +1 -1
  27. data/app/components/primer/alpha/underline_nav.pcss +1 -0
  28. data/app/components/primer/alpha/underline_nav.rb +2 -2
  29. data/app/components/primer/alpha/underline_panels.rb +2 -2
  30. data/app/components/primer/beta/button.html.erb +1 -1
  31. data/app/components/primer/beta/button.rb +2 -1
  32. data/app/components/primer/component.rb +34 -0
  33. data/app/components/primer/navigation/tab_component.rb +3 -157
  34. data/lib/primer/deprecations.yml +4 -0
  35. data/lib/primer/view_components/version.rb +1 -1
  36. data/lib/primer/yard/component_manifest.rb +2 -1
  37. data/lib/tasks/docs.rake +1 -1
  38. data/previews/primer/alpha/action_list_preview.rb +6 -14
  39. data/previews/primer/alpha/nav_list_preview/trailing_action.html.erb +19 -0
  40. data/previews/primer/alpha/nav_list_preview.rb +19 -30
  41. data/previews/primer/alpha/overlay_preview.rb +7 -2
  42. data/previews/primer/alpha/tab_nav_preview/with_extra.html.erb +8 -0
  43. data/previews/primer/alpha/tab_nav_preview.rb +5 -0
  44. data/previews/primer/alpha/tab_panels_preview/with_extra.html.erb +17 -0
  45. data/previews/primer/alpha/tab_panels_preview.rb +5 -0
  46. data/static/arguments.json +63 -7
  47. data/static/audited_at.json +2 -1
  48. data/static/constants.json +20 -8
  49. data/static/previews.json +10 -0
  50. data/static/statuses.json +3 -2
  51. metadata +8 -6
  52. data/app/components/primer/alpha/nav_list/section.html.erb +0 -3
  53. data/previews/primer/alpha/action_list_preview/heading.html.erb +0 -4
  54. /data/app/components/primer/{navigation/tab_component.html.erb → alpha/navigation/tab.html.erb} +0 -0
@@ -3,7 +3,7 @@
3
3
  module Primer
4
4
  module Alpha
5
5
  class ActionList
6
- # Section heading rendered above the section contents.
6
+ # Group heading rendered above the group contents.
7
7
  class Divider < Primer::Component
8
8
  DEFAULT_SCHEME = :subtle
9
9
  SCHEME_MAPPINGS = {
@@ -16,7 +16,7 @@ module Primer
16
16
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
17
17
  def initialize(scheme: DEFAULT_SCHEME, **system_arguments)
18
18
  @system_arguments = system_arguments
19
- @system_arguments[:tag] = :li
19
+ @system_arguments[:tag] = :div
20
20
  @system_arguments[:role] = :separator
21
21
  @system_arguments[:'aria-hidden'] = true
22
22
  @scheme = fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)
@@ -1,4 +1,4 @@
1
- <%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
1
+ <%= render(Primer::BaseComponent.new(tag: :div, **@system_arguments)) do %>
2
2
  <%= render(Primer::BaseComponent.new(tag: @tag, classes: "ActionList-sectionDivider-title", id: @list_id)) do %>
3
3
  <%= @title %>
4
4
  <% end %>
@@ -11,21 +11,27 @@ module Primer
11
11
  :filled => "ActionList-sectionDivider--filled"
12
12
  }.freeze
13
13
  SCHEME_OPTIONS = SCHEME_MAPPINGS.keys.freeze
14
+ HEADING_MIN = 1
15
+ HEADING_MAX = 6
16
+ HEADING_LEVELS = (HEADING_MIN..HEADING_MAX).to_a.freeze
14
17
 
15
18
  # @param list_id [String] The unique identifier of the sub list the heading belongs to. Used internally.
16
19
  # @param title [String] Sub list title.
20
+ # @param heading_level [Integer] Heading level. Level 2 results in an `<h2>` tag, level 3 an `<h3>` tag, etc.
17
21
  # @param subtitle [String] Optional sub list description.
18
22
  # @param scheme [Symbol] Display a background color if scheme is `filled`.
19
- # @param tag [Symbol] Semantic tag for the heading.
23
+ # @param tag [Integer] Semantic tag for the heading.
20
24
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
21
- def initialize(list_id:, title:, tag: :h3, scheme: DEFAULT_SCHEME, subtitle: nil, **system_arguments)
22
- @tag = tag
25
+ def initialize(list_id:, title:, heading_level: 3, scheme: DEFAULT_SCHEME, subtitle: nil, **system_arguments)
26
+ raise "Heading level must be between #{HEADING_MIN} and #{HEADING_MAX}" unless HEADING_LEVELS.include?(heading_level)
27
+
28
+ @heading_level = heading_level
29
+ @tag = :"h#{heading_level}"
23
30
  @system_arguments = system_arguments
24
31
  @list_id = list_id
25
32
  @title = title
26
33
  @subtitle = subtitle
27
34
  @scheme = fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)
28
- @system_arguments[:tag] = :li
29
35
  @system_arguments[:classes] = class_names(
30
36
  "ActionList-sectionDivider",
31
37
  SCHEME_MAPPINGS[@scheme],
@@ -112,7 +112,7 @@ module Primer
112
112
  # @private
113
113
  renders_one :private_content
114
114
 
115
- attr_reader :list, :href, :active, :disabled, :parent
115
+ attr_reader :id, :list, :href, :active, :disabled, :parent
116
116
 
117
117
  # Whether or not this item is active.
118
118
  #
@@ -150,7 +150,7 @@ module Primer
150
150
  parent: nil,
151
151
  truncate_label: false,
152
152
  href: nil,
153
- role: :listitem,
153
+ role: nil,
154
154
  size: DEFAULT_SIZE,
155
155
  scheme: DEFAULT_SCHEME,
156
156
  disabled: false,
@@ -184,7 +184,7 @@ module Primer
184
184
  "ActionListItem"
185
185
  )
186
186
 
187
- @system_arguments[:role] = role
187
+ @system_arguments[:role] = role if role
188
188
 
189
189
  @system_arguments[:aria] ||= {}
190
190
  @system_arguments[:aria][:disabled] = "true" if @disabled
@@ -1,15 +1,13 @@
1
- <%= render(Primer::BaseComponent.new(tag: :ul, role: @role, classes: "ActionListWrap")) do %>
1
+ <%= render(Primer::BaseComponent.new(tag: :div)) do %>
2
2
  <% if heading %>
3
3
  <%= heading %>
4
4
  <% end %>
5
- <%= render(Primer::BaseComponent.new(tag: :li, **@list_wrapper_arguments)) do %>
6
- <%= render(Primer::BaseComponent.new(tag: :ul, **@system_arguments)) do %>
7
- <% items.each_with_index do |item, index| %>
8
- <% if index > 0 && @show_dividers %>
9
- <%= render(Primer::Alpha::ActionList::Divider.new) %>
10
- <% end %>
11
- <%= item %>
5
+ <%= render(Primer::BaseComponent.new(tag: :ul, **@system_arguments)) do %>
6
+ <% items.each_with_index do |item, index| %>
7
+ <% if index > 0 && @show_dividers %>
8
+ <%= render(Primer::Alpha::ActionList::Divider.new) %>
12
9
  <% end %>
10
+ <%= item %>
13
11
  <% end %>
14
12
  <% end %>
15
13
  <% end %>
@@ -61,10 +61,10 @@ module Primer
61
61
  **system_arguments
62
62
  )
63
63
  @id = self.class.generate_id
64
- @role = role
65
64
 
66
65
  @system_arguments = system_arguments
67
66
  @system_arguments[:tag] = :ul
67
+ @system_arguments[:role] = role
68
68
  @item_classes = item_classes
69
69
  @scheme = fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)
70
70
  @show_dividers = show_dividers
@@ -72,7 +72,6 @@ module Primer
72
72
  SCHEME_MAPPINGS[@scheme],
73
73
  system_arguments[:classes],
74
74
  "ActionListWrap",
75
- "ActionListWrap--subGroup",
76
75
  "ActionListWrap--divided" => @show_dividers
77
76
  )
78
77
 
@@ -81,18 +80,14 @@ module Primer
81
80
 
82
81
  # @private
83
82
  def before_render
83
+ aria_label = aria(:label, @system_arguments)
84
+
84
85
  if heading.present?
85
86
  @system_arguments[:"aria-labelledby"] = @id
86
- elsif aria(:label, @system_arguments).blank?
87
+ raise ArgumentError, "An aria-label should not be provided if a heading is present" if aria_label.present?
88
+ elsif aria_label.blank?
87
89
  raise ArgumentError, "An aria-label or heading must be provided"
88
90
  end
89
-
90
- return if items.blank?
91
-
92
- @list_wrapper_arguments[:classes] = class_names(
93
- @list_wrapper_arguments[:classes],
94
- "ActionListItem--hasSubItem"
95
- )
96
91
  end
97
92
 
98
93
  # @private
@@ -6,10 +6,10 @@ module Primer
6
6
  # A logical grouping of navigation links with an optional heading.
7
7
  #
8
8
  # See <%= link_to_component(Primer::Alpha::NavList) %> for usage examples.
9
- class Section < ActionList
10
- # A special "show more" list item that appears at the bottom of the section. Clicking
9
+ class Group < ActionList
10
+ # A special "show more" list item that appears at the bottom of the group. Clicking
11
11
  # the item will fetch the next page of results from the URL passed in the `src` argument
12
- # and append the resulting chunk of HTML to the section.
12
+ # and append the resulting chunk of HTML to the group.
13
13
  #
14
14
  # @param src [String] The URL to query for additional pages of list items.
15
15
  # @param pages [Integer] The total number of pages in the result set.
@@ -51,14 +51,14 @@ module Primer
51
51
  super(**@system_arguments)
52
52
  end
53
53
 
54
- # Cause this section to show its list of sub items when rendered.
54
+ # Cause this group to show its list of sub items when rendered.
55
55
  # :nocov:
56
56
  def expand!
57
57
  @expanded = true
58
58
  end
59
59
  # :nocov:
60
60
 
61
- # The items contained within this section.
61
+ # The items contained within this group.
62
62
  #
63
63
  # @return [Array<Primer::Alpha::ActionList::Item>]
64
64
  def items
@@ -1,7 +1,7 @@
1
1
  <% with_private_content do %>
2
2
  <% unless items.empty? %>
3
3
  <% capture do %>
4
- <%= render(Primer::BaseComponent.new(tag: :ul, **@sub_list_arguments)) do %>
4
+ <%= render(Primer::BaseComponent.new(tag: :ul, role: :list, **@sub_list_arguments)) do %>
5
5
  <% items.each do |item| %>
6
6
  <%= item %>
7
7
  <% end %>
@@ -47,6 +47,10 @@ module Primer
47
47
  )
48
48
  }
49
49
 
50
+ @list = system_arguments[:list]
51
+
52
+ @sub_list_arguments["data-action"] = "keydown:#{@list.custom_element_name}#handleItemWithSubItemKeydown" if @list
53
+
50
54
  overrides = { "data-item-id": @selected_by_ids.join(" ") }
51
55
 
52
56
  super(**system_arguments, **overrides)
@@ -79,9 +83,19 @@ module Primer
79
83
 
80
84
  return if items.blank?
81
85
 
86
+ @sub_list_arguments[:aria] = merge_aria(
87
+ @sub_list_arguments,
88
+ { aria: { labelledby: id } }
89
+ )
90
+
91
+ raise ArgumentError, "Items with sub-items cannot have hrefs" if href.present?
92
+
82
93
  @content_arguments[:tag] = :button
83
94
  @content_arguments[:"aria-expanded"] = @expanded.to_s
84
- @content_arguments[:"data-action"] = "click:#{@list.custom_element_name}#handleItemWithSubItemClick"
95
+ @content_arguments[:"data-action"] = "
96
+ click:#{@list.custom_element_name}#handleItemWithSubItemClick
97
+ keydown:#{@list.custom_element_name}#handleItemWithSubItemKeydown
98
+ "
85
99
 
86
100
  with_private_trailing_action_icon(:"chevron-down", classes: "ActionListItem-collapseIcon")
87
101
 
@@ -18,6 +18,7 @@ export declare class NavListElement extends HTMLElement {
18
18
  collapseItem(item: HTMLElement): void;
19
19
  itemIsExpanded(item: HTMLElement | null): boolean;
20
20
  handleItemWithSubItemClick(e: Event): void;
21
+ handleItemWithSubItemKeydown(e: KeyboardEvent): void;
21
22
  private showMore;
22
23
  private setShowMoreItemState;
23
24
  }
@@ -1,10 +1,10 @@
1
- <%= render(Primer::BaseComponent.new(tag: :ul, **@system_arguments)) do %>
2
- <% sections.each_with_index do |section, index| %>
3
- <% if index > 0 %>
4
- <%= render(Primer::Alpha::ActionList::Divider.new) %>
1
+ <%= render(Primer::BaseComponent.new(tag: :nav, **@system_arguments)) do %>
2
+ <nav-list>
3
+ <% groups.each_with_index do |group, index| %>
4
+ <% if index > 0 %>
5
+ <%= render(Primer::Alpha::ActionList::Divider.new) %>
6
+ <% end %>
7
+ <%= group %>
5
8
  <% end %>
6
- <li>
7
- <%= section %>
8
- </li>
9
- <% end %>
9
+ </nav-list>
10
10
  <% end %>
@@ -82,6 +82,7 @@ let NavListElement = class NavListElement extends HTMLElement {
82
82
  var _a;
83
83
  (_a = item.nextElementSibling) === null || _a === void 0 ? void 0 : _a.setAttribute('data-hidden', '');
84
84
  item.setAttribute('aria-expanded', 'false');
85
+ item.focus();
85
86
  }
86
87
  itemIsExpanded(item) {
87
88
  if ((item === null || item === void 0 ? void 0 : item.tagName) === 'A') {
@@ -105,6 +106,26 @@ let NavListElement = class NavListElement extends HTMLElement {
105
106
  }
106
107
  e.stopPropagation();
107
108
  }
109
+ // collapse item
110
+ handleItemWithSubItemKeydown(e) {
111
+ const el = e.currentTarget;
112
+ if (!(el instanceof HTMLElement))
113
+ return;
114
+ let button = el.closest('button');
115
+ if (!button) {
116
+ const button_id = el.getAttribute('aria-labelledby');
117
+ if (button_id) {
118
+ button = document.getElementById(button_id);
119
+ }
120
+ else {
121
+ return;
122
+ }
123
+ }
124
+ if (this.itemIsExpanded(button) && e.key === 'Escape') {
125
+ this.collapseItem(button);
126
+ }
127
+ e.stopPropagation();
128
+ }
108
129
  async showMore(e) {
109
130
  var _a, _b;
110
131
  e.preventDefault();
@@ -3,13 +3,13 @@
3
3
  module Primer
4
4
  module Alpha
5
5
  # `NavList` provides a simple way to render side navigation, i.e. navigation
6
- # that appears to the left or right side of some main content. Each section in a
6
+ # that appears to the left or right side of some main content. Each group in a
7
7
  # nav list is a list of links.
8
8
  #
9
- # Nav list sections can contain sub items. Rather than navigating to a URL, sections
9
+ # Nav list groups can contain sub items. Rather than navigating to a URL, groups
10
10
  # with sub items expand and collapse on click. To indicate this functionality, the
11
- # section will automatically render with a trailing chevron icon that changes direction
12
- # when the section expands and collapses.
11
+ # group will automatically render with a trailing chevron icon that changes direction
12
+ # when the group expands and collapses.
13
13
  #
14
14
  # Nav list items appear visually active when selected. Each nav item must have one
15
15
  # or more ID values that determine which item will appear selected. Use the
@@ -22,40 +22,40 @@ module Primer
22
22
  "nav-list"
23
23
  end
24
24
 
25
- # Sections. Each section is a list of links and an optional heading.
25
+ # Groups. Each group is a list of links and an optional heading.
26
26
  #
27
- # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::NavList::Section) %>.
28
- renders_many :sections, lambda { |**system_arguments|
29
- Primer::Alpha::NavList::Section.new(selected_item_id: @selected_item_id, **system_arguments)
27
+ # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::NavList::Group) %>.
28
+ renders_many :groups, lambda { |**system_arguments|
29
+ Primer::Alpha::NavList::Group.new(selected_item_id: @selected_item_id, **system_arguments)
30
30
  }
31
31
 
32
32
  # @example Items and headings
33
33
  #
34
34
  # <%= render(Primer::Alpha::NavList.new(selected_item_id: :personal_info)) do |component| %>
35
- # <% component.with_section(aria: { label: "Settings" }) do |section| %>
36
- # <% section.with_item(label: "General", selected_by_ids: :general, href: "/settings/general") %>
35
+ # <% component.with_group(aria: { label: "Settings" }) do |group| %>
36
+ # <% group.with_item(label: "General", selected_by_ids: :general, href: "/settings/general") %>
37
37
  # <% end %>
38
- # <% component.with_section(aria: { label: "Account settings" }) do |section| %>
39
- # <% section.with_heading(title: "Account Settings") %>
40
- # <% section.with_item(label: "Personal Information", selected_by_ids: :personal_info, href: "/account/info") %>
41
- # <% section.with_item(label: "Password", selected_by_ids: :password, href: "/account/password") %>
42
- # <% section.with_item(label: "Billing info", selected_by_ids: :billing, href: "/account/billing") %>
38
+ # <% component.with_group do |group| %>
39
+ # <% group.with_heading(title: "Account Settings") %>
40
+ # <% group.with_item(label: "Personal Information", selected_by_ids: :personal_info, href: "/account/info") %>
41
+ # <% group.with_item(label: "Password", selected_by_ids: :password, href: "/account/password") %>
42
+ # <% group.with_item(label: "Billing info", selected_by_ids: :billing, href: "/account/billing") %>
43
43
  # <% end %>
44
44
  # <% end %>
45
45
  #
46
46
  # @example Leading and trailing visuals
47
47
  #
48
48
  # <%= render(Primer::Alpha::NavList.new(selected_item_id: :personal_info)) do |component| %>
49
- # <% component.with_section(aria: { label: "Account settings" }) do |section| %>
50
- # <% section.with_heading(title: "Account Settings") %>
51
- # <% section.with_item(label: "Personal Information", selected_by_ids: :personal_info, href: "/account/info") do |item| %>
49
+ # <% component.with_group do |group| %>
50
+ # <% group.with_heading(title: "Account Settings") %>
51
+ # <% group.with_item(label: "Personal Information", selected_by_ids: :personal_info, href: "/account/info") do |item| %>
52
52
  # <% item.with_leading_visual_avatar(src: "https://github.com/github.png", alt: "GitHub") %>
53
53
  # <% end %>
54
- # <% section.with_item(label: "Notifications", selected_by_ids: :notifications, href: "/account/notifications") do |item| %>
54
+ # <% group.with_item(label: "Notifications", selected_by_ids: :notifications, href: "/account/notifications") do |item| %>
55
55
  # <% item.with_leading_visual_icon(icon: :bell) %>
56
56
  # <% item.with_trailing_visual_counter(count: 15) %>
57
57
  # <% end %>
58
- # <% section.with_item(label: "Billing info", selected_by_ids: :billing, href: "/account/billing") do |item| %>
58
+ # <% group.with_item(label: "Billing info", selected_by_ids: :billing, href: "/account/billing") do |item| %>
59
59
  # <% item.with_leading_visual_icon(icon: :package) %>
60
60
  # <% item.with_trailing_visual_icon(icon: :"dot-fill", color: :attention) %>
61
61
  # <% end %>
@@ -65,9 +65,9 @@ module Primer
65
65
  # @example Expandable sub items
66
66
  #
67
67
  # <%= render(Primer::Alpha::NavList.new(selected_item_id: :email_notifications)) do |component| %>
68
- # <% component.with_section(aria: { label: "Account settings" }) do |section| %>
69
- # <% section.with_heading(title: "Account Settings") %>
70
- # <% section.with_item(label: "Notification settings") do |item| %>
68
+ # <% component.with_group do |group| %>
69
+ # <% group.with_heading(title: "Account Settings") %>
70
+ # <% group.with_item(label: "Notification settings") do |item| %>
71
71
  # <% item.with_leading_visual_icon(icon: :bell) %>
72
72
  # <% item.with_item(label: "Email", selected_by_ids: :email_notifications, href: "/account/notifications/email") do |subitem| %>
73
73
  # <% subitem.with_trailing_visual_icon(icon: :mail) %>
@@ -76,7 +76,7 @@ module Primer
76
76
  # <% subitem.with_trailing_visual_icon(icon: :"device-mobile") %>
77
77
  # <% end %>
78
78
  # <% end %>
79
- # <% section.with_item(label: "Messages") do |item| %>
79
+ # <% group.with_item(label: "Messages") do |item| %>
80
80
  # <% item.with_leading_visual_icon(icon: :bookmark) %>
81
81
  # <% item.with_item(label: "Inbox", href: "/account/messages/inbox") do |subitem| %>
82
82
  # <% subitem.with_trailing_visual_counter(count: 10) %>
@@ -91,12 +91,12 @@ module Primer
91
91
  # @example Trailing action
92
92
  #
93
93
  # <%= render(Primer::Alpha::NavList.new) do |component| %>
94
- # <% component.with_section(aria: { label: "Favorite foods" }) do |section| %>
95
- # <% section.with_heading(title: "My Favorite Foods") %>
96
- # <% section.with_item(label: "Popplers", selected_by_ids: :popplers, href: "/foods/popplers") do |item| %>
94
+ # <% component.with_group do |group| %>
95
+ # <% group.with_heading(title: "My Favorite Foods") %>
96
+ # <% group.with_item(label: "Popplers", selected_by_ids: :popplers, href: "/foods/popplers") do |item| %>
97
97
  # <% item.with_trailing_action(show_on_hover: false, icon: "plus", "aria-label": "Add new food", size: :medium) %>
98
98
  # <% end %>
99
- # <% section.with_item(label: "Slurm", selected_by_ids: :slurm, href: "/foods/slurm") do |item| %>
99
+ # <% group.with_item(label: "Slurm", selected_by_ids: :slurm, href: "/foods/slurm") do |item| %>
100
100
  # <% item.with_trailing_action(show_on_hover: true, icon: "plus", "aria-label": "Add new food", size: :medium) %>
101
101
  # <% end %>
102
102
  # <% end %>
@@ -106,10 +106,6 @@ module Primer
106
106
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
107
107
  def initialize(selected_item_id: nil, **system_arguments)
108
108
  @system_arguments = system_arguments
109
- @system_arguments[:classes] = class_names(
110
- @system_arguments[:classes],
111
- "ActionListWrap"
112
- )
113
109
  @selected_item_id = selected_item_id
114
110
  end
115
111
  end
@@ -87,6 +87,7 @@ export class NavListElement extends HTMLElement {
87
87
  collapseItem(item: HTMLElement) {
88
88
  item.nextElementSibling?.setAttribute('data-hidden', '')
89
89
  item.setAttribute('aria-expanded', 'false')
90
+ item.focus()
90
91
  }
91
92
 
92
93
  itemIsExpanded(item: HTMLElement | null) {
@@ -112,6 +113,28 @@ export class NavListElement extends HTMLElement {
112
113
  e.stopPropagation()
113
114
  }
114
115
 
116
+ // collapse item
117
+ handleItemWithSubItemKeydown(e: KeyboardEvent) {
118
+ const el = e.currentTarget
119
+ if (!(el instanceof HTMLElement)) return
120
+
121
+ let button = el.closest<HTMLButtonElement>('button')
122
+ if (!button) {
123
+ const button_id = el.getAttribute('aria-labelledby')
124
+ if (button_id) {
125
+ button = document.getElementById(button_id) as HTMLButtonElement
126
+ } else {
127
+ return
128
+ }
129
+ }
130
+
131
+ if (this.itemIsExpanded(button) && e.key === 'Escape') {
132
+ this.collapseItem(button)
133
+ }
134
+
135
+ e.stopPropagation()
136
+ }
137
+
115
138
  private async showMore(e: Event) {
116
139
  e.preventDefault()
117
140
  if (this.showMoreDisabled) return
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module Alpha
5
+ module Navigation
6
+ # This component is part of navigation components such as `Primer::Alpha::TabNav`
7
+ # and `Primer::Alpha::UnderlineNav` and should not be used by itself.
8
+ #
9
+ # @accessibility
10
+ # `Tab` renders the selected anchor tab with `aria-current="page"` by default.
11
+ # When the selected tab does not correspond to the current page, such as in a nested inner tab, make sure to use aria-current="true"
12
+ class Tab < Primer::Component
13
+ status :alpha
14
+
15
+ DEFAULT_ARIA_CURRENT_FOR_ANCHOR = :page
16
+ ARIA_CURRENT_OPTIONS_FOR_ANCHOR = [true, DEFAULT_ARIA_CURRENT_FOR_ANCHOR].freeze
17
+ # Panel controlled by the Tab. This will not render anything in the tab itself.
18
+ # It will provide a accessor for the Tab's parent to call and render the panel
19
+ # content in the appropriate place.
20
+ # Refer to `UnderlineNav` and `TabNav` implementations for examples.
21
+ #
22
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
23
+ renders_one :panel, lambda { |**system_arguments|
24
+ return unless @with_panel
25
+
26
+ deny_tag_argument(**system_arguments)
27
+ system_arguments[:id] = @panel_id
28
+ system_arguments[:tag] = :div
29
+ system_arguments[:role] ||= :tabpanel
30
+ system_arguments[:tabindex] = 0
31
+ system_arguments[:hidden] = true unless @selected
32
+
33
+ label_present = aria("label", system_arguments) || aria("labelledby", system_arguments)
34
+ unless label_present
35
+ if @id.present?
36
+ system_arguments[:"aria-labelledby"] = @id
37
+ elsif !Rails.env.production?
38
+ raise ArgumentError, "Panels must be labelled. Either set a unique `id` on the tab, or set an `aria-label` directly on the panel"
39
+ end
40
+ end
41
+
42
+ Primer::BaseComponent.new(**system_arguments)
43
+ }
44
+
45
+ # Icon to be rendered in the Tab left.
46
+ #
47
+ # @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::Beta::Octicon) %>.
48
+ renders_one :icon, lambda { |icon = nil, **system_arguments|
49
+ system_arguments[:classes] = class_names(
50
+ @icon_classes,
51
+ system_arguments[:classes]
52
+ )
53
+ Primer::Beta::Octicon.new(icon, **system_arguments)
54
+ }
55
+
56
+ # The Tab's text.
57
+ #
58
+ # @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::Beta::Text) %>.
59
+ renders_one :text, Primer::Beta::Text
60
+
61
+ # Counter to be rendered in the Tab right.
62
+ #
63
+ # @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::Beta::Counter) %>.
64
+ renders_one :counter, Primer::Beta::Counter
65
+
66
+ attr_reader :selected
67
+
68
+ # @example Default
69
+ # <%= render(Primer::Alpha::Navigation::Tab.new(selected: true)) do |component| %>
70
+ # <% component.with_text { "Selected" } %>
71
+ # <% end %>
72
+ # <%= render(Primer::Alpha::Navigation::Tab.new) do |component| %>
73
+ # <% component.with_text { "Not selected" } %>
74
+ # <% end %>
75
+ #
76
+ # @example With icons and counters
77
+ # <%= render(Primer::Alpha::Navigation::Tab.new) do |component| %>
78
+ # <% component.with_icon(:star) %>
79
+ # <% component.with_text { "Tab" } %>
80
+ # <% end %>
81
+ # <%= render(Primer::Alpha::Navigation::Tab.new) do |component| %>
82
+ # <% component.with_icon(:star) %>
83
+ # <% component.with_text { "Tab" } %>
84
+ # <% component.with_counter(count: 10) %>
85
+ # <% end %>
86
+ # <%= render(Primer::Alpha::Navigation::Tab.new) do |component| %>
87
+ # <% component.with_text { "Tab" } %>
88
+ # <% component.with_counter(count: 10) %>
89
+ # <% end %>
90
+ #
91
+ # @example Inside a list
92
+ # <%= render(Primer::Alpha::Navigation::Tab.new(list: true)) do |component| %>
93
+ # <% component.with_text { "Tab" } %>
94
+ # <% end %>
95
+ #
96
+ # @example With custom HTML
97
+ # <%= render(Primer::Alpha::Navigation::Tab.new) do %>
98
+ # <div>
99
+ # This is my <strong>custom HTML</strong>
100
+ # </div>
101
+ # <% end %>
102
+ #
103
+ # @param list [Boolean] Whether the Tab is an item in a `<ul>` list.
104
+ # @param selected [Boolean] Whether the Tab is selected or not.
105
+ # @param with_panel [Boolean] Whether the Tab has an associated panel.
106
+ # @param panel_id [String] Only applies if `with_panel` is `true`. Unique id of panel.
107
+ # @param icon_classes [Boolean] Classes that must always be applied to icons.
108
+ # @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.
109
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
110
+ def initialize(list: false, selected: false, with_panel: false, panel_id: "", icon_classes: "", wrapper_arguments: {}, **system_arguments)
111
+ @selected = selected
112
+ @icon_classes = icon_classes
113
+ @list = list
114
+ @with_panel = with_panel
115
+
116
+ @system_arguments = system_arguments
117
+ @id = @system_arguments[:id]
118
+ @wrapper_arguments = wrapper_arguments
119
+
120
+ if with_panel || @system_arguments[:tag] == :button
121
+ @system_arguments[:tag] = :button
122
+ @system_arguments[:type] = :button
123
+ @system_arguments[:role] = :tab
124
+ panel_id(panel_id)
125
+ # https://www.w3.org/TR/wai-aria-practices/#presentation_role
126
+ @wrapper_arguments[:role] = :presentation
127
+ else
128
+ @system_arguments[:tag] = :a
129
+ end
130
+
131
+ @wrapper_arguments[:tag] = :li
132
+ @wrapper_arguments[:display] ||= :inline_flex
133
+
134
+ return unless @selected
135
+
136
+ if @system_arguments[:tag] == :a
137
+ aria_current = aria("current", system_arguments) || DEFAULT_ARIA_CURRENT_FOR_ANCHOR
138
+ @system_arguments[:"aria-current"] = fetch_or_fallback(ARIA_CURRENT_OPTIONS_FOR_ANCHOR, aria_current, DEFAULT_ARIA_CURRENT_FOR_ANCHOR)
139
+ else
140
+ @system_arguments[:"aria-selected"] = true
141
+ end
142
+ end
143
+
144
+ def wrapper
145
+ unless @list
146
+ yield
147
+ return # returning `yield` caused a double render
148
+ end
149
+
150
+ render(Primer::BaseComponent.new(**@wrapper_arguments)) do
151
+ yield if block_given?
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def panel_id(panel_id)
158
+ if panel_id.blank?
159
+ raise ArgumentError, "`panel_id` is required" unless Rails.env.production?
160
+ else
161
+ @panel_id = panel_id
162
+ @system_arguments[:"aria-controls"] = @panel_id
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end