Skip to content

Commit

Permalink
feat(Spinner): improve a11y (#1274)
Browse files Browse the repository at this point in the history
- `role = "progressbar"`: I've found some examples online where spinner
is represented as a indeterminate progress bar. For example, [Material
does this](https://m2.material.io/components/progress-indicators):


![image](https://github.com/user-attachments/assets/70a6908f-52ba-4646-8976-4ec21dec6f3e)


![image](https://github.com/user-attachments/assets/ad33b8d2-cf0f-4586-a35c-4a305048b141)

- `aria-polite`: we don't need changes announcements to be assertive.

- `aria-busy`: to express that content is being loaded:

![image](https://github.com/user-attachments/assets/e0378615-a03c-46fd-aa39-7481a8cc2812)
  • Loading branch information
marcoskolodny authored Oct 21, 2024
1 parent 85fcb31 commit 5267ad5
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 8 deletions.
30 changes: 30 additions & 0 deletions src/__tests__/spinner-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import {Spinner} from '..';
import {render, screen} from '@testing-library/react';
import ThemeContextProvider from '../theme-context-provider';
import {makeTheme} from './test-utils';

test('spinner is accessible', () => {
render(
<ThemeContextProvider theme={makeTheme()}>
<Spinner />
</ThemeContextProvider>
);

const spinner = screen.getByRole('progressbar', {name: 'Cargando'});

expect(spinner).toBeInTheDocument();
expect(spinner).toHaveAttribute('aria-busy');
});

test('spinner with aria-hidden is not visible', () => {
render(
<ThemeContextProvider theme={makeTheme()}>
<Spinner aria-hidden />
</ThemeContextProvider>
);

const spinner = screen.queryByRole('progressbar');

expect(spinner).not.toBeInTheDocument();
});
2 changes: 1 addition & 1 deletion src/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ const renderButtonContent = ({
>
{shouldRenderSpinner ? (
<Spinner
rolePresentation={!!loadingText}
aria-hidden={!!loadingText}
color="currentcolor"
delay="0s"
size={spinnerSizeRem}
Expand Down
28 changes: 21 additions & 7 deletions src/spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,25 @@ type Props = {
color?: string;
delay?: string;
size?: number | string;
/** @deprecated Use aria-hidden instead */
rolePresentation?: boolean;
'aria-hidden'?: 'true' | 'false' | boolean;
style?: React.CSSProperties;
children?: void;
};

const Spinner = ({color, delay = '500ms', size = 24, style, rolePresentation}: Props): JSX.Element => {
const Spinner = ({
color,
delay = '500ms',
size = 24,
style,
rolePresentation,
'aria-hidden': ariaHidden,
}: Props): JSX.Element => {
const {texts, platformOverrides, t} = useTheme();
const isInverse = useIsInverseOrMediaVariant();
color = color || (isInverse ? vars.colors.controlActivatedInverse : vars.colors.controlActivated);
const spinnerId = React.useId();
const withTitle = !rolePresentation;
const title = texts.loading || t(tokens.loading);
const content =
getPlatform(platformOverrides) === 'ios' ? (
Expand All @@ -31,11 +39,14 @@ const Spinner = ({color, delay = '500ms', size = 24, style, rolePresentation}: P
className={styles.spinnerIos}
height={size}
style={{...style}}
role="img"
role="progressbar"
aria-live="polite"
aria-busy
aria-hidden={ariaHidden || rolePresentation}
viewBox="0 0 30 30"
width={size}
>
{withTitle && <title id={spinnerId}>{title}</title>}
<title id={spinnerId}>{title}</title>
<g role="presentation">
<path
className={styles.spinnerIosSvgPath}
Expand Down Expand Up @@ -81,15 +92,18 @@ const Spinner = ({color, delay = '500ms', size = 24, style, rolePresentation}: P
</svg>
) : (
<svg
aria-labelledby={withTitle ? spinnerId : undefined}
aria-labelledby={spinnerId}
className={styles.spinnerDefault}
height={size}
style={{...style}}
role="img"
role="progressbar"
aria-live="polite"
aria-busy
aria-hidden={ariaHidden || rolePresentation}
viewBox="0 0 66 66"
width={size}
>
{withTitle && <title id={spinnerId}>{title}</title>}
<title id={spinnerId}>{title}</title>
<circle
className={styles.spinnerDefaultPath}
cx="33"
Expand Down

0 comments on commit 5267ad5

Please sign in to comment.