primer_view_components 0.1.0 → 0.1.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/app/assets/javascripts/primer_view_components.js +1 -1
- data/app/assets/javascripts/primer_view_components.js.map +1 -1
- data/app/assets/styles/primer_view_components.css +1 -1
- data/app/assets/styles/primer_view_components.css.map +1 -1
- data/app/components/primer/alpha/action_list/divider.rb +2 -2
- data/app/components/primer/alpha/action_list/heading.html.erb +1 -1
- data/app/components/primer/alpha/action_list/heading.rb +11 -5
- data/app/components/primer/alpha/action_list/item.rb +19 -15
- data/app/components/primer/alpha/action_list.html.erb +7 -8
- data/app/components/primer/alpha/action_list.rb +16 -11
- data/app/components/primer/alpha/nav_list/{section.rb → group.rb} +5 -5
- data/app/components/primer/alpha/nav_list/item.html.erb +1 -1
- data/app/components/primer/alpha/nav_list/item.rb +15 -1
- data/app/components/primer/alpha/nav_list.d.ts +1 -0
- data/app/components/primer/alpha/nav_list.html.erb +8 -8
- data/app/components/primer/alpha/nav_list.js +21 -0
- data/app/components/primer/alpha/nav_list.rb +30 -34
- data/app/components/primer/alpha/nav_list.ts +23 -0
- data/app/components/primer/alpha/navigation/tab.rb +168 -0
- data/app/components/primer/alpha/overlay/header.html.erb +2 -2
- data/app/components/primer/alpha/overlay.rb +29 -9
- data/app/components/primer/alpha/tab_nav.rb +10 -3
- data/app/components/primer/alpha/tab_panels.rb +2 -2
- data/app/components/primer/alpha/underline_nav.css +1 -1
- data/app/components/primer/alpha/underline_nav.css.map +1 -1
- data/app/components/primer/alpha/underline_nav.pcss +1 -0
- data/app/components/primer/alpha/underline_nav.rb +2 -2
- data/app/components/primer/alpha/underline_panels.rb +2 -2
- data/app/components/primer/beta/button.html.erb +1 -1
- data/app/components/primer/beta/button.rb +2 -1
- data/app/components/primer/component.rb +34 -0
- data/app/components/primer/navigation/tab_component.rb +3 -157
- data/app/components/primer/truncate.rb +1 -1
- data/lib/primer/deprecations.yml +9 -0
- data/lib/primer/forms/dsl/text_field_input.rb +1 -1
- data/lib/primer/forms/primer_text_field.js +17 -6
- data/lib/primer/forms/primer_text_field.ts +15 -7
- data/lib/primer/forms/text_field.html.erb +3 -3
- data/lib/primer/view_components/version.rb +1 -1
- data/lib/primer/yard/component_manifest.rb +2 -1
- data/lib/tasks/docs.rake +1 -1
- data/previews/primer/alpha/action_list_preview.rb +41 -29
- data/previews/primer/alpha/nav_list_preview/trailing_action.html.erb +19 -0
- data/previews/primer/alpha/nav_list_preview.rb +19 -30
- data/previews/primer/alpha/overlay_preview.rb +34 -4
- data/previews/primer/alpha/tab_nav_preview/with_extra.html.erb +8 -0
- data/previews/primer/alpha/tab_nav_preview.rb +5 -0
- data/previews/primer/alpha/tab_panels_preview/with_extra.html.erb +17 -0
- data/previews/primer/alpha/tab_panels_preview.rb +5 -0
- data/static/arguments.json +64 -8
- data/static/audited_at.json +2 -1
- data/static/constants.json +20 -8
- data/static/previews.json +20 -5
- data/static/statuses.json +4 -3
- metadata +10 -8
- data/app/components/primer/alpha/nav_list/section.html.erb +0 -3
- data/previews/primer/alpha/action_list_preview/heading.html.erb +0 -4
- /data/app/components/primer/{navigation/tab_component.html.erb → alpha/navigation/tab.html.erb} +0 -0
@@ -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
|
@@ -1,11 +1,11 @@
|
|
1
1
|
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
|
2
2
|
<div class="Overlay-headerContentWrap">
|
3
3
|
<div class="Overlay-titleWrap">
|
4
|
-
<h1 class="Overlay-title <% if @visually_hide_title || content.present? %>sr-only<% end %>"><%= @title %></h1>
|
4
|
+
<h1 id="<%= @id %>" class="Overlay-title <% if @visually_hide_title || content.present? %>sr-only<% end %>"><%= @title %></h1>
|
5
5
|
<% if content.present? %>
|
6
6
|
<%= content %>
|
7
7
|
<% elsif @subtitle.present? %>
|
8
|
-
<h2
|
8
|
+
<h2 class="Overlay-description"><%= @subtitle %></h2>
|
9
9
|
<% end %>
|
10
10
|
</div>
|
11
11
|
<div class="Overlay-actionWrap">
|
@@ -68,14 +68,18 @@ module Primer
|
|
68
68
|
# Optional button to open the Overlay.
|
69
69
|
#
|
70
70
|
# @param system_arguments [Hash] The same arguments as <%= link_to_component(Primer::ButtonComponent) %>.
|
71
|
-
renders_one :show_button, lambda {
|
71
|
+
renders_one :show_button, lambda { |icon: nil, **system_arguments|
|
72
72
|
system_arguments[:classes] = class_names(
|
73
73
|
system_arguments[:classes]
|
74
74
|
)
|
75
|
-
system_arguments[:id] =
|
76
|
-
system_arguments["popovertoggletarget"] =
|
77
|
-
system_arguments[:
|
78
|
-
|
75
|
+
system_arguments[:id] = show_button_id
|
76
|
+
system_arguments["popovertoggletarget"] = overlay_id
|
77
|
+
system_arguments[:aria] = (system_arguments[:aria] || {}).merge({ controls: overlay_id, haspopup: "true" })
|
78
|
+
if icon.present?
|
79
|
+
Primer::Beta::IconButton.new(icon: icon, **system_arguments)
|
80
|
+
else
|
81
|
+
Primer::Beta::Button.new(**system_arguments)
|
82
|
+
end
|
79
83
|
}
|
80
84
|
|
81
85
|
# Header content.
|
@@ -85,7 +89,7 @@ module Primer
|
|
85
89
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
86
90
|
renders_one :header, lambda { |divider: false, size: :medium, visually_hide_title: @visually_hide_title, **system_arguments|
|
87
91
|
Primer::Alpha::Overlay::Header.new(
|
88
|
-
id:
|
92
|
+
id: title_id,
|
89
93
|
title: @title,
|
90
94
|
subtitle: @subtitle,
|
91
95
|
size: size,
|
@@ -165,7 +169,6 @@ module Primer
|
|
165
169
|
@system_arguments[:classes] = class_names(
|
166
170
|
"Overlay",
|
167
171
|
SIZE_MAPPINGS[fetch_or_fallback(SIZE_OPTIONS, size, DEFAULT_SIZE)],
|
168
|
-
"Overlay--motion-scaleFade",
|
169
172
|
system_arguments[:classes]
|
170
173
|
)
|
171
174
|
@system_arguments[:tag] = "anchored-position"
|
@@ -182,13 +185,30 @@ module Primer
|
|
182
185
|
|
183
186
|
@system_arguments[:popover] = popover
|
184
187
|
@system_arguments[:aria] ||= {}
|
185
|
-
@system_arguments[:aria][:describedby] ||= "#{@id}-description"
|
186
188
|
end
|
187
189
|
|
188
190
|
def before_render
|
189
|
-
|
191
|
+
if header?
|
192
|
+
@system_arguments[:aria][:labelledby] ||= title_id
|
193
|
+
else
|
194
|
+
@system_arguments[:aria][:label] = @title
|
195
|
+
end
|
190
196
|
with_body unless body?
|
191
197
|
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def overlay_id
|
202
|
+
@system_arguments[:id]
|
203
|
+
end
|
204
|
+
|
205
|
+
def title_id
|
206
|
+
"overlay-title-#{overlay_id}"
|
207
|
+
end
|
208
|
+
|
209
|
+
def show_button_id
|
210
|
+
"overlay-show-#{overlay_id}"
|
211
|
+
end
|
192
212
|
end
|
193
213
|
end
|
194
214
|
end
|
@@ -9,7 +9,7 @@ module Primer
|
|
9
9
|
# - By default, `TabNav` renders links within a `<nav>` element. `<nav>` has an
|
10
10
|
# implicit landmark role of `navigation` which should be reserved for main links.
|
11
11
|
# For all other set of links, set tag to `:div`.
|
12
|
-
# - See <%= link_to_component(Primer::Navigation::
|
12
|
+
# - See <%= link_to_component(Primer::Alpha::Navigation::Tab) %> for additional
|
13
13
|
# accessibility considerations.
|
14
14
|
class TabNav < Primer::Component
|
15
15
|
include Primer::TabbedComponentHelper
|
@@ -22,13 +22,13 @@ module Primer
|
|
22
22
|
TAG_DEFAULT = :nav
|
23
23
|
TAG_OPTIONS = [TAG_DEFAULT, :div].freeze
|
24
24
|
|
25
|
-
# Tabs to be rendered. For more information, refer to <%= link_to_component(Primer::Navigation::
|
25
|
+
# Tabs to be rendered. For more information, refer to <%= link_to_component(Primer::Alpha::Navigation::Tab) %>.
|
26
26
|
#
|
27
27
|
# @param selected [Boolean] Whether the tab is selected.
|
28
28
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
29
29
|
renders_many :tabs, lambda { |selected: false, **system_arguments|
|
30
30
|
system_arguments[:classes] = tab_nav_tab_classes(system_arguments[:classes])
|
31
|
-
Primer::Navigation::
|
31
|
+
Primer::Alpha::Navigation::Tab.new(
|
32
32
|
list: true,
|
33
33
|
selected: selected,
|
34
34
|
**system_arguments
|
@@ -124,6 +124,13 @@ module Primer
|
|
124
124
|
|
125
125
|
aria_label_for_page_nav(label)
|
126
126
|
end
|
127
|
+
|
128
|
+
def before_render
|
129
|
+
# Eagerly evaluate content to avoid https://github.com/primer/view_components/issues/1790
|
130
|
+
content
|
131
|
+
|
132
|
+
super
|
133
|
+
end
|
127
134
|
end
|
128
135
|
end
|
129
136
|
end
|
@@ -14,7 +14,7 @@ module Primer
|
|
14
14
|
TAG_DEFAULT = :nav
|
15
15
|
TAG_OPTIONS = [TAG_DEFAULT, :div].freeze
|
16
16
|
|
17
|
-
# Tabs to be rendered. For more information, refer to <%= link_to_component(Primer::Navigation::
|
17
|
+
# Tabs to be rendered. For more information, refer to <%= link_to_component(Primer::Alpha::Navigation::Tab) %>.
|
18
18
|
#
|
19
19
|
# @param id [String] Unique ID of tab.
|
20
20
|
# @param selected [Boolean] Whether the tab is selected.
|
@@ -23,7 +23,7 @@ module Primer
|
|
23
23
|
system_arguments[:id] = id
|
24
24
|
system_arguments[:classes] = tab_nav_tab_classes(system_arguments[:classes])
|
25
25
|
|
26
|
-
Primer::Navigation::
|
26
|
+
Primer::Alpha::Navigation::Tab.new(
|
27
27
|
selected: selected,
|
28
28
|
with_panel: true,
|
29
29
|
list: true,
|
@@ -1 +1 @@
|
|
1
|
-
.UnderlineNav{-webkit-overflow-scrolling:auto;box-shadow:inset 0 -1px 0 var(--color-border-muted);display:flex;justify-content:space-between;min-height:var(--base-size-48,48px);overflow-x:auto;overflow-y:hidden}.UnderlineNav .Counter{background-color:var(--color-neutral-muted);color:var(--color-fg-default);margin-left:var(--primer-control-medium-gap,8px)}.UnderlineNav .Counter--primary{background-color:var(--color-neutral-emphasis);color:var(--color-fg-on-emphasis)}.UnderlineNav-body{align-items:center;display:flex;gap:var(--primer-control-medium-gap,8px);list-style:none}.UnderlineNav-item{align-items:center;background-color:initial;border:0;border-radius:var(--primer-borderRadius-medium,6px);color:var(--color-fg-default);cursor:pointer;display:flex;font-size:var(--primer-text-body-size-medium,14px);line-height:30px;padding:0 var(--primer-control-medium-paddingInline-condensed,8px);position:relative;text-align:center;white-space:nowrap}.UnderlineNav-item:focus,.UnderlineNav-item:focus-visible,.UnderlineNav-item:hover{border-bottom-color:var(--color-neutral-muted);color:var(--color-fg-default);outline-offset:-2px;text-decoration:none;transition:border-bottom-color .12s ease-out}.UnderlineNav-item [data-content]:before{content:attr(data-content);display:block;font-weight:var(--base-text-weight-semibold,600);height:0;visibility:hidden}.UnderlineNav-item:before{content:"";height:100%;left:50%;min-height:48px;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:100%}@media (pointer:fine){.UnderlineNav-item:hover{background:var(--color-action-list-item-default-hover-bg);color:var(--color-fg-default);text-decoration:none;transition:background .12s ease-out}}.UnderlineNav-item.selected,.UnderlineNav-item[aria-current]:not([aria-current=false]),.UnderlineNav-item[role=tab][aria-selected=true]{border-bottom-color:var(--color-primer-border-active);color:var(--color-fg-default);font-weight:var(--base-text-weight-semibold,600)}.UnderlineNav-item.selected:after,.UnderlineNav-item[aria-current]:not([aria-current=false]):after,.UnderlineNav-item[role=tab][aria-selected=true]:after{background:var(--color-primer-border-active);border-radius:var(--primer-borderRadius-medium,6px);bottom:calc(50% - 25px);content:"";height:2px;position:absolute;right:50%;transform:translate(50%,-50%);width:100
|
1
|
+
.UnderlineNav{-webkit-overflow-scrolling:auto;box-shadow:inset 0 -1px 0 var(--color-border-muted);display:flex;justify-content:space-between;min-height:var(--base-size-48,48px);overflow-x:auto;overflow-y:hidden}.UnderlineNav .Counter{background-color:var(--color-neutral-muted);color:var(--color-fg-default);margin-left:var(--primer-control-medium-gap,8px)}.UnderlineNav .Counter--primary{background-color:var(--color-neutral-emphasis);color:var(--color-fg-on-emphasis)}.UnderlineNav-body{align-items:center;display:flex;gap:var(--primer-control-medium-gap,8px);list-style:none}.UnderlineNav-item{align-items:center;background-color:initial;border:0;border-radius:var(--primer-borderRadius-medium,6px);color:var(--color-fg-default);cursor:pointer;display:flex;font-size:var(--primer-text-body-size-medium,14px);line-height:30px;padding:0 var(--primer-control-medium-paddingInline-condensed,8px);position:relative;text-align:center;white-space:nowrap}.UnderlineNav-item:focus,.UnderlineNav-item:focus-visible,.UnderlineNav-item:hover{border-bottom-color:var(--color-neutral-muted);color:var(--color-fg-default);outline-offset:-2px;text-decoration:none;transition:border-bottom-color .12s ease-out}.UnderlineNav-item [data-content]:before{content:attr(data-content);display:block;font-weight:var(--base-text-weight-semibold,600);height:0;visibility:hidden}.UnderlineNav-item:before{content:"";height:100%;left:50%;min-height:48px;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:100%}@media (pointer:fine){.UnderlineNav-item:hover{background:var(--color-action-list-item-default-hover-bg);color:var(--color-fg-default);text-decoration:none;transition:background .12s ease-out}}.UnderlineNav-item.selected,.UnderlineNav-item[aria-current]:not([aria-current=false]),.UnderlineNav-item[role=tab][aria-selected=true]{border-bottom-color:var(--color-primer-border-active);color:var(--color-fg-default);font-weight:var(--base-text-weight-semibold,600)}.UnderlineNav-item.selected:after,.UnderlineNav-item[aria-current]:not([aria-current=false]):after,.UnderlineNav-item[role=tab][aria-selected=true]:after{background:var(--color-primer-border-active);border-radius:var(--primer-borderRadius-medium,6px);bottom:calc(50% - 25px);content:"";height:2px;position:absolute;right:50%;transform:translate(50%,-50%);width:100%;z-index:1}.UnderlineNav--right{justify-content:flex-end}.UnderlineNav--right .UnderlineNav-actions{flex:1 1 auto}.UnderlineNav-actions{align-self:center}.UnderlineNav--full{display:block}.UnderlineNav--full .UnderlineNav-body{min-height:var(--base-size-48,48px)}.UnderlineNav-octicon{fill:var(--color-fg-muted);color:var(--color-fg-muted);display:inline!important;margin-right:var(--primer-control-medium-gap,8px)}.UnderlineNav-container{display:flex;justify-content:space-between}
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["underline_nav.pcss","<no source>"],"names":[],"mappings":"AAEA,cAME,+BAAgC,CADhC,mDAAoD,CAJpD,YAAa,CAMb,6BAA8B,CAL9B,mCAAqC,CACrC,eAAgB,CAChB,iBAeF,CAVE,uBAGE,2CAA4C,CAD5C,6BAA8B,CAD9B,gDAGF,CAEA,gCAEE,8CAA+C,CAD/C,iCAEF,CAGF,mBAEE,kBAAmB,CADnB,YAAa,CAEb,wCAA0C,CAC1C,eACF,CAEA,mBAaE,kBAAmB,CAHnB,wBAA6B,CAC7B,QAAS,CACT,mDAAqD,CANrD,6BAA8B,CAG9B,cAAe,CAPf,YAAa,CAEb,kDAAoD,CACpD,gBAAiB,CAFjB,kEAAoE,CAFpE,iBAAkB,CAMlB,iBAAkB,CAClB,
|
1
|
+
{"version":3,"sources":["underline_nav.pcss","<no source>"],"names":[],"mappings":"AAEA,cAME,+BAAgC,CADhC,mDAAoD,CAJpD,YAAa,CAMb,6BAA8B,CAL9B,mCAAqC,CACrC,eAAgB,CAChB,iBAeF,CAVE,uBAGE,2CAA4C,CAD5C,6BAA8B,CAD9B,gDAGF,CAEA,gCAEE,8CAA+C,CAD/C,iCAEF,CAGF,mBAEE,kBAAmB,CADnB,YAAa,CAEb,wCAA0C,CAC1C,eACF,CAEA,mBAaE,kBAAmB,CAHnB,wBAA6B,CAC7B,QAAS,CACT,mDAAqD,CANrD,6BAA8B,CAG9B,cAAe,CAPf,YAAa,CAEb,kDAAoD,CACpD,gBAAiB,CAFjB,kEAAoE,CAFpE,iBAAkB,CAMlB,iBAAkB,CAClB,kBA8DF,CAvDE,mFAKE,8CAA+C,CAF/C,6BAA8B,CAG9B,mBAAoB,CAFpB,oBAAqB,CAGrB,4CACF,CAGA,yCAKE,0BAA2B,CAJ3B,aAAc,CAEd,gDAAkD,CADlD,QAAS,CAET,iBAEF,CAIE,0BClEJ,WAAA,YAAA,SAAA,gBAAA,kBAAA,QAAA,4CAAA,UDkE8B,CAI5B,sBACE,yBAGE,yDAA0D,CAF1D,6BAA8B,CAC9B,oBAAqB,CAErB,mCACF,CACF,CAEA,wIAKE,qDAAsD,CADtD,6BAA8B,CAD9B,gDAiBF,CAZE,0JAQE,4CAA6C,CAC7C,mDAAqD,CALrD,uBAAwB,CAGxB,UAAW,CADX,UAAW,CALX,iBAAkB,CAElB,SAAU,CAOV,6BAA+B,CAL/B,UAAW,CAHX,SASF,CAIJ,qBACE,wBAKF,CAHE,2CACE,aACF,CAGF,sBACE,iBACF,CAEA,oBACE,aAMF,CAHE,uCACE,mCACF,CAGF,sBAIE,0BAA2B,CAD3B,2BAA4B,CAF5B,wBAA0B,CAC1B,iDAGF,CAEA,wBACE,YAAa,CACb,6BACF","file":"underline_nav.css","sourcesContent":["/* UnderlineNav */\n\n.UnderlineNav {\n display: flex;\n min-height: var(--base-size-48, 48px);\n overflow-x: auto;\n overflow-y: hidden;\n box-shadow: inset 0 -1px 0 var(--color-border-muted);\n -webkit-overflow-scrolling: auto;\n justify-content: space-between;\n\n & .Counter {\n margin-left: var(--primer-control-medium-gap, 8px);\n color: var(--color-fg-default);\n background-color: var(--color-neutral-muted);\n }\n\n & .Counter--primary {\n color: var(--color-fg-on-emphasis);\n background-color: var(--color-neutral-emphasis);\n }\n}\n\n.UnderlineNav-body {\n display: flex;\n align-items: center;\n gap: var(--primer-control-medium-gap, 8px);\n list-style: none;\n}\n\n.UnderlineNav-item {\n position: relative;\n display: flex;\n padding: 0 var(--primer-control-medium-paddingInline-condensed, 8px);\n font-size: var(--primer-text-body-size-medium, 14px);\n line-height: 30px;\n color: var(--color-fg-default);\n text-align: center;\n white-space: nowrap;\n cursor: pointer;\n background-color: transparent;\n border: 0;\n border-radius: var(--primer-borderRadius-medium, 6px);\n align-items: center;\n\n &:hover,\n &:focus,\n &:focus-visible {\n color: var(--color-fg-default);\n text-decoration: none;\n border-bottom-color: var(--color-neutral-muted);\n outline-offset: -2px;\n transition: border-bottom-color 0.12s ease-out;\n }\n\n /* renders a visibly hidden \"copy\" of the label in bold, reserving box space for when label becomes bold on selected */\n & [data-content]::before {\n display: block;\n height: 0;\n font-weight: var(--base-text-weight-semibold, 600);\n visibility: hidden;\n content: attr(data-content);\n }\n\n /* increase touch target area */\n &::before {\n @mixin minTouchTarget 48px;\n }\n\n /* hover state was \"sticking\" on mobile after click */\n @media (pointer: fine) {\n &:hover {\n color: var(--color-fg-default);\n text-decoration: none;\n background: var(--color-action-list-item-default-hover-bg);\n transition: background 0.12s ease-out;\n }\n }\n\n &.selected,\n &[role='tab'][aria-selected='true'],\n &[aria-current]:not([aria-current='false']) {\n font-weight: var(--base-text-weight-semibold, 600);\n color: var(--color-fg-default);\n border-bottom-color: var(--color-primer-border-active);\n\n /* current/selected underline */\n &::after {\n position: absolute;\n z-index: 1; /* raise above full-width flash banner */\n right: 50%;\n bottom: calc(50% - 25px); /* 48px total height / 2 (24px) + 1px */\n width: 100%;\n height: 2px;\n content: '';\n background: var(--color-primer-border-active);\n border-radius: var(--primer-borderRadius-medium, 6px);\n transform: translate(50%, -50%);\n }\n }\n}\n\n.UnderlineNav--right {\n justify-content: flex-end;\n\n & .UnderlineNav-actions {\n flex: 1 1 auto;\n }\n}\n\n.UnderlineNav-actions {\n align-self: center;\n}\n\n.UnderlineNav--full {\n display: block;\n\n /* required for underline to align with additional wrapper element */\n & .UnderlineNav-body {\n min-height: var(--base-size-48, 48px);\n }\n}\n\n.UnderlineNav-octicon {\n display: inline !important;\n margin-right: var(--primer-control-medium-gap, 8px);\n color: var(--color-fg-muted);\n fill: var(--color-fg-muted);\n}\n\n.UnderlineNav-container {\n display: flex;\n justify-content: space-between;\n}\n",null]}
|
@@ -12,7 +12,7 @@ module Primer
|
|
12
12
|
# - By default, `UnderlineNav` renders links within a `<nav>` element. `<nav>` has an
|
13
13
|
# implicit landmark role of `navigation` which should be reserved for main links.
|
14
14
|
# For all other set of links, set tag to `:div`.
|
15
|
-
# - See <%= link_to_component(Primer::Navigation::
|
15
|
+
# - See <%= link_to_component(Primer::Alpha::Navigation::Tab) %> for additional
|
16
16
|
# accessibility considerations.
|
17
17
|
class UnderlineNav < Primer::Component
|
18
18
|
include Primer::TabbedComponentHelper
|
@@ -29,7 +29,7 @@ module Primer
|
|
29
29
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
30
30
|
renders_many :tabs, lambda { |selected: false, **system_arguments|
|
31
31
|
system_arguments[:classes] = underline_nav_tab_classes(system_arguments[:classes])
|
32
|
-
Primer::Navigation::
|
32
|
+
Primer::Alpha::Navigation::Tab.new(
|
33
33
|
list: true,
|
34
34
|
selected: selected,
|
35
35
|
icon_classes: "UnderlineNav-octicon",
|
@@ -6,7 +6,7 @@ module Primer
|
|
6
6
|
class UnderlinePanels < Primer::Component
|
7
7
|
include Primer::TabbedComponentHelper
|
8
8
|
include Primer::UnderlineNavHelper
|
9
|
-
# Use to render a button and an associated panel slot. See the example below or refer to <%= link_to_component(Primer::Navigation::
|
9
|
+
# Use to render a button and an associated panel slot. See the example below or refer to <%= link_to_component(Primer::Alpha::Navigation::Tab) %>.
|
10
10
|
#
|
11
11
|
# @param id [String] Unique ID of tab.
|
12
12
|
# @param selected [Boolean] Whether the tab is selected.
|
@@ -15,7 +15,7 @@ module Primer
|
|
15
15
|
system_arguments[:id] = id
|
16
16
|
system_arguments[:classes] = underline_nav_tab_classes(system_arguments[:classes])
|
17
17
|
|
18
|
-
Primer::Navigation::
|
18
|
+
Primer::Alpha::Navigation::Tab.new(
|
19
19
|
selected: selected,
|
20
20
|
with_panel: true,
|
21
21
|
list: true,
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<%= render Primer::ConditionalWrapper.new(condition: tooltip.present?, tag: :div, classes: "Button-withTooltip") do -%>
|
1
|
+
<%= render Primer::ConditionalWrapper.new(condition: tooltip.present?, tag: :div, display: (:block if @block), classes: "Button-withTooltip") do -%>
|
2
2
|
<%= render Primer::Beta::BaseButton.new(**@system_arguments) do -%>
|
3
3
|
<span class="<%= @align_content_classes %>">
|
4
4
|
<% if leading_visual %>
|
@@ -144,6 +144,7 @@ module Primer
|
|
144
144
|
**system_arguments
|
145
145
|
)
|
146
146
|
@scheme = scheme
|
147
|
+
@block = block
|
147
148
|
|
148
149
|
@system_arguments = system_arguments
|
149
150
|
|
@@ -162,7 +163,7 @@ module Primer
|
|
162
163
|
SCHEME_MAPPINGS[fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)],
|
163
164
|
SIZE_MAPPINGS[fetch_or_fallback(SIZE_OPTIONS, size, DEFAULT_SIZE)],
|
164
165
|
"Button",
|
165
|
-
"Button--fullWidth" => block
|
166
|
+
"Button--fullWidth" => @block
|
166
167
|
)
|
167
168
|
end
|
168
169
|
|
@@ -58,6 +58,40 @@ module Primer
|
|
58
58
|
system_arguments[:"aria-#{val}"] || system_arguments.dig(:aria, val.to_sym)
|
59
59
|
end
|
60
60
|
|
61
|
+
# Merges hashes that contain "aria-*" keys and nested aria: hashes. Removes keys from
|
62
|
+
# each hash and returns them in the new hash.
|
63
|
+
#
|
64
|
+
# Eg. merge_aria({ "aria-disabled": "true" }, { aria: { invalid: "true" } })
|
65
|
+
# => { disabled: "true", invalid: "true" }
|
66
|
+
#
|
67
|
+
# It's designed to be used to normalize and merge aria information from system_arguments
|
68
|
+
# hashes. Consider using this pattern in component initializers:
|
69
|
+
#
|
70
|
+
# @system_arguments[:aria] = merge_aria(
|
71
|
+
# @system_arguments,
|
72
|
+
# { aria: { labelled_by: id } }
|
73
|
+
# )
|
74
|
+
def merge_aria(*hashes)
|
75
|
+
{}.tap do |result|
|
76
|
+
hashes.each do |hash|
|
77
|
+
next unless hash
|
78
|
+
|
79
|
+
result.merge!(hash.delete(:aria) || {})
|
80
|
+
|
81
|
+
hash.delete_if do |key, val|
|
82
|
+
key_s = key.to_s
|
83
|
+
|
84
|
+
if key.start_with?("aria-")
|
85
|
+
result[key_s.sub("aria-", "").to_sym] = val
|
86
|
+
true
|
87
|
+
else
|
88
|
+
false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
61
95
|
def validate_aria_label
|
62
96
|
aria_label = aria("label", @system_arguments)
|
63
97
|
aria_labelledby = aria("labelledby", @system_arguments)
|
@@ -2,163 +2,9 @@
|
|
2
2
|
|
3
3
|
module Primer
|
4
4
|
module Navigation
|
5
|
-
#
|
6
|
-
|
7
|
-
|
8
|
-
# @accessibility
|
9
|
-
# `TabComponent` renders the selected anchor tab with `aria-current="page"` by default.
|
10
|
-
# 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"
|
11
|
-
class TabComponent < Primer::Component
|
12
|
-
DEFAULT_ARIA_CURRENT_FOR_ANCHOR = :page
|
13
|
-
ARIA_CURRENT_OPTIONS_FOR_ANCHOR = [true, DEFAULT_ARIA_CURRENT_FOR_ANCHOR].freeze
|
14
|
-
# Panel controlled by the Tab. This will not render anything in the tab itself.
|
15
|
-
# It will provide a accessor for the Tab's parent to call and render the panel
|
16
|
-
# content in the appropriate place.
|
17
|
-
# Refer to `UnderlineNav` and `TabNav` implementations for examples.
|
18
|
-
#
|
19
|
-
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
20
|
-
renders_one :panel, lambda { |**system_arguments|
|
21
|
-
return unless @with_panel
|
22
|
-
|
23
|
-
deny_tag_argument(**system_arguments)
|
24
|
-
system_arguments[:id] = @panel_id
|
25
|
-
system_arguments[:tag] = :div
|
26
|
-
system_arguments[:role] ||= :tabpanel
|
27
|
-
system_arguments[:tabindex] = 0
|
28
|
-
system_arguments[:hidden] = true unless @selected
|
29
|
-
|
30
|
-
label_present = aria("label", system_arguments) || aria("labelledby", system_arguments)
|
31
|
-
unless label_present
|
32
|
-
if @id.present?
|
33
|
-
system_arguments[:"aria-labelledby"] = @id
|
34
|
-
elsif !Rails.env.production?
|
35
|
-
raise ArgumentError, "Panels must be labelled. Either set a unique `id` on the tab, or set an `aria-label` directly on the panel"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
Primer::BaseComponent.new(**system_arguments)
|
40
|
-
}
|
41
|
-
|
42
|
-
# Icon to be rendered in the Tab left.
|
43
|
-
#
|
44
|
-
# @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::Beta::Octicon) %>.
|
45
|
-
renders_one :icon, lambda { |icon = nil, **system_arguments|
|
46
|
-
system_arguments[:classes] = class_names(
|
47
|
-
@icon_classes,
|
48
|
-
system_arguments[:classes]
|
49
|
-
)
|
50
|
-
Primer::Beta::Octicon.new(icon, **system_arguments)
|
51
|
-
}
|
52
|
-
|
53
|
-
# The Tab's text.
|
54
|
-
#
|
55
|
-
# @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::Beta::Text) %>.
|
56
|
-
renders_one :text, Primer::Beta::Text
|
57
|
-
|
58
|
-
# Counter to be rendered in the Tab right.
|
59
|
-
#
|
60
|
-
# @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::Beta::Counter) %>.
|
61
|
-
renders_one :counter, Primer::Beta::Counter
|
62
|
-
|
63
|
-
attr_reader :selected
|
64
|
-
|
65
|
-
# @example Default
|
66
|
-
# <%= render(Primer::Navigation::TabComponent.new(selected: true)) do |component| %>
|
67
|
-
# <% component.with_text { "Selected" } %>
|
68
|
-
# <% end %>
|
69
|
-
# <%= render(Primer::Navigation::TabComponent.new) do |component| %>
|
70
|
-
# <% component.with_text { "Not selected" } %>
|
71
|
-
# <% end %>
|
72
|
-
#
|
73
|
-
# @example With icons and counters
|
74
|
-
# <%= render(Primer::Navigation::TabComponent.new) do |component| %>
|
75
|
-
# <% component.with_icon(:star) %>
|
76
|
-
# <% component.with_text { "Tab" } %>
|
77
|
-
# <% end %>
|
78
|
-
# <%= render(Primer::Navigation::TabComponent.new) do |component| %>
|
79
|
-
# <% component.with_icon(:star) %>
|
80
|
-
# <% component.with_text { "Tab" } %>
|
81
|
-
# <% component.with_counter(count: 10) %>
|
82
|
-
# <% end %>
|
83
|
-
# <%= render(Primer::Navigation::TabComponent.new) do |component| %>
|
84
|
-
# <% component.with_text { "Tab" } %>
|
85
|
-
# <% component.with_counter(count: 10) %>
|
86
|
-
# <% end %>
|
87
|
-
#
|
88
|
-
# @example Inside a list
|
89
|
-
# <%= render(Primer::Navigation::TabComponent.new(list: true)) do |component| %>
|
90
|
-
# <% component.with_text { "Tab" } %>
|
91
|
-
# <% end %>
|
92
|
-
#
|
93
|
-
# @example With custom HTML
|
94
|
-
# <%= render(Primer::Navigation::TabComponent.new) do %>
|
95
|
-
# <div>
|
96
|
-
# This is my <strong>custom HTML</strong>
|
97
|
-
# </div>
|
98
|
-
# <% end %>
|
99
|
-
#
|
100
|
-
# @param list [Boolean] Whether the Tab is an item in a `<ul>` list.
|
101
|
-
# @param selected [Boolean] Whether the Tab is selected or not.
|
102
|
-
# @param with_panel [Boolean] Whether the Tab has an associated panel.
|
103
|
-
# @param panel_id [String] Only applies if `with_panel` is `true`. Unique id of panel.
|
104
|
-
# @param icon_classes [Boolean] Classes that must always be applied to icons.
|
105
|
-
# @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.
|
106
|
-
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
107
|
-
def initialize(list: false, selected: false, with_panel: false, panel_id: "", icon_classes: "", wrapper_arguments: {}, **system_arguments)
|
108
|
-
@selected = selected
|
109
|
-
@icon_classes = icon_classes
|
110
|
-
@list = list
|
111
|
-
@with_panel = with_panel
|
112
|
-
|
113
|
-
@system_arguments = system_arguments
|
114
|
-
@id = @system_arguments[:id]
|
115
|
-
@wrapper_arguments = wrapper_arguments
|
116
|
-
|
117
|
-
if with_panel || @system_arguments[:tag] == :button
|
118
|
-
@system_arguments[:tag] = :button
|
119
|
-
@system_arguments[:type] = :button
|
120
|
-
@system_arguments[:role] = :tab
|
121
|
-
panel_id(panel_id)
|
122
|
-
# https://www.w3.org/TR/wai-aria-practices/#presentation_role
|
123
|
-
@wrapper_arguments[:role] = :presentation
|
124
|
-
else
|
125
|
-
@system_arguments[:tag] = :a
|
126
|
-
end
|
127
|
-
|
128
|
-
@wrapper_arguments[:tag] = :li
|
129
|
-
@wrapper_arguments[:display] ||= :inline_flex
|
130
|
-
|
131
|
-
return unless @selected
|
132
|
-
|
133
|
-
if @system_arguments[:tag] == :a
|
134
|
-
aria_current = aria("current", system_arguments) || DEFAULT_ARIA_CURRENT_FOR_ANCHOR
|
135
|
-
@system_arguments[:"aria-current"] = fetch_or_fallback(ARIA_CURRENT_OPTIONS_FOR_ANCHOR, aria_current, DEFAULT_ARIA_CURRENT_FOR_ANCHOR)
|
136
|
-
else
|
137
|
-
@system_arguments[:"aria-selected"] = true
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def wrapper
|
142
|
-
unless @list
|
143
|
-
yield
|
144
|
-
return # returning `yield` caused a double render
|
145
|
-
end
|
146
|
-
|
147
|
-
render(Primer::BaseComponent.new(**@wrapper_arguments)) do
|
148
|
-
yield if block_given?
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
private
|
153
|
-
|
154
|
-
def panel_id(panel_id)
|
155
|
-
if panel_id.blank?
|
156
|
-
raise ArgumentError, "`panel_id` is required" unless Rails.env.production?
|
157
|
-
else
|
158
|
-
@panel_id = panel_id
|
159
|
-
@system_arguments[:"aria-controls"] = @panel_id
|
160
|
-
end
|
161
|
-
end
|
5
|
+
# nodoc
|
6
|
+
class TabComponent < Primer::Alpha::Navigation::Tab
|
7
|
+
status :deprecated
|
162
8
|
end
|
163
9
|
end
|
164
10
|
end
|
data/lib/primer/deprecations.yml
CHANGED
@@ -26,6 +26,15 @@ deprecations:
|
|
26
26
|
autocorrect: true
|
27
27
|
replacement: "Primer::Beta::IconButton"
|
28
28
|
|
29
|
+
- component: "Primer::Navigation::TabComponent"
|
30
|
+
autocorrect: true
|
31
|
+
replacement: "Primer::Alpha::Navigation::Tab"
|
32
|
+
|
29
33
|
- component: "Primer::Tooltip"
|
30
34
|
autocorrect: true
|
31
35
|
replacement: "Primer::Alpha::Tooltip"
|
36
|
+
|
37
|
+
- component: "Primer::Truncate"
|
38
|
+
autocorrect: false
|
39
|
+
replacement: "Primer::Beta::Truncate"
|
40
|
+
guide: "https://primer.style/view-components/guides/primer_truncate"
|