Skip to content

Commit 942f65a

Browse files
langermankjonrohanactions-user
authored
Global focus styles (#1744)
* setup base focus styles * add feature stories * add stories for focusable things, delete outline:0 * switch back to box-shadow * support class * stylelint * fix theme viewer * switch back to outline, address feedback * lint * temp stashing stories here * Create giant-bees-talk.md * I think we got it! * address input directly * checkbox/radio outline offset * lint * change actionlist to just use focus * merge * Update giant-bees-talk.md * address marketing styles * tabnav focus fix * reach all buttons * attempt windows hc selector * Stylelint auto-fixes * fixes * add focus style testing page * Stylelint auto-fixes * add href for testing * remove position relative to fix chrome bug * fix details scenario * add offset to WHC * maintain offset specificity in whc * inset tabnav focus * switch offset to inset * fix actionlist focus * lint * better scoping, handle forms for safari * moving specific styles from dotcom * address autocomplete * cleanup * cleanup * selected focus states * adjust marketing focus * use offset instead for marketing * Stylelint auto-fixes * fix merge Co-authored-by: Jon Rohan <[email protected]> Co-authored-by: Actions Auto Build <[email protected]>
1 parent 00c9060 commit 942f65a

29 files changed

+618
-254
lines changed

.changeset/giant-bees-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/css": major
3+
---
4+
5+
Global CSS focus styles

docs/src/stories/components/Link/Link.stories.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const LinkTemplate = ({label, variant, href, noUnderline, focusElement, f
7171
<>
7272
<a
7373
href={href}
74-
className={clsx(variant && `${variant}`, noUnderline && 'no-underline', focusAllElements && 'focus')}
74+
className={clsx('Link', variant && `${variant}`, noUnderline && 'no-underline', focusAllElements && 'focus')}
7575
>
7676
{label}
7777
</a>

docs/src/stories/components/Marketing/MarketingButton.stories.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default {
77
layout: 'padded'
88
},
99

10-
excludeStories: ['ButtonTemplate'],
10+
excludeStories: ['MarketingButtonTemplate'],
1111
argTypes: {
1212
variant: {
1313
options: [0, 1, 2, 3], // iterator
@@ -77,7 +77,7 @@ const focusMethod = function getFocus() {
7777
button.focus()
7878
}
7979

80-
export const ButtonTemplate = ({label, variant, disabled, size, animated, focusElement, focusAllElements}) => (
80+
export const MarketingButtonTemplate = ({label, variant, disabled, size, animated, focusElement, focusAllElements}) => (
8181
<>
8282
<button
8383
disabled={disabled}
@@ -111,7 +111,7 @@ export const ButtonTemplate = ({label, variant, disabled, size, animated, focusE
111111
</>
112112
)
113113

114-
export const Playground = ButtonTemplate.bind({})
114+
export const Playground = MarketingButtonTemplate.bind({})
115115
Playground.args = {
116116
animated: false,
117117
focusElement: false,

docs/src/stories/components/Marketing/MarketingLink.stories.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default {
77
layout: 'padded'
88
},
99

10-
excludeStories: ['LinkTemplate'],
10+
excludeStories: ['MarketingLinkTemplate'],
1111
argTypes: {
1212
size: {
1313
options: [0, 1], // iterator
@@ -67,7 +67,7 @@ const focusMethod = function getFocus() {
6767
link.focus()
6868
}
6969

70-
export const LinkTemplate = ({label, emphasis, href, size, focusElement, focusAllElements}) => (
70+
export const MarketingLinkTemplate = ({label, emphasis, href, size, focusElement, focusAllElements}) => (
7171
<>
7272
<a
7373
href={href}
@@ -98,7 +98,7 @@ export const LinkTemplate = ({label, emphasis, href, size, focusElement, focusAl
9898
</>
9999
)
100100

101-
export const Playground = LinkTemplate.bind({})
101+
export const Playground = MarketingLinkTemplate.bind({})
102102
Playground.args = {
103103
label: 'Link label',
104104
href: '/',
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from 'react'
2+
import clsx from 'clsx'
3+
import {ButtonTemplate} from '../components/Button/Button.stories.jsx'
4+
import {CheckboxTemplate} from '../components/Forms/Checkbox.stories.jsx'
5+
import {InputTemplate} from '../components/Forms/Input.stories.jsx'
6+
import {SelectTemplate} from '../components/Forms/Select.stories.jsx'
7+
import {TextareaTemplate} from '../components/Forms/Textarea.stories.jsx'
8+
import {LinkTemplate} from '../components/Link/Link.stories.jsx'
9+
import {MarketingButtonTemplate} from '../components/Marketing/MarketingButton.stories.jsx'
10+
import {MarketingLinkTemplate} from '../components/Marketing/MarketingLink.stories.jsx'
11+
import {TabNavTemplate} from '../components/Navigation/TabNav.stories.jsx'
12+
import {TabNavItemTemplate} from '../components/Navigation/TabNavItem.stories.jsx'
13+
14+
export default {
15+
title: 'Patterns/FocusStyles',
16+
layout: 'padded'
17+
}
18+
19+
export const FocusStyles = ({}) => (
20+
<div style={{display: 'flex', flexDirection: 'column', gap: '2rem'}}>
21+
<div style={{display: 'flex', gap: '0.5rem'}}>
22+
<ButtonTemplate variant="btn-primary" label="Primary" />
23+
<ButtonTemplate variant="btn-secondary" label="Secondary" />
24+
<ButtonTemplate variant="btn-outline" label="Outline" />
25+
<ButtonTemplate variant="btn-danger" label="Danger" />
26+
<ButtonTemplate variant="btn-link" label="Link" />
27+
<ButtonTemplate variant="btn-invisible" label="Invisible" />
28+
<ButtonTemplate
29+
variant="btn-octicon"
30+
leadingVisual={`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
31+
<path d="M8 16c.9 0 1.7-.6 1.9-1.5.1-.3-.1-.5-.4-.5h-3c-.3 0-.5.2-.4.5.2.9 1 1.5 1.9 1.5zM3 5c0-2.8 2.2-5 5-5s5 2.2 5 5v3l1.7 2.6c.2.2.3.5.3.8 0 .8-.7 1.5-1.5 1.5h-11c-.8.1-1.5-.6-1.5-1.4 0-.3.1-.6.3-.8L3 8.1V5z"></path>
32+
</svg>`}
33+
/>
34+
<ButtonTemplate variant="btn-primary" label="Primary" disabled />
35+
<ButtonTemplate variant="btn-secondary" label="Secondary" disabled />
36+
<ButtonTemplate variant="btn-outline" label="Outline" disabled />
37+
<ButtonTemplate variant="btn-danger" label="Danger" disabled />
38+
<ButtonTemplate variant="btn-link" label="Link" disabled />
39+
<ButtonTemplate variant="btn-invisible" label="Invisible" disabled />
40+
</div>
41+
<div style={{display: 'flex', flexDirection: 'column', gap: '1rem'}}>
42+
<CheckboxTemplate label="checkbox" type="checkbox" />
43+
<CheckboxTemplate label="radio" type="radio" />
44+
<InputTemplate label="input" type="text" />
45+
<input className="form-control border-0" placeholder="no border form control"></input>
46+
<SelectTemplate label="select" />
47+
<TextareaTemplate label="textarea" />
48+
<LinkTemplate label="Primer link" href="/" />
49+
<a href="/">Link with no CSS class</a>
50+
<MarketingButtonTemplate label="Marketing Button" />
51+
<MarketingLinkTemplate label="Marketing Link" href="/" />
52+
</div>
53+
<div>
54+
<TabNavTemplate>
55+
<TabNavItemTemplate text="First tab" ariaCurrent="location" href="#url" />
56+
<TabNavItemTemplate text="Second tab" href="#url" />
57+
</TabNavTemplate>
58+
</div>
59+
<div class="BtnGroup">
60+
<a href="/" class="btn-sm btn BtnGroup-item">
61+
One
62+
</a>
63+
<a href="/" class="btn-sm btn BtnGroup-item">
64+
Two
65+
</a>
66+
</div>
67+
<div class="Box faketarget">:target styles</div>
68+
<nav class="UnderlineNav" aria-label="Foo bar">
69+
<div class="UnderlineNav-body">
70+
<a class="UnderlineNav-item" href="#url" aria-current="page">
71+
Item 1
72+
</a>
73+
<a class="UnderlineNav-item" href="#url">
74+
Item 2
75+
</a>
76+
<a class="UnderlineNav-item" href="#url">
77+
Item 3
78+
</a>
79+
<a class="UnderlineNav-item" href="#url">
80+
Item 4
81+
</a>
82+
</div>
83+
<div class="UnderlineNav-actions">
84+
<a class="btn btn-sm">Button</a>
85+
</div>
86+
</nav>
87+
</div>
88+
)

src/actionlist/action-list-item.scss

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
// stylelint-disable max-nesting-depth, selector-max-specificity, selector-max-compound-selectors
22

3-
@mixin focusOutline {
4-
position: relative;
5-
z-index: 1;
6-
outline: none;
7-
box-shadow: 0 0 0 2px var(--color-accent-fg); // this color breaks convention
8-
}
9-
103
@mixin activeIndicatorLine {
114
position: absolute;
125
top: calc(50% - 12px);
@@ -314,6 +307,16 @@
314307
text-decoration: none;
315308
}
316309

310+
&:focus {
311+
@include focusOutline;
312+
313+
// remove fallback :focus if :focus-visible is supported
314+
&:not(:focus-visible) {
315+
outline: solid 1px transparent;
316+
}
317+
}
318+
319+
// default focus state
317320
&:focus-visible {
318321
@include focusOutline;
319322
}

src/autocomplete/autocomplete.scss

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
align-items: center;
3232

3333
&:focus-within {
34-
border-color: var(--color-accent-emphasis);
35-
outline: none;
36-
box-shadow: var(--color-primer-shadow-focus);
34+
border-color: var(--color-accent-fg);
35+
36+
@include focusBoxShadowInset;
3737
}
3838

3939
.form-control {
@@ -42,10 +42,6 @@
4242
// stylelint-disable-next-line
4343
border: none;
4444
box-shadow: none;
45-
46-
&:focus {
47-
box-shadow: none;
48-
}
4945
}
5046
}
5147

src/base/base.scss

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// stylelint-disable selector-max-type
1+
// stylelint-disable selector-max-type, selector-no-qualifying-type
22
* {
33
box-sizing: border-box;
44
}
@@ -77,10 +77,70 @@ button {
7777
}
7878

7979
details {
80-
summary { cursor: pointer; }
80+
summary {
81+
cursor: pointer;
82+
}
8183

8284
&:not([open]) {
8385
// Set details content hidden by default for browsers that don't do this
84-
> *:not(summary) { display: none !important; }
86+
> *:not(summary) {
87+
display: none !important;
88+
}
89+
}
90+
}
91+
92+
// global focus styles
93+
94+
a,
95+
button,
96+
[role='button'],
97+
input[type='radio'],
98+
input[type='checkbox'] {
99+
transition: 80ms cubic-bezier(0.33, 1, 0.68, 1);
100+
transition-property: color, background-color, box-shadow, border-color;
101+
// fallback :focus state
102+
&:focus {
103+
@include focusOutline;
104+
105+
// remove fallback :focus if :focus-visible is supported
106+
&:not(:focus-visible) {
107+
outline: solid 1px transparent;
108+
}
109+
}
110+
111+
// default focus state
112+
&:focus-visible {
113+
@include focusOutline;
114+
}
115+
}
116+
117+
a:not([class]),
118+
input[type='radio'],
119+
input[type='checkbox'] {
120+
&:focus,
121+
&:focus-visible {
122+
outline-offset: 0;
123+
}
124+
}
125+
126+
// for handling focus conditionally
127+
.focus {
128+
@include focusBoxShadowInset;
129+
}
130+
131+
// Windows High Contrast mode
132+
@media (forced-colors: active) {
133+
*:focus,
134+
*:focus-visible {
135+
outline: solid 1px transparent;
136+
}
137+
138+
input:not([type='radio'], [type='checkbox']),
139+
textarea,
140+
select {
141+
&:focus,
142+
&:focus-visible {
143+
outline-offset: 2px;
144+
}
85145
}
86146
}

0 commit comments

Comments
 (0)