Skip to content

Commit 5343fc5

Browse files
committed
fix: narrow input+role exemption to documented pairings
The previous exemption used a cartesian product of `{checkbox, menuitemcheckbox, menuitemradio, radio, switch}` against input types `{checkbox, radio}`, which silently accepted undocumented pairings such as `input[type=checkbox] role=radio` or `input[type=radio] role=switch`. Replace that with an explicit whitelist of `{type}:{role}` pairs: - `input[type=checkbox]` + `role=checkbox` (redundant but valid) - `input[type=checkbox]` + `role=switch` (WAI-ARIA APG Switch pattern) - `input[type=checkbox]` + `role=menuitemcheckbox` - `input[type=radio]` + `role=radio` (redundant but valid) - `input[type=radio]` + `role=menuitemradio` The whitelist matches axobject-query's `elementAXObjects` for these input-role combinations plus the WAI-ARIA APG Switch pattern (https://www.w3.org/WAI/ARIA/apg/patterns/switch/). Other combinations remain flagged for missing `aria-checked`. Also lowercase the `type` attribute value before matching; HTML `type` keywords are ASCII case-insensitive per the HTML spec, so values like `Checkbox` must be treated the same as `checkbox`.
1 parent 7e141bf commit 5343fc5

2 files changed

Lines changed: 79 additions & 14 deletions

File tree

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

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

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',
31+
// `{input type}:{role}` pairings where the required `aria-checked` is supplied
32+
// natively by the semantic <input>. Pairings match axobject-query's
33+
// `elementAXObjects` for the input-role combinations and the WAI-ARIA APG
34+
// Switch pattern (https://www.w3.org/WAI/ARIA/apg/patterns/switch/).
35+
// Undocumented pairings (e.g. `input[type=checkbox] role=radio`) are NOT
36+
// exempted.
37+
const NATIVELY_CHECKED_INPUT_ROLE_PAIRS = new Set([
38+
'checkbox:checkbox',
39+
'checkbox:switch',
40+
'checkbox:menuitemcheckbox',
41+
'radio:radio',
42+
'radio:menuitemradio',
4143
]);
4244

4345
function getInputType(node) {
@@ -46,17 +48,18 @@ function getInputType(node) {
4648
}
4749
const typeAttr = node.attributes?.find((a) => a.name === 'type');
4850
if (typeAttr?.value?.type === 'GlimmerTextNode') {
49-
return typeAttr.value.chars;
51+
// HTML input `type` keywords are ASCII case-insensitive.
52+
return typeAttr.value.chars?.toLowerCase();
5053
}
5154
return undefined;
5255
}
5356

5457
function isNativelyChecked(node, role) {
55-
if (!ROLES_WITH_IMPLICIT_CHECKED_FROM_INPUT.has(role)) {
58+
const type = getInputType(node);
59+
if (!type) {
5660
return false;
5761
}
58-
const type = getInputType(node);
59-
return type === 'checkbox' || type === 'radio';
62+
return NATIVELY_CHECKED_INPUT_ROLE_PAIRS.has(`${type}:${role}`);
6063
}
6164

6265
function getMissingRequiredAttributes(role, foundAriaAttributes, node) {

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ ruleTester.run('template-require-mandatory-role-attributes', rule, {
3434
'<template><input type="radio" role="menuitemradio" /></template>',
3535
'<template><input type="radio" role="radio" /></template>',
3636
'<template><input type="checkbox" role="checkbox" /></template>',
37+
38+
// HTML `type` is ASCII case-insensitive; `Checkbox` must match `checkbox`.
39+
'<template><input type="Checkbox" role="switch" /></template>',
3740
],
3841

3942
invalid: [
@@ -84,6 +87,34 @@ ruleTester.run('template-require-mandatory-role-attributes', rule, {
8487
output: null,
8588
errors: [{ message: 'The attribute aria-checked is required by the role checkbox' }],
8689
},
90+
91+
// Undocumented {input type, role} pairings are NOT exempted.
92+
{
93+
code: '<template><input type="checkbox" role="radio" /></template>',
94+
output: null,
95+
errors: [{ message: 'The attribute aria-checked is required by the role radio' }],
96+
},
97+
{
98+
code: '<template><input type="radio" role="switch" /></template>',
99+
output: null,
100+
errors: [{ message: 'The attribute aria-checked is required by the role switch' }],
101+
},
102+
{
103+
code: '<template><input type="radio" role="checkbox" /></template>',
104+
output: null,
105+
errors: [{ message: 'The attribute aria-checked is required by the role checkbox' }],
106+
},
107+
{
108+
code: '<template><input type="text" role="switch" /></template>',
109+
output: null,
110+
errors: [{ message: 'The attribute aria-checked is required by the role switch' }],
111+
},
112+
{
113+
// No `type` attribute; defaults to text.
114+
code: '<template><input role="switch" /></template>',
115+
output: null,
116+
errors: [{ message: 'The attribute aria-checked is required by the role switch' }],
117+
},
87118
],
88119
});
89120

@@ -121,6 +152,9 @@ hbsRuleTester.run('template-require-mandatory-role-attributes', rule, {
121152
'<input type="radio" role="menuitemradio" />',
122153
'<input type="radio" role="radio" />',
123154
'<input type="checkbox" role="checkbox" />',
155+
156+
// HTML `type` is ASCII case-insensitive; `Checkbox` must match `checkbox`.
157+
'<input type="Checkbox" role="switch" />',
124158
],
125159
invalid: [
126160
{
@@ -162,5 +196,33 @@ hbsRuleTester.run('template-require-mandatory-role-attributes', rule, {
162196
output: null,
163197
errors: [{ message: 'The attribute aria-checked is required by the role checkbox' }],
164198
},
199+
200+
// Undocumented {input type, role} pairings are NOT exempted.
201+
{
202+
code: '<input type="checkbox" role="radio" />',
203+
output: null,
204+
errors: [{ message: 'The attribute aria-checked is required by the role radio' }],
205+
},
206+
{
207+
code: '<input type="radio" role="switch" />',
208+
output: null,
209+
errors: [{ message: 'The attribute aria-checked is required by the role switch' }],
210+
},
211+
{
212+
code: '<input type="radio" role="checkbox" />',
213+
output: null,
214+
errors: [{ message: 'The attribute aria-checked is required by the role checkbox' }],
215+
},
216+
{
217+
code: '<input type="text" role="switch" />',
218+
output: null,
219+
errors: [{ message: 'The attribute aria-checked is required by the role switch' }],
220+
},
221+
{
222+
// No `type` attribute; defaults to text.
223+
code: '<input role="switch" />',
224+
output: null,
225+
errors: [{ message: 'The attribute aria-checked is required by the role switch' }],
226+
},
165227
],
166228
});

0 commit comments

Comments
 (0)