Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
616 changes: 616 additions & 0 deletions docs/data/material/components/buttons/buttons.a11y.json

Large diffs are not rendered by default.

169 changes: 169 additions & 0 deletions packages/mui-material/src/Button/Button.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import * as React from 'react';
import { spy } from 'sinon';
import {
createRenderer,
screen,
Expand Down Expand Up @@ -1031,4 +1032,172 @@ describe('<Button />', () => {
expect(screen.getByRole('button')).to.have.class(classes.loadingPositionEnd);
});
});

describe('WCAG 2.2 conformance', () => {
it('2.1.2 No Keyboard Trap: keyboard focus can enter and leave the button', async () => {
const { user } = render(
<React.Fragment>
<button type="button">Before</button>
<Button>Middle</Button>
<button type="button">After</button>
</React.Fragment>,
);

await user.tab();
expect(screen.getByRole('button', { name: 'Before' })).toHaveFocus();

await user.tab();
expect(screen.getByRole('button', { name: 'Middle' })).toHaveFocus();

// Tab moves focus back out of the button - it is never captured.
await user.tab();
expect(screen.getByRole('button', { name: 'After' })).toHaveFocus();

// Shift+Tab moves back onto it.
Comment thread
michelengelen marked this conversation as resolved.
await user.tab({ shift: true });
expect(screen.getByRole('button', { name: 'Middle' })).toHaveFocus();

// Another Shift+Tab moves out of the button - still not captured.
await user.tab({ shift: true });
expect(screen.getByRole('button', { name: 'Before' })).toHaveFocus();
});

describe('2.4.3 Focus Order', () => {
it('is a single tab stop in natural DOM order with no positive tabIndex', () => {
render(<Button>Middle</Button>);
expect(screen.getByRole('button')).to.have.property('tabIndex', 0);
});

it('removes a disabled button from the tab order', async () => {
Comment thread
michelengelen marked this conversation as resolved.
const { user } = render(
<React.Fragment>
<button type="button">Before</button>
<Button disabled>Disabled</Button>
<button type="button">After</button>
</React.Fragment>,
);

await user.tab();
expect(screen.getByRole('button', { name: 'Before' })).toHaveFocus();

// Tab skips the disabled button and lands on the next control.
await user.tab();
expect(screen.getByRole('button', { name: 'After' })).toHaveFocus();

// Backwards tabbing also skips the disabled button and lands back on the first control.
await user.tab({ shift: true });
expect(screen.getByRole('button', { name: 'Before' })).toHaveFocus();
});

it('removes a loading button from the tab order', async () => {
const { user } = render(
<React.Fragment>
<button type="button">Before</button>
<Button loading>Loading</Button>
<button type="button">After</button>
</React.Fragment>,
);

await user.tab();
expect(screen.getByRole('button', { name: 'Before' })).toHaveFocus();

// Tab skips the disabled button and lands on the next control.
await user.tab();
expect(screen.getByRole('button', { name: 'After' })).toHaveFocus();

// Backwards tabbing also skips the disabled button and lands back on the first control.
await user.tab({ shift: true });
expect(screen.getByRole('button', { name: 'Before' })).toHaveFocus();
});
});

it('2.5.2 Pointer Cancellation: activates on click, but not when released off the target', async () => {
const handleClick = spy();
const { user } = render(
<React.Fragment>
<Button onClick={handleClick}>Pointer cancellation</Button>
<div data-testid="outside" />
</React.Fragment>,
);
const button = screen.getByRole('button', { name: 'Pointer cancellation' });

// Press on the button, move away, then release: nothing runs on the down
// event, and releasing off the target cancels the activation.
await user.pointer([
{ keys: '[MouseLeft>]', target: button },
{ target: screen.getByTestId('outside') },
{ keys: '[/MouseLeft]' },
]);
expect(handleClick.callCount).to.equal(0);

// A full click — press and release over the target — activates.
await user.click(button);
expect(handleClick.callCount).to.equal(1);
});

it('3.2.1 On Focus: moving keyboard focus to the button does not activate it', async () => {
const handleClick = spy();
const { user } = render(<Button onClick={handleClick}>On focus</Button>);

await user.tab();
expect(screen.getByRole('button')).toHaveFocus();
// Focus alone changes no context.
expect(handleClick.callCount).to.equal(0);
});

it('3.2.2 On Input: toggling the pressed or loading state does not activate the button', async () => {
const handleClick = spy();
const { setProps, user } = render(
<Button onClick={handleClick} aria-pressed={false} loading={false}>
Toggle
</Button>,
);

// Flipping the pressed and loading state programmatically must not activate the button.
setProps({ 'aria-pressed': true });
setProps({ loading: true });
expect(handleClick.callCount).to.equal(0);

// Only explicit activation runs the handler. `loading` is turned off first,
// since a loading button is removed from interaction.
setProps({ loading: false });
await user.click(screen.getByRole('button'));
expect(handleClick.callCount).to.equal(1);
});

describe('2.5.3 Label in Name', () => {
it('uses the visible text as the accessible name', () => {
render(<Button>Save changes</Button>);

// getByRole with `name` only resolves if the accessible name matches the label.
expect(screen.getByRole('button', { name: 'Save changes' })).not.to.equal(null);
});

it('keeps the visible words in the name when a decorative icon is present', () => {
render(<Button startIcon={<span data-testid="icon" />}>Delete</Button>);

expect(screen.getByRole('button', { name: 'Delete' })).not.to.equal(null);
});
});

describe('4.1.2 Name, Role, Value', () => {
it('renders a native button with the correct role', () => {
render(<Button>Submit</Button>);

expect(screen.getByRole('button', { name: 'Submit' })).to.have.tagName('button');
});

it('exposes the link role when rendered as an anchor', () => {
render(<Button href="/pricing">Pricing</Button>);

expect(screen.getByRole('link', { name: 'Pricing' })).to.have.tagName('a');
});

it('reflects the disabled state', () => {
render(<Button disabled>Submit</Button>);

expect(screen.getByRole('button')).to.have.property('disabled', true);
});
});
});
});
Loading
Loading