Skip to content
This repository was archived by the owner on Sep 30, 2025. It is now read-only.

Commit c44c96c

Browse files
authored
[Focus states] Use :focus-visible over :focus for focus-ring state. (#8054)
### WHY are these changes introduced? Fixes #8053 Related to: #7585 and #818 We currently use a focus-ring to show a merchant which element is currently in focus. This is useful particularly for users that don't use a traditional pointer device to navigate through the admin. We currently use the :focus CSS selector to add these focus rings to interactive elements. Whilst this is an important accessibility necessity, it can leave the admin looking very busy for merchants that do use a traditional pointer device, with focus rings appearing on every interaction. There is a CSS selector named :focus-visible, which allows the browser to determine when to show the focus state depending on the input device that controls the focus state, and the element in question. This will allow us to persist the vital focus state visibility for users navigating the admin via keyboard, but will leave the admin feeling cleaner for those merchants using a mouse or other pointer device. ### WHAT is this pull request doing? This PR updates instances of `:focus` to show a focus ring and replaces them with `:focus-visible`. It also tidies up some legacy code in some components to detect for keyFocus only, preferring to use the browser standard `:focus-visible` rather than some internal component logic to figure out when focus is being managed by a keyboard. _Note: The Listbox remains untouched as the focus state logic there is pretty custom and will require somebody with more domain knowledge to update that component_ <!-- ℹ️ Delete the following for small / trivial changes --> ### How to 🎩 🖥 [Local development instructions](https://github.com/Shopify/polaris/blob/main/README.md#local-development) 🗒 [General tophatting guidelines](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md) 📄 [Changelog guidelines](https://github.com/Shopify/polaris/blob/main/.github/CONTRIBUTING.md#changelog) <!-- Give as much information as needed to experiment with the component in the playground. --> <details> <summary>Copy-paste this code in <code>playground/Playground.tsx</code>:</summary> ```jsx import React from 'react'; import {Page} from '../src'; export function Playground() { return ( <Page title="Playground"> {/* Add the code you want to test in here */} </Page> ); } ``` </details> ### 🎩 checklist - [x] Tested on [mobile](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md#cross-browser-testing) - [x] Tested on [multiple browsers](https://help.shopify.com/en/manual/shopify-admin/supported-browsers) - [x] Tested for [accessibility](https://github.com/Shopify/polaris/blob/main/documentation/Accessibility%20testing.md) - [x] Updated the component's `README.md` with documentation changes - [x] [Tophatted documentation](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting%20documentation.md) changes in the style guide
1 parent 1aeed24 commit c44c96c

File tree

35 files changed

+64
-250
lines changed

35 files changed

+64
-250
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris': minor
3+
---
4+
5+
Update focus states to be present on :focus-visible rather than :focus

polaris-react/src/components/ActionList/ActionList.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
background-color: var(--p-surface-pressed);
8282
}
8383

84-
&:focus:not(:active) {
84+
&:focus-visible:not(:active) {
8585
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
8686
@include focus-ring($style: 'focused');
8787
outline: var(--p-border-width-3) solid transparent;

polaris-react/src/components/Autocomplete/components/MappedAction/MappedAction.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
background-color: var(--p-surface-pressed);
6060
}
6161

62-
&:focus:not(:active) {
62+
&:focus-visible:not(:active) {
6363
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
6464
@include focus-ring($style: 'focused');
6565
}

polaris-react/src/components/Banner/Banner.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@
236236
text-decoration: underline;
237237
}
238238

239-
&:focus > .Text {
239+
&:focus-visible > .Text {
240240
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
241241
@include plain-button-backdrop;
242242
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
@@ -269,7 +269,7 @@
269269
letter-spacing: initial;
270270
color: var(--p-text);
271271

272-
&:focus {
272+
&:focus-visible {
273273
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
274274
@include focus-ring($style: 'focused');
275275
}

polaris-react/src/components/Breadcrumbs/Breadcrumbs.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@
4343
}
4444
}
4545

46-
&:focus {
46+
&:focus-visible {
4747
outline: none;
4848
}
4949

50-
&:focus:not(:active) {
50+
&:focus-visible:not(:active) {
5151
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
5252
@include focus-ring($style: 'focused');
5353
}

polaris-react/src/components/Button/Button.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@
291291
box-shadow: none;
292292
}
293293

294-
&:focus {
294+
&:focus-visible {
295295
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
296296
@include no-focus-ring;
297297
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
@@ -303,7 +303,7 @@
303303
}
304304
}
305305

306-
&:focus:not(:active) {
306+
&:focus-visible:not(:active) {
307307
> .Content {
308308
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
309309
@include focus-ring($style: 'focused');
@@ -574,7 +574,7 @@
574574
}
575575
}
576576

577-
&:focus {
577+
&:focus-visible {
578578
box-shadow: 0 0 0 var(--p-border-width-1) currentColor;
579579
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
580580
@include focus-ring($style: 'focused');

polaris-react/src/components/Checkbox/Checkbox.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
1313
@include visually-hidden;
1414

15-
&.keyFocused {
15+
&:focus-visible {
1616
+ .Backdrop {
1717
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
1818
@include focus-ring($style: 'focused');

polaris-react/src/components/Checkbox/Checkbox.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, {
22
forwardRef,
33
useRef,
44
useImperativeHandle,
5-
useState,
65
useContext,
76
} from 'react';
87
import {MinusMinor, TickSmallMinor} from '@shopify/polaris-icons';
@@ -13,7 +12,7 @@ import {useUniqueId} from '../../utilities/unique-id';
1312
import {Choice, helpTextID} from '../Choice';
1413
import {errorTextID} from '../InlineError';
1514
import {Icon} from '../Icon';
16-
import {Error, CheckboxHandles, Key} from '../../types';
15+
import type {Error, CheckboxHandles} from '../../types';
1716
import {WithinListboxContext} from '../../utilities/listbox/context';
1817

1918
import styles from './Checkbox.scss';
@@ -76,7 +75,6 @@ export const Checkbox = forwardRef<CheckboxHandles, CheckboxProps>(
7675
setTrue: handleMouseOver,
7776
setFalse: handleMouseOut,
7877
} = useToggle(false);
79-
const [keyFocused, setKeyFocused] = useState(false);
8078
const isWithinListbox = useContext(WithinListboxContext);
8179

8280
useImperativeHandle(ref, () => ({
@@ -89,15 +87,6 @@ export const Checkbox = forwardRef<CheckboxHandles, CheckboxProps>(
8987

9088
const handleBlur = () => {
9189
onBlur && onBlur();
92-
setKeyFocused(false);
93-
};
94-
95-
const handleKeyUp = (event: React.KeyboardEvent) => {
96-
const {keyCode} = event;
97-
98-
if (keyCode === Key.Space || keyCode === Key.Tab) {
99-
!keyFocused && setKeyFocused(true);
100-
}
10190
};
10291

10392
const handleOnClick = () => {
@@ -142,7 +131,6 @@ export const Checkbox = forwardRef<CheckboxHandles, CheckboxProps>(
142131
const inputClassName = classNames(
143132
styles.Input,
144133
isIndeterminate && styles['Input-indeterminate'],
145-
keyFocused && styles.keyFocused,
146134
);
147135

148136
return (
@@ -170,7 +158,6 @@ export const Checkbox = forwardRef<CheckboxHandles, CheckboxProps>(
170158
onChange={noop}
171159
onClick={handleOnClick}
172160
onFocus={onFocus}
173-
onKeyUp={handleKeyUp}
174161
aria-invalid={error != null}
175162
aria-controls={ariaControls}
176163
aria-describedby={ariaDescribedBy}

polaris-react/src/components/Checkbox/tests/Checkbox.test.tsx

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, {AllHTMLAttributes} from 'react';
22
import {mountWithApp} from 'tests/utilities';
33

4-
import {Key} from '../../../types';
54
import {Checkbox} from '../Checkbox';
65

76
describe('<Checkbox />', () => {
@@ -276,31 +275,6 @@ describe('<Checkbox />', () => {
276275
});
277276
});
278277
});
279-
280-
describe('Focus className', () => {
281-
it('on keyUp adds a keyFocused class to the input', () => {
282-
const checkbox = mountWithApp(<Checkbox label="Checkbox" />);
283-
284-
checkbox.find('input')!.trigger('onKeyUp', {
285-
keyCode: Key.Space,
286-
});
287-
288-
expect(checkbox).toContainReactComponent('input', {
289-
className: 'Input keyFocused',
290-
});
291-
});
292-
293-
it('on change does not add a keyFocused class to the input', () => {
294-
const checkbox = mountWithApp(<Checkbox label="Checkbox" />);
295-
const checkboxInput = checkbox.find('input');
296-
checkboxInput!.trigger('onChange', {
297-
currentTarget: checkboxInput!.domNode as HTMLInputElement,
298-
});
299-
expect(checkbox).not.toContainReactComponent('input', {
300-
className: 'Input keyFocused',
301-
});
302-
});
303-
});
304278
});
305279

306280
function noop() {}

polaris-react/src/components/DataTable/DataTable.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@
245245
}
246246
}
247247

248-
&:focus:not(:active) {
248+
&:focus-visible:not(:active) {
249249
// stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY
250250
@include focus-ring($style: 'focused');
251251

0 commit comments

Comments
 (0)