primer_view_components 0.24.0 → 0.25.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -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/banner.rb +13 -13
- data/app/components/primer/alpha/tab_nav.css +1 -1
- data/app/components/primer/alpha/tab_nav.css.json +0 -2
- data/app/components/primer/alpha/tab_nav.css.map +1 -1
- data/app/components/primer/alpha/tab_nav.pcss +0 -10
- data/app/components/primer/alpha/tab_panels.html.erb +9 -5
- data/app/components/primer/alpha/tab_panels.rb +13 -4
- data/app/components/primer/alpha/toggle_switch.js +1 -1
- data/app/components/primer/alpha/toggle_switch.rb +16 -4
- data/app/components/primer/alpha/toggle_switch.ts +1 -1
- data/app/components/primer/alpha/underline_nav.css +1 -1
- data/app/components/primer/alpha/underline_nav.css.json +0 -2
- data/app/components/primer/alpha/underline_nav.css.map +1 -1
- data/app/components/primer/alpha/underline_nav.pcss +1 -7
- data/app/components/primer/alpha/underline_panels.html.erb +8 -6
- data/app/components/primer/alpha/underline_panels.rb +14 -6
- data/app/components/primer/beta/clipboard_copy.html.erb +9 -6
- data/app/components/primer/beta/clipboard_copy.js +15 -0
- data/app/components/primer/beta/clipboard_copy.rb +2 -0
- data/app/components/primer/beta/clipboard_copy.ts +14 -0
- data/app/components/primer/beta/clipboard_copy_button.rb +2 -0
- data/app/components/primer/primer.pcss +0 -1
- data/app/lib/primer/attributes_helper.rb +1 -1
- data/lib/primer/forms/toggle_switch.html.erb +1 -9
- data/lib/primer/view_components/version.rb +1 -1
- data/previews/primer/alpha/toggle_switch_preview.rb +1 -1
- data/previews/primer/beta/label_preview.rb +7 -2
- data/previews/primer/forms_preview/example_toggle_switch_form.html.erb +2 -2
- data/static/arguments.json +12 -0
- data/static/info_arch.json +15 -3
- metadata +2 -6
- data/app/components/primer/alpha/underline_panels.css +0 -1
- data/app/components/primer/alpha/underline_panels.css.json +0 -6
- data/app/components/primer/alpha/underline_panels.css.map +0 -1
- data/app/components/primer/alpha/underline_panels.pcss +0 -4
@@ -5,25 +5,25 @@ module Primer
|
|
5
5
|
# Use `Banner` to highlight important information.
|
6
6
|
#
|
7
7
|
# @accessibility
|
8
|
-
#
|
8
|
+
# ### Improve discoverability with a heading and landmark
|
9
|
+
# Banners are made visually prominent with icons and colors to immediately draw attention.
|
9
10
|
#
|
10
|
-
#
|
11
|
+
# To ensure the Banner is also easily discoverable for assistive technology users, consider:
|
12
|
+
# 1. Providing a heading inside of the Banner that describes the purpose of the Banner.
|
13
|
+
# 2. Designating the Banner as a `region` landmark. This can be achieved by rendering the Banner as a `section` with an `aria-labelledby` pointing to the heading.
|
11
14
|
#
|
12
|
-
#
|
13
|
-
#
|
15
|
+
# ### Communicating feedback
|
16
|
+
# When a Banner is used to communicate feedback, a live region or focus management technique should be in place to ensure that assistive technology users are aware of the feedback, especially when the Banner is shown client-side. Visit the [Banner's Accessibility section](https://primer.style/components/banner#accessibility), or defer to the accessibility team.
|
14
17
|
#
|
15
|
-
#
|
18
|
+
# #### Approach 1: Announcing a Banner
|
19
|
+
# A live region announcement can be used to communicate non-critical feedback, such as a success message.
|
16
20
|
#
|
17
|
-
#
|
21
|
+
# To successfully trigger a live region announcement for a Banner that is un-`hidden` or dynamically injected onto the page, **do not** set the live region attribute/role on the Banner itself. Instead, it's important to rely on a live region that is already guaranteed to be on the page. Learn more about specific techniques at: [Staff only: Challenges with live regions](https://github.com/github/accessibility/blob/main/docs/coaching-recommendations/toast-flash-banner/accessible-banner-prototype.md#challenges-with-dynamically-inserted-live-region).
|
18
22
|
#
|
19
|
-
#
|
23
|
+
# #### Approach 2: Focusing a Banner
|
24
|
+
# Focusing the Banner can be appropriate for critical feedback scenarios, such as a [form validation error summary](https://primer.style/ui-patterns/forms/overview#interactive-summary-of-errors) where the Banner contains actions to help unblock the user.
|
20
25
|
#
|
21
|
-
#
|
22
|
-
# Focusing a Banner when it appears helps to maximize discoverability of the message, especially in critical scenarios.
|
23
|
-
#
|
24
|
-
# To properly focus a banner, add a `tabindex="-1"` to the Banner container, and focus that container (one way is using the [`focus()` API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus)).
|
25
|
-
#
|
26
|
-
# For more information about the focus management technique, visit the [Staff only: Accessible Banner Prototype docs](https://github.com/github/accessibility/blob/main/docs/coaching-recommendations/toast-flash-banner/accessible-banner-prototype.md#consideration). This guidance is subject to change.
|
26
|
+
# To properly focus a Banner, add a `tabindex="-1"` place focus with JavaScript (using the [`focus()` API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus)).
|
27
27
|
class Banner < Primer::Component
|
28
28
|
status :alpha
|
29
29
|
|
@@ -1 +1 @@
|
|
1
|
-
.tabnav{border-bottom:var(--borderWidth-thin) solid var(--borderColor-default);margin-bottom:var(--stack-gap-normal);margin-top:0}.tabnav-tabs{display:flex;margin-bottom:calc(var(--borderWidth-thin)*-1);overflow:auto}.tabnav
|
1
|
+
.tabnav{border-bottom:var(--borderWidth-thin) solid var(--borderColor-default);margin-bottom:var(--stack-gap-normal);margin-top:0}.tabnav-tabs{display:flex;margin-bottom:calc(var(--borderWidth-thin)*-1);overflow:auto}.tabnav-tab{background-color:initial;border:var(--borderWidth-thin) solid #0000;border-bottom:0;color:var(--fgColor-muted);display:inline-block;flex-shrink:0;font-size:var(--text-body-size-medium);line-height:23px;padding:var(--base-size-8) var(--control-medium-paddingInline-spacious);-webkit-text-decoration:none;text-decoration:none;transition:color .2s cubic-bezier(.3,0,.5,1)}.tabnav-tab.selected,.tabnav-tab[aria-current]:not([aria-current=false]),.tabnav-tab[aria-selected=true]{background-color:var(--bgColor-default);border-color:var(--borderColor-default);border-radius:var(--borderRadius-medium) var(--borderRadius-medium) 0 0;color:var(--fgColor-default)}.tabnav-tab.selected .octicon,.tabnav-tab[aria-current]:not([aria-current=false]) .octicon,.tabnav-tab[aria-selected=true] .octicon{color:inherit}.tabnav-tab:hover{color:var(--fgColor-default);-webkit-text-decoration:none;text-decoration:none;transition-duration:.1s}.tabnav-tab:focus,.tabnav-tab:focus-visible{border-radius:var(--borderRadius-medium) var(--borderRadius-medium) 0 0!important;outline-offset:-6px}.tabnav-tab .octicon,.tabnav-tab:active{color:var(--fgColor-muted)}.tabnav-tab .octicon{margin-right:var(--control-small-gap)}.tabnav-tab .Counter{color:inherit;margin-left:var(--control-small-gap)}.tabnav-extra{color:var(--fgColor-muted);display:inline-block;font-size:var(--text-body-size-small);margin-left:10px;padding-top:10px}.tabnav-extra>.octicon{margin-right:2px}a.tabnav-extra:hover{color:var(--fgColor-accent);-webkit-text-decoration:none;text-decoration:none}.tabnav-btn{margin-left:var(--controlStack-medium-gap-condensed)}
|
@@ -3,7 +3,6 @@
|
|
3
3
|
"selectors": [
|
4
4
|
".tabnav",
|
5
5
|
".tabnav-tabs",
|
6
|
-
".tabnav::part(tablist-wrapper)",
|
7
6
|
".tabnav-tab",
|
8
7
|
".tabnav-tab.selected",
|
9
8
|
".tabnav-tab[aria-current]:not([aria-current=false])",
|
@@ -17,7 +16,6 @@
|
|
17
16
|
".tabnav-tab .octicon",
|
18
17
|
".tabnav-tab:active",
|
19
18
|
".tabnav-tab .Counter",
|
20
|
-
"tab-container .tabnav-tab",
|
21
19
|
".tabnav-extra",
|
22
20
|
".tabnav-extra>.octicon",
|
23
21
|
"a.tabnav-extra:hover",
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["tab_nav.pcss"],"names":[],"mappings":"AAGA,QAGE,sEAAuE,CADvE,qCAAsC,CADtC,YAGF,CAEA,aACE,YAAa,CACb,8CAAiD,CACjD,aACF,CAEA
|
1
|
+
{"version":3,"sources":["tab_nav.pcss"],"names":[],"mappings":"AAGA,QAGE,sEAAuE,CADvE,qCAAsC,CADtC,YAGF,CAEA,aACE,YAAa,CACb,8CAAiD,CACjD,aACF,CAEA,YAQE,wBAA6B,CAE7B,0CAAgB,CAAhB,eAAgB,CAJhB,0BAA2B,CAL3B,oBAAqB,CACrB,aAAc,CAEd,sCAAuC,CACvC,gBAAiB,CAFjB,uEAAwE,CAIxE,4BAAqB,CAArB,oBAAqB,CAIrB,4CAwCF,CAtCE,yGAIE,uCAAwC,CACxC,uCAAwC,CACxC,uEAAwE,CAHxE,4BAQF,CAHE,oIACE,aACF,CAGF,kBACE,4BAA6B,CAC7B,4BAAqB,CAArB,oBAAqB,CACrB,uBACF,CAEA,4CAEE,iFAAmF,CACnF,mBACF,CAMA,wCAHE,0BAMF,CAHA,qBACE,qCAEF,CAEA,qBAEE,aAAc,CADd,oCAEF,CAQF,cAKE,0BAA2B,CAJ3B,oBAAqB,CAGrB,qCAAsC,CADtC,gBAAiB,CADjB,gBAQF,CAHE,uBACE,gBACF,CAKF,qBACE,2BAA4B,CAC5B,4BAAqB,CAArB,oBACF,CAOA,YACE,oDACF","file":"tab_nav.css","sourcesContent":["/* tabnav */\n\n/* Outer wrapper */\n.tabnav {\n margin-top: 0;\n margin-bottom: var(--stack-gap-normal);\n border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);\n}\n\n.tabnav-tabs {\n display: flex;\n margin-bottom: calc(var(--borderWidth-thin) * -1);\n overflow: auto;\n}\n\n.tabnav-tab {\n display: inline-block;\n flex-shrink: 0;\n padding: var(--base-size-8) var(--control-medium-paddingInline-spacious);\n font-size: var(--text-body-size-medium);\n line-height: 23px;\n color: var(--fgColor-muted);\n text-decoration: none;\n background-color: transparent;\n border: var(--borderWidth-thin) solid transparent;\n border-bottom: 0;\n transition: color 0.2s cubic-bezier(0.3, 0, 0.5, 1);\n\n &.selected,\n &[aria-selected='true'],\n &[aria-current]:not([aria-current='false']) {\n color: var(--fgColor-default);\n background-color: var(--bgColor-default); /* cover bottom border */\n border-color: var(--borderColor-default);\n border-radius: var(--borderRadius-medium) var(--borderRadius-medium) 0 0;\n\n & .octicon {\n color: inherit;\n }\n }\n\n &:hover {\n color: var(--fgColor-default);\n text-decoration: none;\n transition-duration: 0.1s;\n }\n\n &:focus,\n &:focus-visible {\n border-radius: var(--borderRadius-medium) var(--borderRadius-medium) 0 0 !important;\n outline-offset: -6px;\n }\n\n &:active {\n color: var(--fgColor-muted);\n }\n\n & .octicon {\n margin-right: var(--control-small-gap);\n color: var(--fgColor-muted);\n }\n\n & .Counter {\n margin-left: var(--control-small-gap);\n color: inherit;\n }\n}\n\n/* Tabnav extras\n**\n** Tabnav extras are non-tab elements that sit in the tabnav. Usually they're\n** inline text or links. */\n\n.tabnav-extra {\n display: inline-block;\n padding-top: 10px;\n margin-left: 10px;\n font-size: var(--text-body-size-small);\n color: var(--fgColor-muted);\n\n & > .octicon {\n margin-right: 2px;\n }\n}\n\n/* When tabnav-extra are anchors\n** stylelint-disable-next-line selector-no-qualifying-type */\na.tabnav-extra:hover {\n color: var(--fgColor-accent);\n text-decoration: none;\n}\n\n/* Tabnav buttons\n**\n** For when there are multiple buttons, space them out appropriately. Requires\n** the buttons to be floated or inline-block. */\n\n.tabnav-btn {\n margin-left: var(--controlStack-medium-gap-condensed);\n}\n"]}
|
@@ -13,11 +13,6 @@
|
|
13
13
|
overflow: auto;
|
14
14
|
}
|
15
15
|
|
16
|
-
.tabnav::part(tablist-wrapper) {
|
17
|
-
margin-bottom: var(--stack-gap-normal);
|
18
|
-
border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);
|
19
|
-
}
|
20
|
-
|
21
16
|
.tabnav-tab {
|
22
17
|
display: inline-block;
|
23
18
|
flex-shrink: 0;
|
@@ -71,11 +66,6 @@
|
|
71
66
|
}
|
72
67
|
}
|
73
68
|
|
74
|
-
tab-container .tabnav-tab {
|
75
|
-
margin-bottom: -1px;
|
76
|
-
}
|
77
|
-
|
78
|
-
|
79
69
|
/* Tabnav extras
|
80
70
|
**
|
81
71
|
** Tabnav extras are non-tab elements that sit in the tabnav. Usually they're
|
@@ -1,9 +1,13 @@
|
|
1
|
-
<%=
|
2
|
-
<%=
|
3
|
-
|
4
|
-
<%=
|
1
|
+
<%= tab_container_wrapper(with_panel: true, **@wrapper_arguments) do %>
|
2
|
+
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
|
3
|
+
<%= extra if @align == :left %>
|
4
|
+
<%= render Primer::BaseComponent.new(**@body_arguments) do %>
|
5
|
+
<% tabs.each do |tab| %>
|
6
|
+
<%= tab %>
|
7
|
+
<% end %>
|
8
|
+
<% end %>
|
9
|
+
<%= extra if @align == :right %>
|
5
10
|
<% end %>
|
6
|
-
<%= extra if @align == :right %>
|
7
11
|
<% tabs.each do |tab| %>
|
8
12
|
<%= tab.panel %>
|
9
13
|
<% end %>
|
@@ -26,7 +26,7 @@ module Primer
|
|
26
26
|
Primer::Alpha::Navigation::Tab.new(
|
27
27
|
selected: selected,
|
28
28
|
with_panel: true,
|
29
|
-
list:
|
29
|
+
list: true,
|
30
30
|
panel_id: "panel-#{id}",
|
31
31
|
**system_arguments
|
32
32
|
)
|
@@ -43,14 +43,23 @@ module Primer
|
|
43
43
|
|
44
44
|
# @param label [String] Sets an `aria-label` that helps assistive technology users understand the purpose of the tabs.
|
45
45
|
# @param align [Symbol] <%= one_of(Primer::TabNavHelper::EXTRA_ALIGN_OPTIONS) %> - Defaults to <%= Primer::TabNavHelper::EXTRA_ALIGN_DEFAULT %>
|
46
|
+
# @param body_arguments [Hash] <%= link_to_system_arguments_docs %> for the body wrapper.
|
47
|
+
# @param wrapper_arguments [Hash] <%= link_to_system_arguments_docs %> for the `TabContainer` wrapper.
|
46
48
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
47
49
|
def initialize(label:, body_arguments: {}, wrapper_arguments: {}, **system_arguments)
|
48
50
|
@align = EXTRA_ALIGN_DEFAULT
|
51
|
+
@wrapper_arguments = wrapper_arguments
|
49
52
|
|
50
|
-
@system_arguments =
|
51
|
-
@system_arguments[:tag] = :
|
53
|
+
@system_arguments = deny_tag_argument(**system_arguments)
|
54
|
+
@system_arguments[:tag] = :div
|
52
55
|
@system_arguments[:classes] = tab_nav_classes(@system_arguments[:classes])
|
53
|
-
|
56
|
+
|
57
|
+
@body_arguments = deny_tag_argument(**body_arguments)
|
58
|
+
@body_arguments[:tag] = :ul
|
59
|
+
@body_arguments[:classes] = tab_nav_body_classes(@body_arguments[:classes])
|
60
|
+
|
61
|
+
@body_arguments[:role] = :tablist
|
62
|
+
@body_arguments[:"aria-label"] = label
|
54
63
|
end
|
55
64
|
|
56
65
|
def before_render
|
@@ -20,7 +20,7 @@ let ToggleSwitchElement = class ToggleSwitchElement extends HTMLElement {
|
|
20
20
|
}
|
21
21
|
get csrf() {
|
22
22
|
const csrfElement = this.querySelector('[data-csrf]');
|
23
|
-
return this.getAttribute('csrf') || (csrfElement instanceof HTMLInputElement && csrfElement.value) || null;
|
23
|
+
return this.getAttribute('data-csrf') || (csrfElement instanceof HTMLInputElement && csrfElement.value) || null;
|
24
24
|
}
|
25
25
|
get csrfField() {
|
26
26
|
// the authenticity token is passed into the element and is not generated in js land
|
@@ -56,10 +56,6 @@ module Primer
|
|
56
56
|
}
|
57
57
|
|
58
58
|
@system_arguments[:src] = @src if @src
|
59
|
-
|
60
|
-
return unless @src && @csrf_token
|
61
|
-
|
62
|
-
@system_arguments[:csrf] = @csrf_token
|
63
59
|
end
|
64
60
|
|
65
61
|
def on?
|
@@ -73,6 +69,22 @@ module Primer
|
|
73
69
|
def disabled?
|
74
70
|
!enabled?
|
75
71
|
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def before_render
|
76
|
+
@csrf_token ||= view_context.form_authenticity_token(
|
77
|
+
form_options: {
|
78
|
+
method: :post,
|
79
|
+
action: @src
|
80
|
+
}
|
81
|
+
)
|
82
|
+
|
83
|
+
@system_arguments[:data] = merge_data(
|
84
|
+
@system_arguments,
|
85
|
+
{ data: { csrf: @csrf_token } }
|
86
|
+
)
|
87
|
+
end
|
76
88
|
end
|
77
89
|
end
|
78
90
|
end
|
@@ -19,7 +19,7 @@ class ToggleSwitchElement extends HTMLElement {
|
|
19
19
|
|
20
20
|
get csrf(): string | null {
|
21
21
|
const csrfElement = this.querySelector('[data-csrf]')
|
22
|
-
return this.getAttribute('csrf') || (csrfElement instanceof HTMLInputElement && csrfElement.value) || null
|
22
|
+
return this.getAttribute('data-csrf') || (csrfElement instanceof HTMLInputElement && csrfElement.value) || null
|
23
23
|
}
|
24
24
|
|
25
25
|
get csrfField(): string {
|
@@ -1 +1 @@
|
|
1
|
-
.UnderlineNav{box-shadow:inset 0 -1px 0 var(--borderColor-muted);display:flex;min-height:var(--base-size-48);overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:auto;justify-content:space-between}.UnderlineNav .Counter{background-color:var(--bgColor-neutral-muted,var(--color-neutral-muted));color:var(--fgColor-default);margin-left:var(--control-medium-gap)}.UnderlineNav .Counter--primary{background-color:var(--bgColor-neutral-emphasis);color:var(--fgColor-onEmphasis)}.UnderlineNav
|
1
|
+
.UnderlineNav{box-shadow:inset 0 -1px 0 var(--borderColor-muted);display:flex;min-height:var(--base-size-48);overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:auto;justify-content:space-between}.UnderlineNav .Counter{background-color:var(--bgColor-neutral-muted,var(--color-neutral-muted));color:var(--fgColor-default);margin-left:var(--control-medium-gap)}.UnderlineNav .Counter--primary{background-color:var(--bgColor-neutral-emphasis);color:var(--fgColor-onEmphasis)}.UnderlineNav-body{align-items:center;display:flex;gap:var(--control-medium-gap);list-style:none}.UnderlineNav-item{align-items:center;background-color:initial;border:0;border-radius:var(--borderRadius-medium);color:var(--fgColor-default);cursor:pointer;display:flex;font-size:var(--text-body-size-medium);line-height:30px;padding:0 var(--control-medium-paddingInline-condensed);position:relative;text-align:center;white-space:nowrap}.UnderlineNav-item:focus,.UnderlineNav-item:focus-visible,.UnderlineNav-item:hover{border-bottom-color:var(--borderColor-neutral-muted);color:var(--fgColor-default);outline-offset:-2px;-webkit-text-decoration:none;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);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(--control-transparent-bgColor-hover);color:var(--fgColor-default);-webkit-text-decoration:none;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(--underlineNav-borderColor-active);color:var(--fgColor-default);font-weight:var(--base-text-weight-semibold)}.UnderlineNav-item.selected:after,.UnderlineNav-item[aria-current]:not([aria-current=false]):after,.UnderlineNav-item[role=tab][aria-selected=true]:after{background:var(--underlineNav-borderColor-active);border-radius:var(--borderRadius-medium);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)}.UnderlineNav-octicon{color:var(--fgColor-muted);display:inline!important;margin-right:var(--control-medium-gap);fill:var(--fgColor-muted)}.UnderlineNav-container{display:flex;justify-content:space-between}
|
@@ -4,9 +4,7 @@
|
|
4
4
|
".UnderlineNav",
|
5
5
|
".UnderlineNav .Counter",
|
6
6
|
".UnderlineNav .Counter--primary",
|
7
|
-
".UnderlineNav::part(tablist-wrapper)",
|
8
7
|
".UnderlineNav-body",
|
9
|
-
".UnderlineNav::part(tablist)",
|
10
8
|
".UnderlineNav-item",
|
11
9
|
".UnderlineNav-item:focus",
|
12
10
|
".UnderlineNav-item:focus-visible",
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["underline_nav.pcss","<no source>"],"names":[],"mappings":"AAEA,cAKE,kDAAmD,CAJnD,YAAa,CACb,8BAA+B,CAC/B,eAAgB,CAChB,iBAAkB,CAElB,+BAAgC,CAChC,6BAYF,CAVE,uBAGE,wEAA0E,CAD1E,4BAA6B,CAD7B,qCAGF,CAEA,gCAEE,gDAAiD,CADjD,+BAEF,CAGF,
|
1
|
+
{"version":3,"sources":["underline_nav.pcss","<no source>"],"names":[],"mappings":"AAEA,cAKE,kDAAmD,CAJnD,YAAa,CACb,8BAA+B,CAC/B,eAAgB,CAChB,iBAAkB,CAElB,+BAAgC,CAChC,6BAYF,CAVE,uBAGE,wEAA0E,CAD1E,4BAA6B,CAD7B,qCAGF,CAEA,gCAEE,gDAAiD,CADjD,+BAEF,CAGF,mBAEE,kBAAmB,CADnB,YAAa,CAEb,6BAA8B,CAC9B,eACF,CAEA,mBAaE,kBAAmB,CAHnB,wBAA6B,CAC7B,QAAS,CACT,wCAAyC,CANzC,4BAA6B,CAG7B,cAAe,CAPf,YAAa,CAEb,sCAAuC,CACvC,gBAAiB,CAFjB,uDAAwD,CAFxD,iBAAkB,CAMlB,iBAAkB,CAClB,kBA8DF,CAvDE,mFAKE,oDAAqD,CAFrD,4BAA6B,CAG7B,mBAAoB,CAFpB,4BAAqB,CAArB,oBAAqB,CAGrB,4CACF,CAGA,yCAKE,0BAA2B,CAJ3B,aAAc,CAEd,4CAA6C,CAD7C,QAAS,CAET,iBAEF,CAIE,0BClEJ,WAAA,YAAA,SAAA,gBAAA,kBAAA,QAAA,4CAAA,UDkE8B,CAI5B,sBACE,yBAGE,mDAAoD,CAFpD,4BAA6B,CAC7B,4BAAqB,CAArB,oBAAqB,CAErB,mCACF,CACF,CAEA,wIAKE,0DAA2D,CAD3D,4BAA6B,CAD7B,4CAiBF,CAZE,0JAQE,iDAAkD,CAClD,wCAAyC,CALzC,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,8BACF,CAGF,sBAGE,0BAA2B,CAF3B,wBAA0B,CAC1B,sCAAuC,CAEvC,yBACF,CAEA,wBACE,YAAa,CACb,6BACF","file":"underline_nav.css","sourcesContent":["/* UnderlineNav */\n\n.UnderlineNav {\n display: flex;\n min-height: var(--base-size-48);\n overflow-x: auto;\n overflow-y: hidden;\n box-shadow: inset 0 -1px 0 var(--borderColor-muted);\n -webkit-overflow-scrolling: auto;\n justify-content: space-between;\n\n & .Counter {\n margin-left: var(--control-medium-gap);\n color: var(--fgColor-default);\n background-color: var(--bgColor-neutral-muted, var(--color-neutral-muted));\n }\n\n & .Counter--primary {\n color: var(--fgColor-onEmphasis);\n background-color: var(--bgColor-neutral-emphasis);\n }\n}\n\n.UnderlineNav-body {\n display: flex;\n align-items: center;\n gap: var(--control-medium-gap);\n list-style: none;\n}\n\n.UnderlineNav-item {\n position: relative;\n display: flex;\n padding: 0 var(--control-medium-paddingInline-condensed);\n font-size: var(--text-body-size-medium);\n line-height: 30px;\n color: var(--fgColor-default);\n text-align: center;\n white-space: nowrap;\n cursor: pointer;\n background-color: transparent;\n border: 0;\n border-radius: var(--borderRadius-medium);\n align-items: center;\n\n &:hover,\n &:focus,\n &:focus-visible {\n color: var(--fgColor-default);\n text-decoration: none;\n border-bottom-color: var(--borderColor-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);\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(--fgColor-default);\n text-decoration: none;\n background: var(--control-transparent-bgColor-hover);\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);\n color: var(--fgColor-default);\n border-bottom-color: var(--underlineNav-borderColor-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(--underlineNav-borderColor-active);\n border-radius: var(--borderRadius-medium);\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);\n }\n}\n\n.UnderlineNav-octicon {\n display: inline !important;\n margin-right: var(--control-medium-gap);\n color: var(--fgColor-muted);\n fill: var(--fgColor-muted);\n}\n\n.UnderlineNav-container {\n display: flex;\n justify-content: space-between;\n}\n",null]}
|
@@ -21,13 +21,7 @@
|
|
21
21
|
}
|
22
22
|
}
|
23
23
|
|
24
|
-
.UnderlineNav
|
25
|
-
width: 100%;
|
26
|
-
box-shadow: inset 0 -1px 0 var(--borderColor-muted);
|
27
|
-
padding: var(--control-medium-gap) 0;
|
28
|
-
}
|
29
|
-
|
30
|
-
.UnderlineNav-body,.UnderlineNav::part(tablist) {
|
24
|
+
.UnderlineNav-body {
|
31
25
|
display: flex;
|
32
26
|
align-items: center;
|
33
27
|
gap: var(--control-medium-gap);
|
@@ -1,16 +1,18 @@
|
|
1
|
-
<%=
|
1
|
+
<%= tab_container_wrapper(with_panel: true, **@wrapper_arguments) do %>
|
2
2
|
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
|
3
3
|
<% if @align == :right %>
|
4
4
|
<%= actions %>
|
5
5
|
<% end %>
|
6
|
-
|
7
|
-
|
6
|
+
<%= render body do %>
|
7
|
+
<% tabs.each do |tab| %>
|
8
|
+
<%= tab %>
|
9
|
+
<% end %>
|
8
10
|
<% end %>
|
9
11
|
<% if @align == :left %>
|
10
12
|
<%= actions %>
|
11
13
|
<% end %>
|
12
|
-
|
13
|
-
|
14
|
-
|
14
|
+
<% end %>
|
15
|
+
<% tabs.each do |tab| %>
|
16
|
+
<%= tab.panel %>
|
15
17
|
<% end %>
|
16
18
|
<% end %>
|
@@ -18,7 +18,7 @@ module Primer
|
|
18
18
|
Primer::Alpha::Navigation::Tab.new(
|
19
19
|
selected: selected,
|
20
20
|
with_panel: true,
|
21
|
-
list:
|
21
|
+
list: true,
|
22
22
|
icon_classes: "UnderlineNav-octicon",
|
23
23
|
panel_id: "panel-#{id}",
|
24
24
|
**system_arguments
|
@@ -43,16 +43,24 @@ module Primer
|
|
43
43
|
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
44
44
|
def initialize(label:, align: ALIGN_DEFAULT, body_arguments: {}, wrapper_arguments: {}, **system_arguments)
|
45
45
|
@align = fetch_or_fallback(ALIGN_OPTIONS, align, ALIGN_DEFAULT)
|
46
|
-
@wrapper_arguments =
|
47
|
-
@wrapper_arguments[:tag] = :div
|
46
|
+
@wrapper_arguments = wrapper_arguments
|
48
47
|
|
49
48
|
@system_arguments = deny_tag_argument(**system_arguments)
|
50
|
-
@system_arguments[:tag] = :
|
49
|
+
@system_arguments[:tag] = :div
|
51
50
|
@system_arguments[:classes] = underline_nav_classes(@system_arguments[:classes], @align)
|
52
|
-
@system_arguments[:"aria-label"] = label
|
53
51
|
|
54
52
|
@body_arguments = deny_tag_argument(**body_arguments)
|
55
|
-
@body_arguments[:tag] = :
|
53
|
+
@body_arguments[:tag] = :ul
|
54
|
+
@body_arguments[:classes] = underline_nav_body_classes(@body_arguments[:classes])
|
55
|
+
|
56
|
+
@body_arguments[:role] = :tablist
|
57
|
+
@body_arguments[:"aria-label"] = label
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def body
|
63
|
+
Primer::BaseComponent.new(**@body_arguments)
|
56
64
|
end
|
57
65
|
end
|
58
66
|
end
|
@@ -1,8 +1,11 @@
|
|
1
|
-
<%= render Primer::BaseComponent.new(
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
<%= render Primer::BaseComponent.new(tag: :span) do %>
|
2
|
+
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
|
3
|
+
<% if content.present? %>
|
4
|
+
<%= content %>
|
5
|
+
<% else %>
|
6
|
+
<%= render Primer::Beta::Octicon.new(:copy) %>
|
7
|
+
<%= render Primer::Beta::Octicon.new(:check, color: :success, style: "display: none;") %>
|
8
|
+
<% end %>
|
7
9
|
<% end %>
|
10
|
+
<div aria-live="polite" aria-atomic="true" class="sr-only" data-clipboard-copy-feedback></div>
|
8
11
|
<% end %>
|
@@ -29,12 +29,27 @@ document.addEventListener('clipboard-copy', ({ target }) => {
|
|
29
29
|
if (!target.hasAttribute('data-view-component'))
|
30
30
|
return;
|
31
31
|
const currentTimeout = clipboardCopyElementTimers.get(target);
|
32
|
+
const clipboardCopyLiveRegion = target.parentNode?.querySelector('[data-clipboard-copy-feedback]');
|
33
|
+
const copiedAnnouncement = 'Copied!';
|
32
34
|
if (currentTimeout) {
|
33
35
|
clearTimeout(currentTimeout);
|
34
36
|
clipboardCopyElementTimers.delete(target);
|
35
37
|
}
|
36
38
|
else {
|
37
39
|
showCheck(target);
|
40
|
+
if (clipboardCopyLiveRegion) {
|
41
|
+
if (clipboardCopyLiveRegion.textContent === copiedAnnouncement) {
|
42
|
+
/* This is a hack due to the way the aria live API works.
|
43
|
+
A screen reader will not read a live region again
|
44
|
+
if the text is the same. Adding a space character tells
|
45
|
+
the browser that the live region has updated,
|
46
|
+
which will cause it to read again, but with no audible difference. */
|
47
|
+
clipboardCopyLiveRegion.textContent = `${copiedAnnouncement}\u00A0`;
|
48
|
+
}
|
49
|
+
else {
|
50
|
+
clipboardCopyLiveRegion.textContent = copiedAnnouncement;
|
51
|
+
}
|
52
|
+
}
|
38
53
|
}
|
39
54
|
clipboardCopyElementTimers.set(target, setTimeout(() => {
|
40
55
|
showCopy(target);
|
@@ -10,6 +10,8 @@ module Primer
|
|
10
10
|
#
|
11
11
|
# @accessibility
|
12
12
|
# Always set an accessible label to help the user interact with the component.
|
13
|
+
#
|
14
|
+
# This component has a built-in `aria-live` region that announces "Copied!" when the copy element is pressed.
|
13
15
|
class ClipboardCopy < Primer::Component
|
14
16
|
status :beta
|
15
17
|
|
@@ -37,12 +37,26 @@ document.addEventListener('clipboard-copy', ({target}) => {
|
|
37
37
|
if (!target.hasAttribute('data-view-component')) return
|
38
38
|
|
39
39
|
const currentTimeout = clipboardCopyElementTimers.get(target)
|
40
|
+
const clipboardCopyLiveRegion = target.parentNode?.querySelector<HTMLElement>('[data-clipboard-copy-feedback]')
|
41
|
+
const copiedAnnouncement = 'Copied!'
|
40
42
|
|
41
43
|
if (currentTimeout) {
|
42
44
|
clearTimeout(currentTimeout)
|
43
45
|
clipboardCopyElementTimers.delete(target)
|
44
46
|
} else {
|
45
47
|
showCheck(target)
|
48
|
+
if (clipboardCopyLiveRegion) {
|
49
|
+
if (clipboardCopyLiveRegion.textContent === copiedAnnouncement) {
|
50
|
+
/* This is a hack due to the way the aria live API works.
|
51
|
+
A screen reader will not read a live region again
|
52
|
+
if the text is the same. Adding a space character tells
|
53
|
+
the browser that the live region has updated,
|
54
|
+
which will cause it to read again, but with no audible difference. */
|
55
|
+
clipboardCopyLiveRegion.textContent = `${copiedAnnouncement}\u00A0`
|
56
|
+
} else {
|
57
|
+
clipboardCopyLiveRegion.textContent = copiedAnnouncement
|
58
|
+
}
|
59
|
+
}
|
46
60
|
}
|
47
61
|
|
48
62
|
clipboardCopyElementTimers.set(
|
@@ -5,6 +5,8 @@ module Primer
|
|
5
5
|
# `ClipboardCopyButton` uses the `ClipboardCopy` component to copy text to the clipboard,
|
6
6
|
# styled as a Primer button. It can be used wherever a button is desired, and works well
|
7
7
|
# with components like `ButtonGroup`.
|
8
|
+
# @accessibility
|
9
|
+
# This component has a built-in `aria-live` region that announces "Copied!" when the copy button is pressed.
|
8
10
|
class ClipboardCopyButton < Primer::Beta::Button
|
9
11
|
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::Button) %> and <%= link_to_component(Primer::Beta::ClipboardCopy) %>.
|
10
12
|
def initialize(**system_arguments)
|
@@ -54,7 +54,7 @@ module Primer
|
|
54
54
|
# It's designed to be used to normalize and merge data information from system_arguments
|
55
55
|
# hashes. Consider using this pattern in component initializers:
|
56
56
|
#
|
57
|
-
# @system_arguments[:data] =
|
57
|
+
# @system_arguments[:data] = merge_data(
|
58
58
|
# @system_arguments,
|
59
59
|
# { data: { foo: "bar" } }
|
60
60
|
# )
|
@@ -10,13 +10,5 @@
|
|
10
10
|
|
11
11
|
<div><%= render(Caption.new(input: @input)) %></div>
|
12
12
|
</span>
|
13
|
-
|
14
|
-
csrf = @input.csrf || @view_context.form_authenticity_token(
|
15
|
-
form_options: {
|
16
|
-
method: :post,
|
17
|
-
action: @input.src
|
18
|
-
}
|
19
|
-
)
|
20
|
-
%>
|
21
|
-
<%= render(Primer::Alpha::ToggleSwitch.new(src: @input.src, csrf: csrf, **@input.input_arguments)) %>
|
13
|
+
<%= render(Primer::Alpha::ToggleSwitch.new(src: @input.src, csrf_token: @input.csrf, **@input.input_arguments)) %>
|
22
14
|
<% end %>
|
@@ -52,7 +52,7 @@ module Primer
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def with_csrf_token
|
55
|
-
render(Primer::Alpha::ToggleSwitch.new(src: UrlHelpers.toggle_switch_index_path
|
55
|
+
render(Primer::Alpha::ToggleSwitch.new(src: UrlHelpers.toggle_switch_index_path))
|
56
56
|
end
|
57
57
|
|
58
58
|
def with_bad_csrf_token
|
@@ -9,8 +9,13 @@ module Primer
|
|
9
9
|
# @param size [Symbol] select [medium, large]
|
10
10
|
# @param tag [Symbol] select [span, summary, a, div]
|
11
11
|
# @param inline [Boolean] toggle
|
12
|
-
|
13
|
-
|
12
|
+
# @param href [String] URL to be used with an anchor tag
|
13
|
+
def playground(size: :medium, tag: :span, inline: false, href: nil)
|
14
|
+
if tag == :a
|
15
|
+
render(Primer::Beta::Label.new(tag: tag, size: size, inline: inline, href: href || "#")) { "Link label" }
|
16
|
+
else
|
17
|
+
render(Primer::Beta::Label.new(tag: tag, size: size, inline: inline)) { "Label" }
|
18
|
+
end
|
14
19
|
end
|
15
20
|
|
16
21
|
# @label Default Options
|
@@ -1,3 +1,3 @@
|
|
1
|
-
<%= render(ExampleToggleSwitchForm.new(
|
1
|
+
<%= render(ExampleToggleSwitchForm.new(label: "Good example", src: toggle_switch_index_path, id: "success-toggle")) %>
|
2
2
|
<hr>
|
3
|
-
<%= render(ExampleToggleSwitchForm.new(
|
3
|
+
<%= render(ExampleToggleSwitchForm.new(label: "Bad example", src: toggle_switch_index_path(fail: true), id: "error-toggle")) %>
|
data/static/arguments.json
CHANGED
@@ -2462,6 +2462,18 @@
|
|
2462
2462
|
"default": "N/A",
|
2463
2463
|
"description": "One of `:left` or `:right`. - Defaults to left"
|
2464
2464
|
},
|
2465
|
+
{
|
2466
|
+
"name": "body_arguments",
|
2467
|
+
"type": "Hash",
|
2468
|
+
"default": "`{}`",
|
2469
|
+
"description": "[System arguments](/system-arguments) for the body wrapper."
|
2470
|
+
},
|
2471
|
+
{
|
2472
|
+
"name": "wrapper_arguments",
|
2473
|
+
"type": "Hash",
|
2474
|
+
"default": "`{}`",
|
2475
|
+
"description": "[System arguments](/system-arguments) for the `TabContainer` wrapper."
|
2476
|
+
},
|
2465
2477
|
{
|
2466
2478
|
"name": "system_arguments",
|
2467
2479
|
"type": "Hash",
|
data/static/info_arch.json
CHANGED
@@ -2284,7 +2284,7 @@
|
|
2284
2284
|
{
|
2285
2285
|
"fully_qualified_name": "Primer::Alpha::Banner",
|
2286
2286
|
"description": "Use `Banner` to highlight important information.",
|
2287
|
-
"accessibility_docs": "
|
2287
|
+
"accessibility_docs": "### Improve discoverability with a heading and landmark\nBanners are made visually prominent with icons and colors to immediately draw attention.\n\nTo ensure the Banner is also easily discoverable for assistive technology users, consider:\n1. Providing a heading inside of the Banner that describes the purpose of the Banner.\n2. Designating the Banner as a `region` landmark. This can be achieved by rendering the Banner as a `section` with an `aria-labelledby` pointing to the heading.\n\n### Communicating feedback\nWhen a Banner is used to communicate feedback, a live region or focus management technique should be in place to ensure that assistive technology users are aware of the feedback, especially when the Banner is shown client-side. Visit the [Banner's Accessibility section](https://primer.style/components/banner#accessibility), or defer to the accessibility team.\n\n#### Approach 1: Announcing a Banner\nA live region announcement can be used to communicate non-critical feedback, such as a success message.\n\nTo successfully trigger a live region announcement for a Banner that is un-`hidden` or dynamically injected onto the page, **do not** set the live region attribute/role on the Banner itself. Instead, it's important to rely on a live region that is already guaranteed to be on the page. Learn more about specific techniques at: [Staff only: Challenges with live regions](https://github.com/github/accessibility/blob/main/docs/coaching-recommendations/toast-flash-banner/accessible-banner-prototype.md#challenges-with-dynamically-inserted-live-region).\n\n#### Approach 2: Focusing a Banner\nFocusing the Banner can be appropriate for critical feedback scenarios, such as a [form validation error summary](https://primer.style/ui-patterns/forms/overview#interactive-summary-of-errors) where the Banner contains actions to help unblock the user.\n\nTo properly focus a Banner, add a `tabindex=\"-1\"` place focus with JavaScript (using the [`focus()` API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus)).",
|
2288
2288
|
"is_form_component": false,
|
2289
2289
|
"is_published": true,
|
2290
2290
|
"requires_js": true,
|
@@ -7628,6 +7628,18 @@
|
|
7628
7628
|
"default": "N/A",
|
7629
7629
|
"description": "One of `:left` or `:right`. - Defaults to left"
|
7630
7630
|
},
|
7631
|
+
{
|
7632
|
+
"name": "body_arguments",
|
7633
|
+
"type": "Hash",
|
7634
|
+
"default": "`{}`",
|
7635
|
+
"description": "{{link_to_system_arguments_docs}} for the body wrapper."
|
7636
|
+
},
|
7637
|
+
{
|
7638
|
+
"name": "wrapper_arguments",
|
7639
|
+
"type": "Hash",
|
7640
|
+
"default": "`{}`",
|
7641
|
+
"description": "{{link_to_system_arguments_docs}} for the `TabContainer` wrapper."
|
7642
|
+
},
|
7631
7643
|
{
|
7632
7644
|
"name": "system_arguments",
|
7633
7645
|
"type": "Hash",
|
@@ -11618,7 +11630,7 @@
|
|
11618
11630
|
{
|
11619
11631
|
"fully_qualified_name": "Primer::Beta::ClipboardCopy",
|
11620
11632
|
"description": "Use `ClipboardCopy` to copy element text content or input values to the clipboard.\n\nThis component by itself is not styled as a button, and can therefore only be used in limited circumstances.\nIf you're looking for a button, consider using {{#link_to_component}}Primer::Beta::ClipboardCopyButton{{/link_to_component}}\ninstead.",
|
11621
|
-
"accessibility_docs": "Always set an accessible label to help the user interact with the component.",
|
11633
|
+
"accessibility_docs": "Always set an accessible label to help the user interact with the component.\n\nThis component has a built-in `aria-live` region that announces \"Copied!\" when the copy element is pressed.",
|
11622
11634
|
"is_form_component": false,
|
11623
11635
|
"is_published": true,
|
11624
11636
|
"requires_js": true,
|
@@ -11785,7 +11797,7 @@
|
|
11785
11797
|
{
|
11786
11798
|
"fully_qualified_name": "Primer::Beta::ClipboardCopyButton",
|
11787
11799
|
"description": "`ClipboardCopyButton` uses the `ClipboardCopy` component to copy text to the clipboard,\nstyled as a Primer button. It can be used wherever a button is desired, and works well\nwith components like `ButtonGroup`.",
|
11788
|
-
"accessibility_docs":
|
11800
|
+
"accessibility_docs": "This component has a built-in `aria-live` region that announces \"Copied!\" when the copy button is pressed.",
|
11789
11801
|
"is_form_component": false,
|
11790
11802
|
"is_published": true,
|
11791
11803
|
"requires_js": false,
|