Skip to content

Commit 7e141bf

Browse files
committed
fix(template-require-mandatory-role-attributes): recognize native aria-checked from <input type=checkbox|radio>
The rule required an explicit `aria-checked` on every element with a role that needs it — even when the host element is a semantic form control that contributes the state natively. WAI-ARIA APG documents <input type="checkbox" role="switch"> as an accessible switch pattern (see https://www.w3.org/WAI/ARIA/apg/patterns/switch/); the <input>'s native checked state maps to aria-checked. Fix: when the host is <input type="checkbox"> or <input type="radio"> and the role is one of {checkbox, menuitemcheckbox, menuitemradio, radio, switch}, treat aria-checked as satisfied. Matches eslint-plugin-jsx-a11y's role-has-required-aria-props, which uses axobject-query's isSemanticRoleElement to skip the check for element+role combinations where the native element already exposes the required state. Our implementation covers the common cases without pulling in the axobject-query dependency. Five new test cases added covering the documented pattern. Rule doc updated with an explicit example and an APG link.
1 parent 24882a3 commit 7e141bf

3 files changed

Lines changed: 68 additions & 5 deletions

File tree

docs/rules/template-require-mandatory-role-attributes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@ This rule **allows** the following:
3131
<div role="option" aria-selected="false" />
3232
<CustomComponent role="checkbox" aria-required="true" aria-checked="false" />
3333
{{some-component role="heading" aria-level="2"}}
34+
35+
{{! <input type="checkbox|radio"> supplies aria-checked natively for roles that require it. }}
36+
<input type="checkbox" role="switch" />
37+
<input type="checkbox" role="menuitemcheckbox" />
38+
<input type="radio" role="menuitemradio" />
3439
</template>
3540
```
3641

3742
## References
3843

3944
- [WAI-ARIA Roles - Accessibility \_ MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
45+
- [WAI-ARIA APG — Switch pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/) (documents `<input type="checkbox" role="switch">` as an accessible switch)

lib/rules/template-require-mandatory-role-attributes.js

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,54 @@ function getStaticRoleFromMustache(node) {
2828
return undefined;
2929
}
3030

31-
function getMissingRequiredAttributes(role, foundAriaAttributes) {
31+
// Roles whose required `aria-checked` is supplied natively by <input type="checkbox">
32+
// or <input type="radio">. Per WAI-ARIA APG, semantic form controls contribute
33+
// implicit state; e.g. <input type="checkbox" role="switch"> is a documented
34+
// accessible switch pattern where aria-checked mirrors the native checkedness.
35+
const ROLES_WITH_IMPLICIT_CHECKED_FROM_INPUT = new Set([
36+
'checkbox',
37+
'menuitemcheckbox',
38+
'menuitemradio',
39+
'radio',
40+
'switch',
41+
]);
42+
43+
function getInputType(node) {
44+
if (node?.tag !== 'input') {
45+
return undefined;
46+
}
47+
const typeAttr = node.attributes?.find((a) => a.name === 'type');
48+
if (typeAttr?.value?.type === 'GlimmerTextNode') {
49+
return typeAttr.value.chars;
50+
}
51+
return undefined;
52+
}
53+
54+
function isNativelyChecked(node, role) {
55+
if (!ROLES_WITH_IMPLICIT_CHECKED_FROM_INPUT.has(role)) {
56+
return false;
57+
}
58+
const type = getInputType(node);
59+
return type === 'checkbox' || type === 'radio';
60+
}
61+
62+
function getMissingRequiredAttributes(role, foundAriaAttributes, node) {
3263
const roleDefinition = roles.get(role);
3364

3465
if (!roleDefinition) {
3566
return null;
3667
}
3768

3869
const requiredAttributes = Object.keys(roleDefinition.requiredProps);
39-
const missingRequiredAttributes = requiredAttributes.filter(
40-
(requiredAttribute) => !foundAriaAttributes.includes(requiredAttribute)
41-
);
70+
const missingRequiredAttributes = requiredAttributes.filter((requiredAttribute) => {
71+
if (foundAriaAttributes.includes(requiredAttribute)) {
72+
return false;
73+
}
74+
if (requiredAttribute === 'aria-checked' && isNativelyChecked(node, role)) {
75+
return false;
76+
}
77+
return true;
78+
});
4279

4380
return missingRequiredAttributes.length > 0 ? missingRequiredAttributes : null;
4481
}
@@ -93,7 +130,11 @@ module.exports = {
93130
.filter((attribute) => attribute.name?.startsWith('aria-'))
94131
.map((attribute) => attribute.name);
95132

96-
const missingRequiredAttributes = getMissingRequiredAttributes(role, foundAriaAttributes);
133+
const missingRequiredAttributes = getMissingRequiredAttributes(
134+
role,
135+
foundAriaAttributes,
136+
node
137+
);
97138

98139
if (missingRequiredAttributes) {
99140
reportMissingAttributes(node, role, missingRequiredAttributes);

tests/lib/rules/template-require-mandatory-role-attributes.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ ruleTester.run('template-require-mandatory-role-attributes', rule, {
2525
'<template>{{foo-component role="button"}}</template>',
2626
'<template>{{foo-component role="unknown"}}</template>',
2727
'<template>{{foo-component role=role}}</template>',
28+
29+
// Semantic inputs supply aria-checked natively; the role is satisfied
30+
// without an explicit aria-checked attribute. Documented accessible
31+
// patterns: https://www.w3.org/WAI/ARIA/apg/patterns/switch/#keyboardinteraction
32+
'<template><input type="checkbox" role="switch" /></template>',
33+
'<template><input type="checkbox" role="menuitemcheckbox" /></template>',
34+
'<template><input type="radio" role="menuitemradio" /></template>',
35+
'<template><input type="radio" role="radio" /></template>',
36+
'<template><input type="checkbox" role="checkbox" /></template>',
2837
],
2938

3039
invalid: [
@@ -105,6 +114,13 @@ hbsRuleTester.run('template-require-mandatory-role-attributes', rule, {
105114
'{{foo-component role="button"}}',
106115
'{{foo-component role="unknown"}}',
107116
'{{foo-component role=role}}',
117+
118+
// Semantic inputs supply aria-checked natively.
119+
'<input type="checkbox" role="switch" />',
120+
'<input type="checkbox" role="menuitemcheckbox" />',
121+
'<input type="radio" role="menuitemradio" />',
122+
'<input type="radio" role="radio" />',
123+
'<input type="checkbox" role="checkbox" />',
108124
],
109125
invalid: [
110126
{

0 commit comments

Comments
 (0)