From 2bfd08c130f94db5195565ccf5e9f33e79b77f85 Mon Sep 17 00:00:00 2001 From: Rohit Singh Date: Wed, 21 Oct 2026 23:02:38 +0530 Subject: [PATCH 1/2] [test][FormControlLabel] Add axe tests and WCAG conformance report --- .../form-control-label.a11y.json | 62 ++++ .../accessibility/accessibility.md | 5 +- .../accessibility/scorecard.json | 161 ++++++++- .../FormControlLabel/FormControlLabel.test.js | 84 +++++ .../src/FormControlLabel/accessibility.json | 18 + .../src/FormControlLabel/accessibility.md | 332 ++++++++++++++++++ packages/mui-material/src/accessibility.md | 1 + packages/mui-material/src/manual-testing.md | 12 + test/regressions/a11y/criteriaSuites.mjs | 6 + .../FormControlLabelA11yStates.js | 34 ++ test/regressions/demoMeta.test.ts | 9 + test/regressions/demoMeta.ts | 8 + 12 files changed, 729 insertions(+), 3 deletions(-) create mode 100644 docs/data/material/components/form-control-label/form-control-label.a11y.json create mode 100644 packages/mui-material/src/FormControlLabel/accessibility.json create mode 100644 packages/mui-material/src/FormControlLabel/accessibility.md create mode 100644 test/regressions/a11y/fixtures/form-control-label/FormControlLabelA11yStates.js diff --git a/docs/data/material/components/form-control-label/form-control-label.a11y.json b/docs/data/material/components/form-control-label/form-control-label.a11y.json new file mode 100644 index 00000000000000..e30d44a8519931 --- /dev/null +++ b/docs/data/material/components/form-control-label/form-control-label.a11y.json @@ -0,0 +1,62 @@ +{ + "FormControlLabelA11yStates": { + "rules": { + "aria-allowed-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-conditional-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-deprecated-role": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-hidden-focus": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-prohibited-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-required-attr": { + "status": "pass", + "tags": ["wcag2a"] + }, + "aria-roles": { + "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"] + } + } + } +} diff --git a/docs/data/material/getting-started/accessibility/accessibility.md b/docs/data/material/getting-started/accessibility/accessibility.md index ca08a4cef34293..5920054099cf5a 100644 --- a/docs/data/material/getting-started/accessibility/accessibility.md +++ b/docs/data/material/getting-started/accessibility/accessibility.md @@ -3,7 +3,7 @@

How Material UI components conform to WCAG 2.2 Level A and AA, reported in VPAT terms for procurement and accessibility review.

:::warning -**Draft — partial coverage.** This report covers the 13 components assessed so far, not the whole library. It has not been reviewed by an external auditor, and no assistive-technology testing has been performed yet. See [Scope and limitations](#scope-and-limitations) before relying on it for a procurement decision. +**Draft — partial coverage.** This report covers the 14 components assessed so far, not the whole library. It has not been reviewed by an external auditor, and no assistive-technology testing has been performed yet. See [Scope and limitations](#scope-and-limitations) before relying on it for a procurement decision. ::: ## About this report @@ -43,6 +43,7 @@ Each row counts only the criteria that **apply** to that component; criteria tha | [Button](https://github.com/mui/material-ui/blob/master/packages/mui-material/src/Button/accessibility.md) | 15 | 12 | 27 | 24 | 3 | 20/27 | 11 | | [Checkbox](https://github.com/mui/material-ui/blob/master/packages/mui-material/src/Checkbox/accessibility.md) | 14 | 11 | 25 | 24 | 1 | 22/25 | 11 | | [CircularProgress](https://github.com/mui/material-ui/blob/master/packages/mui-material/src/CircularProgress/accessibility.md) | 6 | 5 | 11 | 8 | 3 | 6/11 | 1 | +| [FormControlLabel](https://github.com/mui/material-ui/blob/master/packages/mui-material/src/FormControlLabel/accessibility.md) | 8 | 7 | 15 | 15 | 0 | 14/15 | 5 | | [LinearProgress](https://github.com/mui/material-ui/blob/master/packages/mui-material/src/LinearProgress/accessibility.md) | 6 | 5 | 11 | 8 | 3 | 6/11 | 1 | | [Radio](https://github.com/mui/material-ui/blob/master/packages/mui-material/src/Radio/accessibility.md) | 14 | 11 | 25 | 24 | 1 | 23/25 | 11 | | [RadioGroup](https://github.com/mui/material-ui/blob/master/packages/mui-material/src/RadioGroup/accessibility.md) | 6 | 1 | 7 | 7 | 0 | 4/7 | 2 | @@ -106,7 +107,7 @@ const theme = createTheme({ focusVisible: true }); Read this section before citing the report. ::: -- **Component coverage is partial.** 13 components are assessed. Widely used components including Select, Autocomplete, Dialog, Menu, Table, Tabs, Slider, Tooltip, Snackbar, and Drawer are **not yet assessed**, and this report says nothing about them. +- **Component coverage is partial.** 14 components are assessed. Widely used components including Select, Autocomplete, Dialog, Menu, Table, Tabs, Slider, Tooltip, Snackbar, and Drawer are **not yet assessed**, and this report says nothing about them. - **No assistive-technology testing.** No screen-reader passes have been performed. Criteria that depend on how a specific assistive technology behaves are assessed from the exposed accessibility tree, not from observed behavior. - **Level AAA is out of scope**, as it is for a standard VPAT. - **Components are rated in isolation**, as rendered with default props and the default theme. Customization, composition, and your surrounding page can change the result. diff --git a/docs/data/material/getting-started/accessibility/scorecard.json b/docs/data/material/getting-started/accessibility/scorecard.json index 5602791ebcc2b5..d34ce77e0ad428 100644 --- a/docs/data/material/getting-started/accessibility/scorecard.json +++ b/docs/data/material/getting-started/accessibility/scorecard.json @@ -1,6 +1,6 @@ { "standard": "WCAG 2.2 Level A and AA", - "componentCount": 13, + "componentCount": 14, "totals": { "rated": 32, "supports": 27, @@ -1538,6 +1538,165 @@ "automated": 1 } }, + { + "component": "FormControlLabel", + "counts": { + "supports": 15, + "partiallySupports": 0, + "doesNotSupport": 0, + "notApplicable": 40, + "flagged": 1, + "rated": 15 + }, + "gaps": [], + "criteria": [ + { + "number": "1.1.1", + "name": "Non-text Content", + "level": "A", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Hybrid", + "flagged": false + }, + { + "number": "1.3.1", + "name": "Info and Relationships", + "level": "A", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Hybrid", + "flagged": false + }, + { + "number": "1.3.2", + "name": "Meaningful Sequence", + "level": "A", + "conformance": "Supports", + "responsibility": "Author", + "group": "Manual", + "flagged": false + }, + { + "number": "1.3.3", + "name": "Sensory Characteristics", + "level": "A", + "conformance": "Supports", + "responsibility": "Author", + "group": "Manual", + "flagged": false + }, + { + "number": "1.4.1", + "name": "Use of Color", + "level": "A", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Hybrid", + "flagged": true + }, + { + "number": "1.4.3", + "name": "Contrast (Minimum)", + "level": "AA", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Hybrid", + "flagged": false + }, + { + "number": "1.4.4", + "name": "Resize Text", + "level": "AA", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Automated", + "flagged": false + }, + { + "number": "1.4.5", + "name": "Images of Text", + "level": "AA", + "conformance": "Supports", + "responsibility": "Author", + "group": "Manual", + "flagged": false + }, + { + "number": "1.4.10", + "name": "Reflow", + "level": "AA", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Automated", + "flagged": false + }, + { + "number": "1.4.12", + "name": "Text Spacing", + "level": "AA", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Automated", + "flagged": false + }, + { + "number": "2.4.6", + "name": "Headings and Labels", + "level": "AA", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Hybrid", + "flagged": false + }, + { + "number": "2.5.3", + "name": "Label in Name", + "level": "A", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Automated", + "flagged": false + }, + { + "number": "2.5.8", + "name": "Target Size (Minimum)", + "level": "AA", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Automated", + "flagged": false + }, + { + "number": "3.3.2", + "name": "Labels or Instructions", + "level": "A", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Hybrid", + "flagged": false + }, + { + "number": "4.1.2", + "name": "Name, Role, Value", + "level": "A", + "conformance": "Supports", + "responsibility": "Shared", + "group": "Hybrid", + "flagged": false + } + ], + "summary": { + "rated": 15, + "levelA": 8, + "levelAA": 7, + "supports": 15, + "partiallySupports": 0, + "doesNotSupport": 0, + "verified": 14, + "automated": 5 + } + }, { "component": "LinearProgress", "counts": { diff --git a/packages/mui-material/src/FormControlLabel/FormControlLabel.test.js b/packages/mui-material/src/FormControlLabel/FormControlLabel.test.js index 26f78afd45686d..f52748c92a78db 100644 --- a/packages/mui-material/src/FormControlLabel/FormControlLabel.test.js +++ b/packages/mui-material/src/FormControlLabel/FormControlLabel.test.js @@ -6,6 +6,8 @@ import FormControlLabel, { } from '@mui/material/FormControlLabel'; import Checkbox from '@mui/material/Checkbox'; import FormControl from '@mui/material/FormControl'; +import Radio from '@mui/material/Radio'; +import Switch from '@mui/material/Switch'; import Typography from '@mui/material/Typography'; import describeConformance from '../../test/describeConformance'; @@ -343,4 +345,86 @@ describe('', () => { expect(screen.getByTestId('control')).to.have.attribute('value', 'test'); }); + + // Deterministic checks for the WCAG success criteria the FormControlLabel + // report rates as the label's own. See ./accessibility.md. + describe('WCAG 2.2 conformance', () => { + it('2.5.3 Label in Name: a Checkbox exposes the visible label as its accessible name', () => { + render(} label="Email notifications" />); + // getByRole with `name` only resolves if the accessible name matches the visible label. + expect(screen.getByRole('checkbox', { name: 'Email notifications' })).not.to.equal(null); + }); + + it('2.5.3 Label in Name: a Radio exposes the visible label as its accessible name', () => { + render(} value="standard" label="Standard shipping" />); + + expect(screen.getByRole('radio', { name: 'Standard shipping' })).not.to.equal(null); + }); + + it('2.5.3 Label in Name: a Switch exposes the visible label as its accessible name', () => { + render(} label="Dark mode" />); + + expect(screen.getByRole('switch', { name: 'Dark mode' })).not.to.equal(null); + }); + + it('2.5.3 Label in Name: the required asterisk is excluded from the accessible name', () => { + render(} label="Accept the terms" />); + + // The name is the label text; the asterisk is decorative and not in the name. + expect(screen.getByRole('checkbox', { name: 'Accept the terms' })).not.to.equal(null); + }); + + describe('4.1.2 Name, Role, Value', () => { + it('exposes the control role with the label as its accessible name', () => { + render(} label="Subscribe" />); + + expect(screen.getByRole('checkbox', { name: 'Subscribe' })).to.have.property( + 'checked', + false, + ); + }); + + it('forwards required to the control without adding the asterisk to the name', () => { + const { container } = render( + } label="Accept the terms" />, + ); + const checkbox = screen.getByRole('checkbox', { name: 'Accept the terms' }); + + expect(checkbox).to.have.property('required', true); + // The visible text carries the asterisk, but the control's name does not. + expect(container.querySelector('label')).to.have.text('Accept the terms\u2009*'); + }); + }); + + it('toggles the wrapped control when the label text is clicked', async () => { + const { user } = render( + } label="Email notifications" />, + ); + const checkbox = screen.getByRole('checkbox', { name: 'Email notifications' }); + expect(checkbox).to.have.property('checked', false); + + await user.click(screen.getByText('Email notifications')); + expect(checkbox).to.have.property('checked', true); + }); + + it('keeps the control before the label in the DOM for every labelPlacement', () => { + ['end', 'start', 'top', 'bottom'].forEach((labelPlacement) => { + const { container, unmount } = render( + } + label="Email notifications" + labelPlacement={labelPlacement} + />, + ); + const input = container.querySelector('input'); + const labelText = screen.getByText('Email notifications'); + + // DOM order is control then label regardless of the visual placement. + // The control and the label are in separate subtrees, so the position + // is exactly DOCUMENT_POSITION_FOLLOWING when the label comes after it. + expect(input.compareDocumentPosition(labelText)).to.equal(Node.DOCUMENT_POSITION_FOLLOWING); + unmount(); + }); + }); + }); }); diff --git a/packages/mui-material/src/FormControlLabel/accessibility.json b/packages/mui-material/src/FormControlLabel/accessibility.json new file mode 100644 index 00000000000000..d9e920dee309eb --- /dev/null +++ b/packages/mui-material/src/FormControlLabel/accessibility.json @@ -0,0 +1,18 @@ +{ + "axe": "docs/data/material/components/form-control-label/form-control-label.a11y.json", + "criteria": { + "2.5.8": { "responsibility": "Shared" } + }, + "notApplicable": [ + "1.4.11", + "2.1.1", + "2.1.2", + "2.4.3", + "2.4.7", + "2.4.11", + "2.5.2", + "3.2.1", + "3.2.2", + "3.2.4" + ] +} diff --git a/packages/mui-material/src/FormControlLabel/accessibility.md b/packages/mui-material/src/FormControlLabel/accessibility.md new file mode 100644 index 00000000000000..26ab64f98408d5 --- /dev/null +++ b/packages/mui-material/src/FormControlLabel/accessibility.md @@ -0,0 +1,332 @@ + + +# FormControlLabel accessibility conformance + +Rated against WCAG 2.2 Level A and AA. See the [reports legend](../accessibility.md). + + + +FormControlLabel attaches a visible, clickable label to a wrapped [Checkbox](../Checkbox/accessibility.md), [Radio](../Radio/accessibility.md), or [Switch](../Switch/accessibility.md), optionally placing it and marking it required. This report rates the criteria the label's own text, placement, and association are responsible for. The wrapped control's keyboard, focus, pointer, role, and value behavior is rated in those component reports and is marked Not Applicable here. + + + +| Result | Count | +| :-------------------- | :---- | +| ✅ Supports | 15 | +| ⚠️ Partially Supports | 0 | +| ❌ Does Not Support | 0 | +| ➖ Not Applicable | 40 | +| 🚩 Flagged | 1/15 | + +## Known gaps + +None. + +## Success criteria + +### 🔍 Manual + +#### 1.3.2 Meaningful Sequence · A + +`✅ Supports` · `○ Author` + + + +- The control is always rendered before the label in the DOM, so the default reading order is control then label. +- `labelPlacement` changes only the visual order through flex direction: `start` is `row-reverse`, `top` is `column-reverse`, and `bottom` is `column`. The DOM order is unchanged, so the exposed reading order of a single control and its label still matches what is seen. +- Order carries meaning only across several controls, which the surrounding layout sets. + +**Manual testing steps** + +1. Render a group of controls with `labelPlacement` `start`, `top`, and `bottom`, then inspect the DOM or accessibility tree order of each row. +2. Compare that order to the visual order. +3. Confirm no `labelPlacement` or flex layout reorders a row visually without changing the DOM. + + + +**Pass:** reading order matches visual order for each control and across a group. + +#### 1.3.3 Sensory Characteristics · A + +`✅ Supports` · `○ Author` + + + +- The label is a text identifier for its control, so the control can be referenced by its label text rather than by shape, color, size, or position. +- Instructions in the surrounding copy must not point to a control by a sensory characteristic alone, for example "the switch on the right". That copy is author content. + +**Manual testing steps** + +1. Review the copy near a labeled control for sensory-only references. +2. Confirm each also names the control by its label text. + + + +**Pass:** no instruction relies on a sensory characteristic without naming the control by its label. + +#### 1.4.5 Images of Text · AA + +`✅ Supports` · `○ Author` + + + +- The label is live DOM text rendered through `Typography`, never an image of text, so it scales and restyles with the page. + +**Manual testing steps** + +1. Select the label text with the mouse, or zoom in, and confirm it behaves like real text rather than an image. + + + +**Pass:** the label is real text, not an image of text. + +### 🔁 Hybrid + +#### 1.1.1 Non-text Content · A + +`✅ Supports` · `◐ Shared` + + + +- The label is author-supplied text rendered through `Typography`; it carries no images or other non-text content that would need a text alternative. +- The only non-text mark is the required `*`, which is decorative and `aria-hidden`, so assistive technology skips it. The required state is conveyed natively on the control. + +**Manual testing steps** + +1. Inspect a required label in the accessibility tree and confirm the asterisk is `aria-hidden` and absent from the control's name. + + + +**Pass:** the label exposes its text directly; the decorative required asterisk is hidden from assistive technology. + +#### 1.3.1 Info and Relationships · A + +`✅ Supports` · `◐ Shared` + + + +- `FormControlLabel` renders a real `