Skip to content

Commit 0f01f92

Browse files
committed
Extract rule: template-require-valid-form-groups
1 parent d63932f commit 0f01f92

4 files changed

Lines changed: 281 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html
197197
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
198198
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
199199
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | |
200+
| [template-require-valid-form-groups](docs/rules/template-require-valid-form-groups.md) | require grouped form controls to have fieldset/legend or WAI-ARIA group labeling | | | |
200201
| [template-table-groups](docs/rules/template-table-groups.md) | require table elements to use table grouping elements | | | |
201202

202203
### Best Practices
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ember/template-require-valid-form-groups
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Require grouped form controls to have appropriate semantics.
6+
7+
When multiple form controls are related, they should be grouped with either:
8+
9+
- `<fieldset>` and `<legend>` (preferred), or
10+
- `role="group"` together with `aria-labelledby`.
11+
12+
## Examples
13+
14+
Examples of **incorrect** code for this rule:
15+
16+
```gjs
17+
<template>
18+
<div>
19+
<label for="radio-001">Chicago Zoey</label>
20+
<input id="radio-001" type="radio" name="prefMascot-Zoey" />
21+
<label for="radio-002">Chicago Tom</label>
22+
<input id="radio-002" type="radio" name="prefMascot-Tom" />
23+
</div>
24+
</template>
25+
```
26+
27+
Examples of **correct** code for this rule:
28+
29+
```gjs
30+
<template>
31+
<fieldset>
32+
<legend>Preferred Mascot Version</legend>
33+
<label for="radio-001">Chicago Zoey</label>
34+
<input id="radio-001" type="radio" name="prefMascot-Zoey" />
35+
<label for="radio-002">Chicago Tom</label>
36+
<input id="radio-002" type="radio" name="prefMascot-Tom" />
37+
</fieldset>
38+
</template>
39+
```
40+
41+
```gjs
42+
<template>
43+
<div role="group" aria-labelledby="preferred-mascot-heading">
44+
<div id="preferred-mascot-heading">Preferred Mascot Version</div>
45+
<label for="radio-001">Chicago Zoey</label>
46+
<input id="radio-001" type="radio" name="prefMascot-Zoey" />
47+
<label for="radio-002">Chicago Tom</label>
48+
<input id="radio-002" type="radio" name="prefMascot-Tom" />
49+
</div>
50+
</template>
51+
```
52+
53+
## References
54+
55+
- [Grouping Controls](https://www.w3.org/WAI/tutorials/forms/grouping/)
56+
- [The fieldset element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
const FORM_ELEMENTS = new Set(['input']);
3+
4+
function hasRoleGroup(node) {
5+
const roleAttr = node.attributes?.find((attr) => attr.name === 'role');
6+
return roleAttr && roleAttr.value?.type === 'GlimmerTextNode' && roleAttr.value.chars === 'group';
7+
}
8+
9+
function hasAriaLabel(node) {
10+
return node.attributes?.some((attr) => attr.name === 'aria-labelledby');
11+
}
12+
13+
function isValidFormGroup(node) {
14+
if (node.tag === 'fieldset' || node.tag === 'legend') {
15+
return true;
16+
}
17+
18+
return hasRoleGroup(node) && hasAriaLabel(node);
19+
}
20+
21+
function hasMultipleFormElementsInParentScope(node) {
22+
const parent = node.parent;
23+
24+
if (!parent || parent.type !== 'GlimmerElementNode') {
25+
return false;
26+
}
27+
28+
const elementChildren =
29+
parent.children?.filter((child) => child.type === 'GlimmerElementNode') || [];
30+
const formElements = elementChildren.filter((child) => FORM_ELEMENTS.has(child.tag));
31+
32+
return formElements.length > 1;
33+
}
34+
35+
function hasValidGroupingAncestor(node) {
36+
let parent = node.parent;
37+
38+
while (parent) {
39+
if (parent.type === 'GlimmerElementNode' && isValidFormGroup(parent)) {
40+
return true;
41+
}
42+
43+
parent = parent.parent;
44+
}
45+
46+
return false;
47+
}
48+
49+
module.exports = {
50+
meta: {
51+
type: 'problem',
52+
docs: {
53+
description:
54+
'require grouped form controls to have fieldset/legend or WAI-ARIA group labeling',
55+
category: 'Accessibility',
56+
recommendedGjs: false,
57+
recommendedGts: false,
58+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-valid-form-groups.md',
59+
templateMode: 'both',
60+
},
61+
schema: [],
62+
messages: {
63+
requireValidFormGroups:
64+
'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels',
65+
},
66+
originallyFrom: {
67+
name: 'ember-template-lint',
68+
rule: 'lib/rules/require-valid-form-groups.js',
69+
docs: 'docs/rule/require-valid-form-groups.md',
70+
tests: 'test/unit/rules/require-valid-form-groups-test.js',
71+
},
72+
},
73+
74+
create(context) {
75+
return {
76+
GlimmerElementNode(node) {
77+
if (!FORM_ELEMENTS.has(node.tag)) {
78+
return;
79+
}
80+
81+
if (!hasMultipleFormElementsInParentScope(node)) {
82+
return;
83+
}
84+
85+
if (hasValidGroupingAncestor(node)) {
86+
return;
87+
}
88+
89+
context.report({
90+
node,
91+
messageId: 'requireValidFormGroups',
92+
});
93+
},
94+
};
95+
},
96+
};
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
const rule = require('../../../lib/rules/template-require-valid-form-groups');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ruleTester = new RuleTester({
5+
parser: require.resolve('ember-eslint-parser'),
6+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
7+
});
8+
9+
ruleTester.run('template-require-valid-form-groups', rule, {
10+
valid: [
11+
`<template>
12+
<fieldset>
13+
<legend>Preferred Mascot Version</legend>
14+
<div>
15+
<label for="radio-001">Chicago Zoey</label>
16+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey" />
17+
</div>
18+
</fieldset>
19+
</template>`,
20+
`<template>
21+
<div role="group" aria-labelledby="preferred-mascot-heading">
22+
<div id="preferred-mascot-heading">Preferred Mascot Version</div>
23+
<label for="radio-001">Chicago Zoey</label>
24+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey" />
25+
<label for="radio-002">Chicago Tom</label>
26+
<input id="radio-002" type="radio" name="prefMascot-Tom" value="chicago zoey" />
27+
</div>
28+
</template>`,
29+
`<template>
30+
<div>
31+
<label for="radio-001">Chicago Zoey</label>
32+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey" />
33+
</div>
34+
</template>`,
35+
36+
`<template><fieldset>
37+
<legend>Preferred Mascot Version</legend>
38+
<div>
39+
<label for="radio-001">Chicago Zoey</label>
40+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey">
41+
</div>
42+
</fieldset></template>`,
43+
`<template><div role="group" aria-labelledby="preferred-mascot-heading">
44+
<div id="preferred-mascot-heading">Preferred Mascot Version</div>
45+
<label for="radio-001">Chicago Zoey</label>
46+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey">
47+
<label for="radio-002">Chicago Tom</label>
48+
<input id="radio-002" type="radio" name="prefMascot-Tom" value="chicago zoey">
49+
</div></template>`,
50+
`<template><div>
51+
<label for="radio-001">Chicago Zoey</label>
52+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey">
53+
</div></template>`,
54+
],
55+
invalid: [
56+
{
57+
code: '<template><div><input name="a1">Chicago Zoey<input name="a2">Chicago Tom</div></template>',
58+
output: null,
59+
errors: [{ messageId: 'requireValidFormGroups' }, { messageId: 'requireValidFormGroups' }],
60+
},
61+
{
62+
code: '<template><div><input id="prefMascot-Zoey"><label for="prefMascot-Zoey" /><input id="prefMascot-tom"><label for="prefMascot-tom" /></div></template>',
63+
output: null,
64+
errors: [{ messageId: 'requireValidFormGroups' }, { messageId: 'requireValidFormGroups' }],
65+
},
66+
],
67+
});
68+
69+
const hbsRuleTester = new RuleTester({
70+
parser: require.resolve('ember-eslint-parser/hbs'),
71+
parserOptions: {
72+
ecmaVersion: 2022,
73+
sourceType: 'module',
74+
},
75+
});
76+
77+
hbsRuleTester.run('template-require-valid-form-groups', rule, {
78+
valid: [
79+
`<fieldset>
80+
<legend>Preferred Mascot Version</legend>
81+
<div>
82+
<label for="radio-001">Chicago Zoey</label>
83+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey">
84+
</div>
85+
</fieldset>`,
86+
`<div role="group" aria-labelledby="preferred-mascot-heading">
87+
<div id="preferred-mascot-heading">Preferred Mascot Version</div>
88+
<label for="radio-001">Chicago Zoey</label>
89+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey">
90+
<label for="radio-002">Chicago Tom</label>
91+
<input id="radio-002" type="radio" name="prefMascot-Tom" value="chicago zoey">
92+
</div>`,
93+
`<div>
94+
<label for="radio-001">Chicago Zoey</label>
95+
<input id="radio-001" type="radio" name="prefMascot-Zoey" value="chicago zoey">
96+
</div>`,
97+
],
98+
invalid: [
99+
{
100+
code: '<div><input name="a1">Chicago Zoey<input name="a2">Chicago Tom</div>',
101+
output: null,
102+
errors: [
103+
{
104+
message:
105+
'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels',
106+
},
107+
{
108+
message:
109+
'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels',
110+
},
111+
],
112+
},
113+
{
114+
code: '<div><input id="prefMascot-Zoey"><label for="prefMascot-Zoey" /><input id="prefMascot-tom"><label for="prefMascot-tom" /></div>',
115+
output: null,
116+
errors: [
117+
{
118+
message:
119+
'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels',
120+
},
121+
{
122+
message:
123+
'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels',
124+
},
125+
],
126+
},
127+
],
128+
});

0 commit comments

Comments
 (0)