primer_view_components 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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