diff --git a/docs/data/material/components/checkboxes/checkboxes.a11y.json b/docs/data/material/components/checkboxes/checkboxes.a11y.json new file mode 100644 index 00000000000000..b8eaac57f25c1b --- /dev/null +++ b/docs/data/material/components/checkboxes/checkboxes.a11y.json @@ -0,0 +1,406 @@ +{ + "Checkboxes": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + }, + "CheckboxesGroup": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "color-contrast": { + "status": "pass", + "tags": ["wcag2aa"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + }, + "CheckboxLabels": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "color-contrast": { + "status": "pass", + "tags": ["wcag2aa"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + }, + "ColorCheckboxes": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + }, + "ControlledCheckbox": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + }, + "CustomizedCheckbox": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + }, + "IconCheckboxes": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + }, + "IndeterminateCheckbox": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "color-contrast": { + "status": "pass", + "tags": ["wcag2aa"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + }, + "SizeCheckboxes": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-valid-attr-value": { + "status": "pass", + "tags": ["wcag2a"] + }, + "form-field-multiple-labels": { + "status": "pass", + "tags": ["wcag2a"] + }, + "label": { + "status": "pass", + "tags": ["wcag2a"] + }, + "nested-interactive": { + "status": "pass", + "tags": ["wcag2a"] + }, + "target-size": { + "status": "pass", + "tags": ["wcag22aa"] + } + } + } +} diff --git a/packages/mui-material/src/Checkbox/Checkbox.test.js b/packages/mui-material/src/Checkbox/Checkbox.test.js index d697a2048d6504..50920ae6401c71 100644 --- a/packages/mui-material/src/Checkbox/Checkbox.test.js +++ b/packages/mui-material/src/Checkbox/Checkbox.test.js @@ -11,6 +11,7 @@ import { } from '@mui/internal-test-utils'; import Checkbox, { checkboxClasses as classes } from '@mui/material/Checkbox'; import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; import ButtonBase, { buttonBaseClasses } from '@mui/material/ButtonBase'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import describeConformance from '../../test/describeConformance'; @@ -311,6 +312,166 @@ describe('', () => { expect(checkbox.querySelector('.touch-ripple')).not.to.equal(null); }); + // Deterministic checks for the WCAG success criteria the report rates as + // automatable. See packages/mui-material/src/Checkbox/accessibility.md. + describe('WCAG 2.2 conformance', () => { + it('2.1.1 Keyboard: toggles the checked state with the Space key', async () => { + const handleChange = spy(); + const { user } = render(); + const checkbox = screen.getByRole('checkbox'); + + await user.tab(); + expect(checkbox).toHaveFocus(); + + await user.keyboard('[Space]'); + expect(checkbox).to.have.property('checked', true); + expect(handleChange.callCount).to.equal(1); + + await user.keyboard('[Space]'); + expect(checkbox).to.have.property('checked', false); + expect(handleChange.callCount).to.equal(2); + }); + + it('2.1.2 No Keyboard Trap: keyboard focus can enter and leave the checkbox', async () => { + const { user } = render( + + before + + after + , + ); + const checkbox = screen.getByRole('checkbox'); + + await user.tab(); + expect(screen.getByRole('button', { name: 'before' })).toHaveFocus(); + + await user.tab(); + expect(checkbox).toHaveFocus(); + + // Tab moves focus back out of the checkbox; it is never captured. + await user.tab(); + expect(screen.getByRole('button', { name: 'after' })).toHaveFocus(); + + // Shift+Tab moves back onto it. + await user.tab({ shift: true }); + expect(checkbox).toHaveFocus(); + }); + + describe('2.4.3 Focus Order', () => { + it('is a single tab stop in natural DOM order with no positive tabIndex', async () => { + const { user } = render( + + first + + last + , + ); + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).to.have.property('tabIndex', 0); + + await user.tab(); + expect(screen.getByRole('button', { name: 'first' })).toHaveFocus(); + await user.tab(); + expect(checkbox).toHaveFocus(); + await user.tab(); + expect(screen.getByRole('button', { name: 'last' })).toHaveFocus(); + }); + + it('removes a disabled checkbox from the tab order', async () => { + const { user } = render( + + + after + , + ); + + // Tab skips the disabled checkbox and lands on the next control. + await user.tab(); + expect(screen.getByRole('button', { name: 'after' })).toHaveFocus(); + }); + }); + + it('2.5.2 Pointer Cancellation: activates on click, but not when released off the target', async () => { + const handleChange = spy(); + const { user } = render( + + + + , + ); + const checkbox = screen.getByRole('checkbox'); + + // Press on the checkbox, move away, then release: nothing runs on the down + // event, and releasing off the target cancels the activation. + await user.pointer([ + { keys: '[MouseLeft>]', target: checkbox }, + { target: screen.getByTestId('outside') }, + { keys: '[/MouseLeft]' }, + ]); + expect(checkbox).to.have.property('checked', false); + expect(handleChange.callCount).to.equal(0); + + // A full click (press and release over the target) activates. + await user.click(checkbox); + expect(checkbox).to.have.property('checked', true); + expect(handleChange.callCount).to.equal(1); + }); + + it('2.5.3 Label in Name: exposes an accessible name matching the visible label', () => { + render(} label="Remember me" />); + // getByRole with `name` only resolves if the accessible name matches the visible label. + expect(screen.getByRole('checkbox', { name: 'Remember me' })).to.have.property( + 'checked', + false, + ); + }); + + it('3.2.1 On Focus: moving keyboard focus to the checkbox does not activate it', async () => { + const handleChange = spy(); + const { user } = render(); + const checkbox = screen.getByRole('checkbox'); + + await user.tab(); + + expect(checkbox).toHaveFocus(); + expect(checkbox).to.have.property('checked', false); + expect(handleChange.callCount).to.equal(0); + }); + + describe('4.1.2 Name, Role, Value', () => { + it('exposes the mixed state through the native indeterminate property', () => { + // The `prop: indeterminate` block above covers the property lifecycle; + // this asserts the 4.1.2 exposure: native property, no aria-checked. + render(); + const checkbox = screen.getByRole('checkbox'); + + expect(checkbox).to.have.property('indeterminate', true); + expect(checkbox).not.to.have.attribute('aria-checked'); + }); + + it('exposes the checkbox role and its accessible name', () => { + render(} label="Subscribe" />); + + expect(screen.getByRole('checkbox', { name: 'Subscribe' })).not.to.equal(null); + }); + + it('reflects the checked state and notifies on change', async () => { + const { user } = render(); + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).to.have.property('checked', false); + + await user.click(checkbox); + expect(checkbox).to.have.property('checked', true); + }); + + it('reflects the disabled state', () => { + render(); + + expect(screen.getByRole('checkbox')).to.have.property('disabled', true); + }); + }); + }); + describe('theme.focusVisible', () => { // `cssVariables: true` guards the shouldSkipGeneratingVar fix — the recipe must stay inline on // the svg. No-vars coverage is the FocusVisible/SelectionControls regression fixture. diff --git a/packages/mui-material/src/Checkbox/accessibility.md b/packages/mui-material/src/Checkbox/accessibility.md new file mode 100644 index 00000000000000..a3e36dd2c62b74 --- /dev/null +++ b/packages/mui-material/src/Checkbox/accessibility.md @@ -0,0 +1,359 @@ +# Checkbox accessibility conformance + +Rated against WCAG 2.2 Level A and AA. See the [reports legend](../accessibility.md). + +| Result | Count | +| :-------------------- | :---- | +| ✅ Supports | 23 | +| ⚠️ Partially Supports | 2 | +| ❌ Does Not Support | 0 | +| ➖ Not Applicable | 30 | +| 🚩 Flagged | 4/25 | + +## Known gaps + +- ⚠️ **1.4.11 Non-text Contrast.** The default checkmark icons clear 3:1 (`warning` is the tightest at 3.11:1). The keyboard focus indicator is untested. `disableRipple`/`disableFocusRipple` or custom icons can drop below 3:1 — a faint custom unchecked box can be about 1.1:1 against the page. +- ⚠️ **2.4.7 Focus Visible.** `disableRipple`/`disableFocusRipple` removes the default focus indicator (the focus ripple). None remains unless the author adds `.Mui-focusVisible` styling or enables the `theme.focusVisible` ring, which draws on the icon and does not depend on the ripple. + +## Success criteria + +### 🔍 Manual + +#### 1.3.2 Meaningful Sequence · A + +`✅ Supports` · `○ Author` + +- The checkbox is one control: the hidden `` followed by the `aria-hidden` checkmark icon, with `FormControlLabel` placing the label and control in source order, so the exposed reading order matches the visual order. The component applies no CSS reordering to itself. +- Order carries meaning only across several controls, which the surrounding layout sets. `labelPlacement="start"` flips the label/control visually with `row-reverse`, so confirm the reading order of a checkbox group still matches its visual order. + +**Manual testing steps** + +1. In a group of checkboxes (`Checkbox` + `FormControlLabel` inside a `FormGroup`), press Tab through the rows and note the focus order. +2. Compare that order to the visual top-to-bottom order. +3. Watch for `labelPlacement` or flex layouts that reorder the row visually without changing the DOM. + +**Pass:** focus and reading order match the visual order. + +#### 1.3.3 Sensory Characteristics · A + +`✅ Supports` · `○ Author` + +- The component renders no instructional text of its own, so it introduces no shape-, color-, or position-only instructions. +- Surrounding copy must not rely on sensory characteristics alone (for example "check the green box" or "the boxes on the right"). That is authored content. + +**Manual testing steps** + +1. Review the copy near checkboxes for sensory-only references. +2. Check that each also names the checkbox by its label. + +**Pass:** no instruction relies on color, shape, size, or position without naming the control. + +#### 1.4.5 Images of Text · AA + +`✅ Supports` · `○ Author` + +- Labels and helper text render as live DOM text; the checkmark icon is a vector graphic, not an image of text, and it carries no textual information. +- This fails only if an author passes an image of text as the label. Use real text unless it is a logo. + +**Manual testing steps** + +1. Inspect the label and helper nodes and confirm they are selectable live text, not `` or background images of text. +2. Confirm no label uses an image of text. + +**Pass:** labels and helper text are live text. + +#### 2.4.11 Focus Not Obscured (Minimum) · AA + +`✅ Supports` · `○ Author` + +- The checkbox is an ordinary focusable control and never places itself behind other content. Obscuring comes from sticky headers, banners, or overlays in the surrounding layout. +- It passes as long as that content never hides a focused checkbox completely; partial overlap is fine. + +**Manual testing steps** + +1. In a page with a sticky header, footer, or banner, scroll so a checkbox sits under the sticky element. +2. Press Tab to move focus onto it. + +**Pass:** at least part of the focused checkbox stays visible, never fully covered. + +#### 3.2.4 Consistent Identification · AA + +`✅ Supports` · `○ Author` + +- The author supplies the name through a `FormControlLabel` or an `aria-label`. Whether the same function is identified consistently across pages is up to the author. +- Consistency is a cross-page property. Confirm that checkboxes with the same function share a label, and that one label is not reused for different functions. + +**Manual testing steps** + +1. List the checkboxes that do the same job across the product. +2. Compare their labels. + +**Pass:** the same function uses the same label, and no label is reused for different functions. + +### 🔁 Hybrid + +#### 1.1.1 Non-text Content · A + +`✅ Supports` · `◐ Shared` + +- The default `CheckBox*` icons, and any `SvgIcon` passed via `icon`/`checkedIcon`, default to `aria-hidden`, so they stay decorative; the accessible name comes from the label, not the checkmark icon. +- The name is author-supplied: a bare `` has none. The author adds one with `FormControlLabel` (a real ``) or `slotProps.input` `aria-label`/`aria-labelledby`, as the docs instruct. +- axe-core `label` confirms a non-empty name across the enrolled demos in `checkboxes.a11y.json`. + +**Manual testing steps** + +1. In the accessibility tree, inspect alabelledcheckbox (`Checkbox` inside a `FormControlLabel`) and confirm it shows `role=checkbox` with the visible label as its name. +2. Inspect a bare `` with no label and confirm its name is empty, the failure mode authors must avoid. +3. Confirm the checkmark icon's SVG is `aria-hidden`. + +**Pass:** every checkbox exposes a non-empty accessible name and the checkmark icon stays `aria-hidden`. + +#### 1.3.1 Info and Relationships · A + +`✅ Supports` · `◐ Shared` + +- The native `` exposes role and checked state; `required` and `disabled` map to native attributes; `indeterminate` sets the native `.indeterminate` property (unit-tested). +- `FormControlLabel` associates the label through a real ``, and `FormControl component="fieldset"` with `FormLabel component="legend"` names a group. +- axe-core's ARIA rules (`aria-allowed-attr`, `aria-valid-attr-value`, `aria-prohibited-attr`, `form-field-multiple-labels`) pass across the demos. +- Two relationships are the author's responsibility. `FormHelperText` is a bare `` with no `aria-describedby`, so helper and error text are not associated with the control, and a tri-state "select all" parent is not linked to its children. The author must associate them with `aria-describedby` and the appropriate group markup. + +**Manual testing steps** + +1. For a requiredlabelledcheckbox (`Checkbox required` inside a `FormControlLabel`), confirm `role=checkbox`, the name, `required`, and the wrapping ``. +2. Toggle indeterminate and confirm the accessibility tree exposes the mixed state (from the native `.indeterminate` property). +3. In a fieldset group (`FormControl component="fieldset"` + `FormLabel component="legend"` around a `FormGroup`), confirm the ``/`` name is exposed but any error or required constraint is not (no `aria-describedby`/`aria-invalid`). + +**Pass:** role, states, label association, and group name are programmatically determinable, and any helper/error text is tied to the control via author-supplied `aria-describedby`. + +#### 1.4.1 Use of Color · A + +`🚩` · `✅ Supports` · `◐ Shared` + +- State is conveyed by the checkmark icon's shape, not color: an empty square (unchecked), a square with a check (checked), and a square with a dash (indeterminate). A color-blind user can tell all three apart. +- The group error state is color-only: `FormHelperText` conveys it through red text. Pair it with text or an icon. (Required, by contrast, shows a visible `*`, which is a non-color cue, so it is fine.) That is author content. + +**Manual testing steps** + +1. View unchecked, checked, and indeterminate checkboxes in grayscale (DevTools Rendering, Emulate vision deficiencies, Achromatopsia); confirm all three remain distinguishable. +2. View a checkbox with an error `FormHelperText` in grayscale and confirm "error" is still perceivable without the red. + +**Pass:** the three states are distinguishable by shape, and any error state is conveyed by more than color. + +#### 1.4.3 Contrast (Minimum) · AA + +`✅ Supports` · `◐ Shared` + +- This criterion governs the visible text (the label and helper text), not the checkmark icon (that is 1.4.11). +- The label uses `text.primary` (about `16:1` on white, `18:1` on dark) and helper text uses `text.secondary` (about `5.7:1`). Error helper text `error.main` is the tightest at `4.98:1`, still above `4.5:1`. +- axe-core `color-contrast` passes on the labeled demos (recorded in `checkboxes.a11y.json`); disabled label text is exempt. +- The component renders no text on a colored fill, so unlike a contained button there is no label-on-background risk. Custom label colors or a colored container behind the row are the author's to check. Disabled text is exempt. + +**Manual testing steps** + +1. Measure the label and helper text against the real background in light and dark themes. +2. Pay attention to light-mode error text (`#d32f2f`, about `5:1`) on any non-white background. +3. Re-check any custom label color. + +**Pass:** label and helper/error text meet `4.5:1` (`3:1` for large text) against the actual background. + +#### 1.4.11 Non-text Contrast · AA + +`🚩` · `⚠️ Partially Supports` · `● Component` + +- This criterion (not 1.4.3) governs the checkmark icon at `3:1`, and every default state passes against the page background (`#fff` light, `#121212` dark). +- Light mode: `warning` is the tightest at `3.11:1`, then `info` `3.86:1`, `primary` `4.60:1`, the unchecked `text.secondary` outline `5.74:1`, and the white check on the fill `4.60:1`. Dark mode clears `3:1` easily (lowest is error at about `5:1`). +- `warning.main` is tuned to just clear `3:1`, so a colored container behind the checkbox can push it under. +- The keyboard focus indicator's contrast is untested, and `disableRipple`/`disableFocusRipple` removes it. +- Custom icons can fall below `3:1`: a faint custom unchecked box (for example a `#f5f8fa` fill) measures about `1.1:1` against the page (`1.5:1` at its border). +- Disabled (`action.disabled`, about `1.9:1`) is exempt. + +**Manual testing steps** + +1. Measure the unchecked outline and each checked color (`primary` through `warning`) against the background; confirm `3:1`, checking `warning` specifically. +2. Measure the keyboard focus indicator against the colors next to it. +3. Audit any custom icons against `3:1` (a faint custom unchecked box is a common fail); treat disabled as exempt. + +**Pass:** the indicator in every active state (unchecked outline, checked fill, indeterminate dash) and the focus indicator are `3:1` against adjacent colors; disabled is exempt. + +#### 2.4.6 Headings and Labels · AA + +`✅ Supports` · `◐ Shared` + +- The component renders whatever label and group legend the author supplies; descriptiveness is a content decision. This criterion does not require a label to exist, only that any provided label describes its purpose. axe-core `label` confirms a name is present across the demos; whether it is descriptive is a manual review. +- A vague label ("Option 1") would fail, and the component cannot enforce wording. + +**Manual testing steps** + +1. Read each checkbox label and group legend. +2. Confirm each describes the option ("Email notifications", not "Option 1"). + +**Pass:** every provided label and legend describes its purpose. + +#### 2.4.7 Focus Visible · AA + +`🚩` · `⚠️ Partially Supports` · `● Component` + +- `ButtonBase` removes the user-agent outline (`outline: 0`). In the default configuration keyboard focus adds the `.Mui-focusVisible` class plus a centered focus ripple, so an indicator is shown. +- `disableRipple` or `disableFocusRipple` removes the default focus indicator (the focus ripple), leaving none unless the author adds `.Mui-focusVisible` styles (for example, a 2px outline). + +**Manual testing steps** + +1. Press Tab to a default `` and confirm a visible focus indicator appears. +2. Press Tab to a `` (or `disableFocusRipple`) with no custom styles and confirm none appears. +3. Click with the mouse and confirm the indicator is keyboard-only. + +**Pass:** every keyboard-focused checkbox shows a visible indicator, including `disableRipple` via author-supplied styles. Today a bare `disableRipple` checkbox shows none. + +#### 3.3.2 Labels or Instructions · A + +`🚩` · `✅ Supports` · `◐ Shared` + +- Used as documented, the component meets this: `FormControlLabel` wraps the control in a real `` whose text is presented to all users. +- An `aria-label` is a name for assistive technology, not a visible label, so on its own it satisfies 4.1.2 but not this criterion. Using `aria-label` alone is fine for a control shown in isolation; in product UI, provide a visible label via `FormControlLabel` or a clear contextual one. + +**Manual testing steps** + +1. Confirm each checkbox in product UI has a visible label (`FormControlLabel`) or a clear contextual one. +2. Where only an `aria-label` is used, confirm the purpose is clear from surrounding context (for example, a column header). + +**Pass:** every checkbox has a label or instruction presented to all users, via `FormControlLabel`, adjacent text, or clear context. + +#### 4.1.2 Name, Role, Value · A + +`✅ Supports` · `◐ Shared` + +- Role, checked, and disabled are met natively: the native `` exposes `role=checkbox`, the checked state, and change notification with no ARIA needed, and `disabled` maps to native `disabled`. axe-core (`aria-allowed-attr`, `aria-valid-attr-value`, `label`, `nested-interactive`) confirms them. +- The `indeterminate` state sets the native `.indeterminate` property and no `aria-checked` attribute, so browsers expose the mixed state through the standard mechanism. axe `aria-conditional-attr` passes across the demos, and unit tests cover the property (set, kept after a click, restored when the input slot changes). +- The name is the shared part: it comes from the author's `FormControlLabel` or `aria-label`. One caveat: `readOnly` sets the native `readonly` attribute, which has no effect on a checkbox and is not exposed to assistive technology, so a read-only checkbox is not announced as such. Use `disabled` or `aria-readonly` if that state must be conveyed. +- Confirmed by unit tests in [`./Checkbox.test.js`](./Checkbox.test.js) (role, `checked`, and `disabled` are exposed). + +**Manual testing steps** + +1. Inspect alabelledcheckbox (`Checkbox` inside a `FormControlLabel`) and confirm `role=checkbox` with a non-empty name in the accessibility tree. +2. Toggle it with a screen reader running and confirm the state change is announced. +3. With `indeterminate` set, confirm "mixed" or partially checked is announced. + +**Pass:** the role, the checked/unchecked/mixed/disabled state with change notification, and an author-supplied accessible name are all programmatically determinable. + +### ⚙️ Automated + +#### 1.4.4 Resize Text · AA + +`✅ Supports` · `◐ Shared` + +- The checkmark icon is an `SvgIcon` whose size comes from `fontSize` in `rem` (medium 24px = 1.5rem, small 20px = 1.25rem) with a `1em` box, so it scales with browser zoom or root font size; the label is normal text. Nothing is pixel-fixed. +- A fixed-pixel container in the surrounding layout could clip at 200%. Covered by a Playwright test at 200% text size. + +#### 1.4.10 Reflow · AA + +`✅ Supports` · `◐ Shared` + +- The checkbox is a small inline-flex control with no fixed min-width and no horizontal-overflow layout of its own, so it reflows into a 320 CSS pixel viewport. +- Real failures usually come from the surrounding layout, such as a wide non-wrapping row of checkboxes overflowing at 320 pixels. Covered by a Playwright test at a 320px viewport. + +#### 1.4.12 Text Spacing · AA + +`✅ Supports` · `◐ Shared` + +- The component sets no text styles of its own and no fixed heights that would clip under user text-spacing overrides; the label is normal flowing text that wraps, and the checkmark icon is a fixed vector unaffected by spacing. +- Whether a long label clips depends on the author's container, not the checkbox. Covered by a Playwright test applying the WCAG text-spacing overrides. + +#### 2.1.1 Keyboard · A + +`✅ Supports` · `● Component` + +- The native `` is the single focusable control (the root span is `role: undefined`, `tabIndex: null`), so the browser provides Tab focus and Space toggle with no timing or path dependence, matching the [APG checkbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/). Disabled leaves the tab order. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (Space toggles the checked state). + +#### 2.1.2 No Keyboard Trap · A + +`✅ Supports` · `● Component` + +- A single native input with standard Tab in and out. It installs no focus-capturing handlers and no containment logic, so focus cannot be trapped. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (Tab is not intercepted, and focus moves away freely). + +#### 2.4.3 Focus Order · A + +`✅ Supports` · `◐ Shared` + +- The checkbox is one focusable element in natural DOM order, with no positive `tabIndex` and no reordering, and a disabled checkbox leaves the order, so it is one correct focus stop. Whether that order preserves meaning in a given layout stays a manual check. +- Order across controls, and group order, is the surrounding layout's responsibility. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (default `tabIndex` is `0`; a disabled checkbox leaves the order). + +#### 2.5.2 Pointer Cancellation · A + +`✅ Supports` · `● Component` + +- Toggling runs on the native input's `click`, fired on pointer-up over the target. Nothing toggles on the down event (the ripple starts there but is decorative), and releasing off the control cancels. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (releasing off the target does not toggle; `click` does). Covered by unit tests. + +#### 2.5.3 Label in Name · A + +`✅ Supports` · `◐ Shared` + +- With `FormControlLabel` and a string label, the accessible name equals the visible text, so the name contains the label. The required `*` is symbolic punctuation, not part of the label, so it is correctly excluded from the name; native `required` conveys that state instead. +- A `slotProps.input` `aria-label` that differs from the visible text breaks this. Keep the visible words in the name. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (the accessible name matches the visible label). Covered by unit tests. + +#### 2.5.8 Target Size (Minimum) · AA + +`✅ Supports` · `◐ Shared` + +- The target is the padded root, not the icon (the input fills it at 100%): medium is a 24px icon plus 2 by 9px padding = 42px, and `size="small"` is 38px, both above the 24 by 24 CSS pixel minimum. A custom 16px icon still yields 34px. axe-core `target-size` confirms this across the demos in `checkboxes.a11y.json`. +- It would drop below 24px only if an author both shrinks the icon and removes the padding. Custom `sx`/`size` overrides and hit-area changes under browser zoom are not covered. + +#### 3.2.1 On Focus · A + +`✅ Supports` · `● Component` + +- Focusing the input only moves focus and shows the focus indicator. There is no navigation, dialog, or content change on focus, and the component registers no `onFocus` side effects. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (focusing the checkbox does not activate it). + +#### 3.2.2 On Input · A + +`✅ Supports` · `◐ Shared` + +- Toggling only flips the checked or indeterminate state and fires `onChange`; per the Understanding document, changing a control's value is not a change of context. `readOnly` short-circuits the change. +- An author `onChange` that navigates or submits without warning would be an author-side failure. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (toggling changes only the checked value). + +## Not applicable + +- **1.3.5 Identify Input Purpose (AA).** Covers inputs that collect information about the user through `autocomplete`. A boolean checkbox is not one of those input purposes. +- **1.4.13 Content on Hover or Focus (AA).** Needs additional content such as a tooltip or popover. The checkbox only restyles itself; a `Tooltip` wrapper would own this. +- **2.1.4 Character Key Shortcuts (A).** The only key is Space, and only while focused. There is no single-character shortcut. +- **2.2.2 Pause, Stop, Hide (A).** No auto-starting moving, blinking, or auto-updating content. +- **2.4.4 Link Purpose (In Context) (A).** The component renders no links. +- **2.5.7 Dragging Movements (AA, new in 2.2).** Covers drag operations. The checkbox toggles by tap or click. +- **3.1.1 Language of Page (A), 3.1.2 Language of Parts (AA).** The component emits no `` and no text of its own. A foreign-language label is the author's phrase to mark. +- **3.2.3 Consistent Navigation (AA).** Covers repeated navigation across a set of pages, not a single control. +- **3.2.6 Consistent Help (A, new in 2.2).** Covers consistent placement of help across pages. +- **3.3.7 Redundant Entry (A, new in 2.2).** Covers repopulating previously entered data. The checkbox captures none. +- **3.3.8 Accessible Authentication (Minimum) (AA, new in 2.2).** The paste, autofill, and cognitive-test duty falls on the credential fields, not a checkbox. +- **4.1.3 Status Messages (AA).** The checkbox emits no status messages; its checked-state change is covered by 4.1.2. +- **Time-based media (1.2.1 to 1.2.5).** No audio or video. +- **Audio Control (1.4.2).** Emits no audio. +- **Orientation (1.3.4).** Sets no orientation lock; a layout concern. +- **Bypass Blocks (2.4.1), Page Titled (2.4.2), Multiple Ways (2.4.5).** Page or site structure, not a single control. +- **Timing Adjustable (2.2.1).** Sets no time limit. +- **Three Flashes or Below Threshold (2.3.1).** Nothing flashes. +- **Pointer Gestures (2.5.1), Motion Actuation (2.5.4).** Toggles on a simple click; reads no device motion. +- **Error Identification (3.3.1), Error Suggestion (3.3.3), Error Prevention (3.3.4).** The checkbox collects and validates no input; these belong to the form or process. When an author surfaces an error, identify it in text and tie it to the control with `aria-describedby` (tracked under 1.3.1). + +## Level AAA + +The following SC are applicable but out of scope: + +- **2.3.3 Animation from Interactions.** The ripple's `scale()` animation honors `prefers-reduced-motion` when the theme sets `motion.reducedMotion` to `system` (follows the OS) or `always`; `disableRipple` also removes it. The default is `never`, so OS reduced-motion is not honored by default. `🚩` +- **2.4.13 Focus Appearance.** The focus ripple is unlikely to meet the area and 3:1 thresholds, and `disableRipple` removes it. `🚩` +- **2.5.5 Target Size (Enhanced), 44 px.** Default sizes (42px medium, 38px small) are below 44px. `🚩` +- **1.4.6 Contrast (Enhanced), 7:1.** The label (`text.primary`, about 16:1) clears 7:1, but helper text (about 5:1) and the checkmark icon fall short where the 7:1 text bar applies. `🚩` +- Also touched, in the same shape as their A and AA siblings: **1.3.6 Identify Purpose, 1.4.9 Images of Text (No Exception), 2.1.3 Keyboard (No Exception), 2.4.12 Focus Not Obscured (Enhanced)**. + +## Scope and test environment + +- **Standard.** WCAG 2.2, Level A and AA. +- **Component version.** `@mui/material` 9.x. +- **Scope.** The Checkbox component and its documented composition with `FormControlLabel`, `FormControl`/`FormGroup`, `FormLabel`, and `FormHelperText`, rendered through the documented API. +- **Automated.** axe-core via the Playwright visual-regression harness (results in [`checkboxes.a11y.json`](../../../../docs/data/material/components/checkboxes/checkboxes.a11y.json)), plus interaction tests in [`Checkbox.test.js`](./Checkbox.test.js). Checkmark icon contrast is computed from the theme tokens, since no axe rule covers non-text contrast. +- **Assistive-technology review.** Not yet performed. Flagged criteria are assessed from source pending a review with NVDA, JAWS, and VoiceOver. diff --git a/packages/mui-material/src/accessibility.md b/packages/mui-material/src/accessibility.md index b57bd2d3a000e2..80234337ae6c20 100644 --- a/packages/mui-material/src/accessibility.md +++ b/packages/mui-material/src/accessibility.md @@ -69,6 +69,7 @@ Components are rated in isolation against WCAG 2.2 A and AA. The levels are [cum | [AccordionSummary](./AccordionSummary/accessibility.md) | 23 | 1 | 0 | 31 | | [Avatar](./Avatar/accessibility.md) | 9 | 2 | 0 | 44 | | [Button](./Button/accessibility.md) | 23 | 4 | 0 | 28 | +| [Checkbox](./Checkbox/accessibility.md) | 23 | 2 | 0 | 30 | | [LinearProgress](./LinearProgress/accessibility.md) | 8 | 3 | 0 | 44 | | [Radio](./Radio/accessibility.md) | 23 | 2 | 0 | 30 | | [RadioGroup](./RadioGroup/accessibility.md) | 7 | 0 | 0 | 30 | diff --git a/test/regressions/demoMeta.ts b/test/regressions/demoMeta.ts index 42a882a29b8d67..e5fb0ea091e5a5 100644 --- a/test/regressions/demoMeta.ts +++ b/test/regressions/demoMeta.ts @@ -242,6 +242,18 @@ const RADIO_A11Y_DEMOS = [ 'UseRadioGroup', ]; +const CHECKBOX_A11Y_DEMOS = [ + 'Checkboxes', + 'CheckboxLabels', + 'ColorCheckboxes', + 'ControlledCheckbox', + 'CustomizedCheckbox', + 'IconCheckboxes', + 'SizeCheckboxes', + 'CheckboxesGroup', + 'IndeterminateCheckbox', +]; + // Switch docs demos enrolled for axe assertions. FormControlLabelPosition is // excluded: its `aria-label` on a role-less FormGroup div trips // `aria-prohibited-attr`, a demo quirk unrelated to Switch. @@ -347,6 +359,14 @@ export const A11Y_RULES: A11yRule[] = [ assertions: 'all', skipAssertions: ['color-contrast'], }, + // IndeterminateCheckbox needs no skip: the component sets the native + // `.indeterminate` property and no aria-checked attribute (#49053), so + // axe's aria-conditional-attr passes. + { + test: `docs/data/material/components/checkboxes/{${CHECKBOX_A11Y_DEMOS.join(',')}}`, + enabled: true, + assertions: 'all', + }, { test: `docs/data/material/components/progress/{${LINEARPROGRESS_A11Y_DEMOS.join(',')}}`, enabled: true,
` with no `aria-describedby`, so helper and error text are not associated with the control, and a tri-state "select all" parent is not linked to its children. The author must associate them with `aria-describedby` and the appropriate group markup. + +**Manual testing steps** + +1. For a requiredlabelledcheckbox (`Checkbox required` inside a `FormControlLabel`), confirm `role=checkbox`, the name, `required`, and the wrapping ``. +2. Toggle indeterminate and confirm the accessibility tree exposes the mixed state (from the native `.indeterminate` property). +3. In a fieldset group (`FormControl component="fieldset"` + `FormLabel component="legend"` around a `FormGroup`), confirm the ``/`` name is exposed but any error or required constraint is not (no `aria-describedby`/`aria-invalid`). + +**Pass:** role, states, label association, and group name are programmatically determinable, and any helper/error text is tied to the control via author-supplied `aria-describedby`. + +#### 1.4.1 Use of Color · A + +`🚩` · `✅ Supports` · `◐ Shared` + +- State is conveyed by the checkmark icon's shape, not color: an empty square (unchecked), a square with a check (checked), and a square with a dash (indeterminate). A color-blind user can tell all three apart. +- The group error state is color-only: `FormHelperText` conveys it through red text. Pair it with text or an icon. (Required, by contrast, shows a visible `*`, which is a non-color cue, so it is fine.) That is author content. + +**Manual testing steps** + +1. View unchecked, checked, and indeterminate checkboxes in grayscale (DevTools Rendering, Emulate vision deficiencies, Achromatopsia); confirm all three remain distinguishable. +2. View a checkbox with an error `FormHelperText` in grayscale and confirm "error" is still perceivable without the red. + +**Pass:** the three states are distinguishable by shape, and any error state is conveyed by more than color. + +#### 1.4.3 Contrast (Minimum) · AA + +`✅ Supports` · `◐ Shared` + +- This criterion governs the visible text (the label and helper text), not the checkmark icon (that is 1.4.11). +- The label uses `text.primary` (about `16:1` on white, `18:1` on dark) and helper text uses `text.secondary` (about `5.7:1`). Error helper text `error.main` is the tightest at `4.98:1`, still above `4.5:1`. +- axe-core `color-contrast` passes on the labeled demos (recorded in `checkboxes.a11y.json`); disabled label text is exempt. +- The component renders no text on a colored fill, so unlike a contained button there is no label-on-background risk. Custom label colors or a colored container behind the row are the author's to check. Disabled text is exempt. + +**Manual testing steps** + +1. Measure the label and helper text against the real background in light and dark themes. +2. Pay attention to light-mode error text (`#d32f2f`, about `5:1`) on any non-white background. +3. Re-check any custom label color. + +**Pass:** label and helper/error text meet `4.5:1` (`3:1` for large text) against the actual background. + +#### 1.4.11 Non-text Contrast · AA + +`🚩` · `⚠️ Partially Supports` · `● Component` + +- This criterion (not 1.4.3) governs the checkmark icon at `3:1`, and every default state passes against the page background (`#fff` light, `#121212` dark). +- Light mode: `warning` is the tightest at `3.11:1`, then `info` `3.86:1`, `primary` `4.60:1`, the unchecked `text.secondary` outline `5.74:1`, and the white check on the fill `4.60:1`. Dark mode clears `3:1` easily (lowest is error at about `5:1`). +- `warning.main` is tuned to just clear `3:1`, so a colored container behind the checkbox can push it under. +- The keyboard focus indicator's contrast is untested, and `disableRipple`/`disableFocusRipple` removes it. +- Custom icons can fall below `3:1`: a faint custom unchecked box (for example a `#f5f8fa` fill) measures about `1.1:1` against the page (`1.5:1` at its border). +- Disabled (`action.disabled`, about `1.9:1`) is exempt. + +**Manual testing steps** + +1. Measure the unchecked outline and each checked color (`primary` through `warning`) against the background; confirm `3:1`, checking `warning` specifically. +2. Measure the keyboard focus indicator against the colors next to it. +3. Audit any custom icons against `3:1` (a faint custom unchecked box is a common fail); treat disabled as exempt. + +**Pass:** the indicator in every active state (unchecked outline, checked fill, indeterminate dash) and the focus indicator are `3:1` against adjacent colors; disabled is exempt. + +#### 2.4.6 Headings and Labels · AA + +`✅ Supports` · `◐ Shared` + +- The component renders whatever label and group legend the author supplies; descriptiveness is a content decision. This criterion does not require a label to exist, only that any provided label describes its purpose. axe-core `label` confirms a name is present across the demos; whether it is descriptive is a manual review. +- A vague label ("Option 1") would fail, and the component cannot enforce wording. + +**Manual testing steps** + +1. Read each checkbox label and group legend. +2. Confirm each describes the option ("Email notifications", not "Option 1"). + +**Pass:** every provided label and legend describes its purpose. + +#### 2.4.7 Focus Visible · AA + +`🚩` · `⚠️ Partially Supports` · `● Component` + +- `ButtonBase` removes the user-agent outline (`outline: 0`). In the default configuration keyboard focus adds the `.Mui-focusVisible` class plus a centered focus ripple, so an indicator is shown. +- `disableRipple` or `disableFocusRipple` removes the default focus indicator (the focus ripple), leaving none unless the author adds `.Mui-focusVisible` styles (for example, a 2px outline). + +**Manual testing steps** + +1. Press Tab to a default `` and confirm a visible focus indicator appears. +2. Press Tab to a `` (or `disableFocusRipple`) with no custom styles and confirm none appears. +3. Click with the mouse and confirm the indicator is keyboard-only. + +**Pass:** every keyboard-focused checkbox shows a visible indicator, including `disableRipple` via author-supplied styles. Today a bare `disableRipple` checkbox shows none. + +#### 3.3.2 Labels or Instructions · A + +`🚩` · `✅ Supports` · `◐ Shared` + +- Used as documented, the component meets this: `FormControlLabel` wraps the control in a real `` whose text is presented to all users. +- An `aria-label` is a name for assistive technology, not a visible label, so on its own it satisfies 4.1.2 but not this criterion. Using `aria-label` alone is fine for a control shown in isolation; in product UI, provide a visible label via `FormControlLabel` or a clear contextual one. + +**Manual testing steps** + +1. Confirm each checkbox in product UI has a visible label (`FormControlLabel`) or a clear contextual one. +2. Where only an `aria-label` is used, confirm the purpose is clear from surrounding context (for example, a column header). + +**Pass:** every checkbox has a label or instruction presented to all users, via `FormControlLabel`, adjacent text, or clear context. + +#### 4.1.2 Name, Role, Value · A + +`✅ Supports` · `◐ Shared` + +- Role, checked, and disabled are met natively: the native `` exposes `role=checkbox`, the checked state, and change notification with no ARIA needed, and `disabled` maps to native `disabled`. axe-core (`aria-allowed-attr`, `aria-valid-attr-value`, `label`, `nested-interactive`) confirms them. +- The `indeterminate` state sets the native `.indeterminate` property and no `aria-checked` attribute, so browsers expose the mixed state through the standard mechanism. axe `aria-conditional-attr` passes across the demos, and unit tests cover the property (set, kept after a click, restored when the input slot changes). +- The name is the shared part: it comes from the author's `FormControlLabel` or `aria-label`. One caveat: `readOnly` sets the native `readonly` attribute, which has no effect on a checkbox and is not exposed to assistive technology, so a read-only checkbox is not announced as such. Use `disabled` or `aria-readonly` if that state must be conveyed. +- Confirmed by unit tests in [`./Checkbox.test.js`](./Checkbox.test.js) (role, `checked`, and `disabled` are exposed). + +**Manual testing steps** + +1. Inspect alabelledcheckbox (`Checkbox` inside a `FormControlLabel`) and confirm `role=checkbox` with a non-empty name in the accessibility tree. +2. Toggle it with a screen reader running and confirm the state change is announced. +3. With `indeterminate` set, confirm "mixed" or partially checked is announced. + +**Pass:** the role, the checked/unchecked/mixed/disabled state with change notification, and an author-supplied accessible name are all programmatically determinable. + +### ⚙️ Automated + +#### 1.4.4 Resize Text · AA + +`✅ Supports` · `◐ Shared` + +- The checkmark icon is an `SvgIcon` whose size comes from `fontSize` in `rem` (medium 24px = 1.5rem, small 20px = 1.25rem) with a `1em` box, so it scales with browser zoom or root font size; the label is normal text. Nothing is pixel-fixed. +- A fixed-pixel container in the surrounding layout could clip at 200%. Covered by a Playwright test at 200% text size. + +#### 1.4.10 Reflow · AA + +`✅ Supports` · `◐ Shared` + +- The checkbox is a small inline-flex control with no fixed min-width and no horizontal-overflow layout of its own, so it reflows into a 320 CSS pixel viewport. +- Real failures usually come from the surrounding layout, such as a wide non-wrapping row of checkboxes overflowing at 320 pixels. Covered by a Playwright test at a 320px viewport. + +#### 1.4.12 Text Spacing · AA + +`✅ Supports` · `◐ Shared` + +- The component sets no text styles of its own and no fixed heights that would clip under user text-spacing overrides; the label is normal flowing text that wraps, and the checkmark icon is a fixed vector unaffected by spacing. +- Whether a long label clips depends on the author's container, not the checkbox. Covered by a Playwright test applying the WCAG text-spacing overrides. + +#### 2.1.1 Keyboard · A + +`✅ Supports` · `● Component` + +- The native `` is the single focusable control (the root span is `role: undefined`, `tabIndex: null`), so the browser provides Tab focus and Space toggle with no timing or path dependence, matching the [APG checkbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/). Disabled leaves the tab order. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (Space toggles the checked state). + +#### 2.1.2 No Keyboard Trap · A + +`✅ Supports` · `● Component` + +- A single native input with standard Tab in and out. It installs no focus-capturing handlers and no containment logic, so focus cannot be trapped. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (Tab is not intercepted, and focus moves away freely). + +#### 2.4.3 Focus Order · A + +`✅ Supports` · `◐ Shared` + +- The checkbox is one focusable element in natural DOM order, with no positive `tabIndex` and no reordering, and a disabled checkbox leaves the order, so it is one correct focus stop. Whether that order preserves meaning in a given layout stays a manual check. +- Order across controls, and group order, is the surrounding layout's responsibility. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (default `tabIndex` is `0`; a disabled checkbox leaves the order). + +#### 2.5.2 Pointer Cancellation · A + +`✅ Supports` · `● Component` + +- Toggling runs on the native input's `click`, fired on pointer-up over the target. Nothing toggles on the down event (the ripple starts there but is decorative), and releasing off the control cancels. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (releasing off the target does not toggle; `click` does). Covered by unit tests. + +#### 2.5.3 Label in Name · A + +`✅ Supports` · `◐ Shared` + +- With `FormControlLabel` and a string label, the accessible name equals the visible text, so the name contains the label. The required `*` is symbolic punctuation, not part of the label, so it is correctly excluded from the name; native `required` conveys that state instead. +- A `slotProps.input` `aria-label` that differs from the visible text breaks this. Keep the visible words in the name. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (the accessible name matches the visible label). Covered by unit tests. + +#### 2.5.8 Target Size (Minimum) · AA + +`✅ Supports` · `◐ Shared` + +- The target is the padded root, not the icon (the input fills it at 100%): medium is a 24px icon plus 2 by 9px padding = 42px, and `size="small"` is 38px, both above the 24 by 24 CSS pixel minimum. A custom 16px icon still yields 34px. axe-core `target-size` confirms this across the demos in `checkboxes.a11y.json`. +- It would drop below 24px only if an author both shrinks the icon and removes the padding. Custom `sx`/`size` overrides and hit-area changes under browser zoom are not covered. + +#### 3.2.1 On Focus · A + +`✅ Supports` · `● Component` + +- Focusing the input only moves focus and shows the focus indicator. There is no navigation, dialog, or content change on focus, and the component registers no `onFocus` side effects. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (focusing the checkbox does not activate it). + +#### 3.2.2 On Input · A + +`✅ Supports` · `◐ Shared` + +- Toggling only flips the checked or indeterminate state and fires `onChange`; per the Understanding document, changing a control's value is not a change of context. `readOnly` short-circuits the change. +- An author `onChange` that navigates or submits without warning would be an author-side failure. +- Confirmed by a unit test in [`./Checkbox.test.js`](./Checkbox.test.js) (toggling changes only the checked value). + +## Not applicable + +- **1.3.5 Identify Input Purpose (AA).** Covers inputs that collect information about the user through `autocomplete`. A boolean checkbox is not one of those input purposes. +- **1.4.13 Content on Hover or Focus (AA).** Needs additional content such as a tooltip or popover. The checkbox only restyles itself; a `Tooltip` wrapper would own this. +- **2.1.4 Character Key Shortcuts (A).** The only key is Space, and only while focused. There is no single-character shortcut. +- **2.2.2 Pause, Stop, Hide (A).** No auto-starting moving, blinking, or auto-updating content. +- **2.4.4 Link Purpose (In Context) (A).** The component renders no links. +- **2.5.7 Dragging Movements (AA, new in 2.2).** Covers drag operations. The checkbox toggles by tap or click. +- **3.1.1 Language of Page (A), 3.1.2 Language of Parts (AA).** The component emits no `` and no text of its own. A foreign-language label is the author's phrase to mark. +- **3.2.3 Consistent Navigation (AA).** Covers repeated navigation across a set of pages, not a single control. +- **3.2.6 Consistent Help (A, new in 2.2).** Covers consistent placement of help across pages. +- **3.3.7 Redundant Entry (A, new in 2.2).** Covers repopulating previously entered data. The checkbox captures none. +- **3.3.8 Accessible Authentication (Minimum) (AA, new in 2.2).** The paste, autofill, and cognitive-test duty falls on the credential fields, not a checkbox. +- **4.1.3 Status Messages (AA).** The checkbox emits no status messages; its checked-state change is covered by 4.1.2. +- **Time-based media (1.2.1 to 1.2.5).** No audio or video. +- **Audio Control (1.4.2).** Emits no audio. +- **Orientation (1.3.4).** Sets no orientation lock; a layout concern. +- **Bypass Blocks (2.4.1), Page Titled (2.4.2), Multiple Ways (2.4.5).** Page or site structure, not a single control. +- **Timing Adjustable (2.2.1).** Sets no time limit. +- **Three Flashes or Below Threshold (2.3.1).** Nothing flashes. +- **Pointer Gestures (2.5.1), Motion Actuation (2.5.4).** Toggles on a simple click; reads no device motion. +- **Error Identification (3.3.1), Error Suggestion (3.3.3), Error Prevention (3.3.4).** The checkbox collects and validates no input; these belong to the form or process. When an author surfaces an error, identify it in text and tie it to the control with `aria-describedby` (tracked under 1.3.1). + +## Level AAA + +The following SC are applicable but out of scope: + +- **2.3.3 Animation from Interactions.** The ripple's `scale()` animation honors `prefers-reduced-motion` when the theme sets `motion.reducedMotion` to `system` (follows the OS) or `always`; `disableRipple` also removes it. The default is `never`, so OS reduced-motion is not honored by default. `🚩` +- **2.4.13 Focus Appearance.** The focus ripple is unlikely to meet the area and 3:1 thresholds, and `disableRipple` removes it. `🚩` +- **2.5.5 Target Size (Enhanced), 44 px.** Default sizes (42px medium, 38px small) are below 44px. `🚩` +- **1.4.6 Contrast (Enhanced), 7:1.** The label (`text.primary`, about 16:1) clears 7:1, but helper text (about 5:1) and the checkmark icon fall short where the 7:1 text bar applies. `🚩` +- Also touched, in the same shape as their A and AA siblings: **1.3.6 Identify Purpose, 1.4.9 Images of Text (No Exception), 2.1.3 Keyboard (No Exception), 2.4.12 Focus Not Obscured (Enhanced)**. + +## Scope and test environment + +- **Standard.** WCAG 2.2, Level A and AA. +- **Component version.** `@mui/material` 9.x. +- **Scope.** The Checkbox component and its documented composition with `FormControlLabel`, `FormControl`/`FormGroup`, `FormLabel`, and `FormHelperText`, rendered through the documented API. +- **Automated.** axe-core via the Playwright visual-regression harness (results in [`checkboxes.a11y.json`](../../../../docs/data/material/components/checkboxes/checkboxes.a11y.json)), plus interaction tests in [`Checkbox.test.js`](./Checkbox.test.js). Checkmark icon contrast is computed from the theme tokens, since no axe rule covers non-text contrast. +- **Assistive-technology review.** Not yet performed. Flagged criteria are assessed from source pending a review with NVDA, JAWS, and VoiceOver. diff --git a/packages/mui-material/src/accessibility.md b/packages/mui-material/src/accessibility.md index b57bd2d3a000e2..80234337ae6c20 100644 --- a/packages/mui-material/src/accessibility.md +++ b/packages/mui-material/src/accessibility.md @@ -69,6 +69,7 @@ Components are rated in isolation against WCAG 2.2 A and AA. The levels are [cum | [AccordionSummary](./AccordionSummary/accessibility.md) | 23 | 1 | 0 | 31 | | [Avatar](./Avatar/accessibility.md) | 9 | 2 | 0 | 44 | | [Button](./Button/accessibility.md) | 23 | 4 | 0 | 28 | +| [Checkbox](./Checkbox/accessibility.md) | 23 | 2 | 0 | 30 | | [LinearProgress](./LinearProgress/accessibility.md) | 8 | 3 | 0 | 44 | | [Radio](./Radio/accessibility.md) | 23 | 2 | 0 | 30 | | [RadioGroup](./RadioGroup/accessibility.md) | 7 | 0 | 0 | 30 | diff --git a/test/regressions/demoMeta.ts b/test/regressions/demoMeta.ts index 42a882a29b8d67..e5fb0ea091e5a5 100644 --- a/test/regressions/demoMeta.ts +++ b/test/regressions/demoMeta.ts @@ -242,6 +242,18 @@ const RADIO_A11Y_DEMOS = [ 'UseRadioGroup', ]; +const CHECKBOX_A11Y_DEMOS = [ + 'Checkboxes', + 'CheckboxLabels', + 'ColorCheckboxes', + 'ControlledCheckbox', + 'CustomizedCheckbox', + 'IconCheckboxes', + 'SizeCheckboxes', + 'CheckboxesGroup', + 'IndeterminateCheckbox', +]; + // Switch docs demos enrolled for axe assertions. FormControlLabelPosition is // excluded: its `aria-label` on a role-less FormGroup div trips // `aria-prohibited-attr`, a demo quirk unrelated to Switch. @@ -347,6 +359,14 @@ export const A11Y_RULES: A11yRule[] = [ assertions: 'all', skipAssertions: ['color-contrast'], }, + // IndeterminateCheckbox needs no skip: the component sets the native + // `.indeterminate` property and no aria-checked attribute (#49053), so + // axe's aria-conditional-attr passes. + { + test: `docs/data/material/components/checkboxes/{${CHECKBOX_A11Y_DEMOS.join(',')}}`, + enabled: true, + assertions: 'all', + }, { test: `docs/data/material/components/progress/{${LINEARPROGRESS_A11Y_DEMOS.join(',')}}`, enabled: true,