From 0f01f92a8e3471608f5654444519d3991463232a Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:17:58 -0400 Subject: [PATCH 1/2] Extract rule: template-require-valid-form-groups --- README.md | 1 + .../template-require-valid-form-groups.md | 56 ++++++++ .../template-require-valid-form-groups.js | 96 +++++++++++++ .../template-require-valid-form-groups.js | 128 ++++++++++++++++++ 4 files changed, 281 insertions(+) create mode 100644 docs/rules/template-require-valid-form-groups.md create mode 100644 lib/rules/template-require-valid-form-groups.js create mode 100644 tests/lib/rules/template-require-valid-form-groups.js diff --git a/README.md b/README.md index 14120af108..8cbc63494a 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html | [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | | | [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | | | [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | | +| [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 | | | | | [template-table-groups](docs/rules/template-table-groups.md) | require table elements to use table grouping elements | | | | ### Best Practices diff --git a/docs/rules/template-require-valid-form-groups.md b/docs/rules/template-require-valid-form-groups.md new file mode 100644 index 0000000000..7bc1f70eb6 --- /dev/null +++ b/docs/rules/template-require-valid-form-groups.md @@ -0,0 +1,56 @@ +# ember/template-require-valid-form-groups + + + +Require grouped form controls to have appropriate semantics. + +When multiple form controls are related, they should be grouped with either: + +- `
` and `` (preferred), or +- `role="group"` together with `aria-labelledby`. + +## Examples + +Examples of **incorrect** code for this rule: + +```gjs + +``` + +Examples of **correct** code for this rule: + +```gjs + +``` + +```gjs + +``` + +## References + +- [Grouping Controls](https://www.w3.org/WAI/tutorials/forms/grouping/) +- [The fieldset element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset) diff --git a/lib/rules/template-require-valid-form-groups.js b/lib/rules/template-require-valid-form-groups.js new file mode 100644 index 0000000000..ce074166c1 --- /dev/null +++ b/lib/rules/template-require-valid-form-groups.js @@ -0,0 +1,96 @@ +/** @type {import('eslint').Rule.RuleModule} */ +const FORM_ELEMENTS = new Set(['input']); + +function hasRoleGroup(node) { + const roleAttr = node.attributes?.find((attr) => attr.name === 'role'); + return roleAttr && roleAttr.value?.type === 'GlimmerTextNode' && roleAttr.value.chars === 'group'; +} + +function hasAriaLabel(node) { + return node.attributes?.some((attr) => attr.name === 'aria-labelledby'); +} + +function isValidFormGroup(node) { + if (node.tag === 'fieldset' || node.tag === 'legend') { + return true; + } + + return hasRoleGroup(node) && hasAriaLabel(node); +} + +function hasMultipleFormElementsInParentScope(node) { + const parent = node.parent; + + if (!parent || parent.type !== 'GlimmerElementNode') { + return false; + } + + const elementChildren = + parent.children?.filter((child) => child.type === 'GlimmerElementNode') || []; + const formElements = elementChildren.filter((child) => FORM_ELEMENTS.has(child.tag)); + + return formElements.length > 1; +} + +function hasValidGroupingAncestor(node) { + let parent = node.parent; + + while (parent) { + if (parent.type === 'GlimmerElementNode' && isValidFormGroup(parent)) { + return true; + } + + parent = parent.parent; + } + + return false; +} + +module.exports = { + meta: { + type: 'problem', + docs: { + description: + 'require grouped form controls to have fieldset/legend or WAI-ARIA group labeling', + category: 'Accessibility', + recommendedGjs: false, + recommendedGts: false, + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-valid-form-groups.md', + templateMode: 'both', + }, + schema: [], + messages: { + requireValidFormGroups: + 'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels', + }, + originallyFrom: { + name: 'ember-template-lint', + rule: 'lib/rules/require-valid-form-groups.js', + docs: 'docs/rule/require-valid-form-groups.md', + tests: 'test/unit/rules/require-valid-form-groups-test.js', + }, + }, + + create(context) { + return { + GlimmerElementNode(node) { + if (!FORM_ELEMENTS.has(node.tag)) { + return; + } + + if (!hasMultipleFormElementsInParentScope(node)) { + return; + } + + if (hasValidGroupingAncestor(node)) { + return; + } + + context.report({ + node, + messageId: 'requireValidFormGroups', + }); + }, + }; + }, +}; diff --git a/tests/lib/rules/template-require-valid-form-groups.js b/tests/lib/rules/template-require-valid-form-groups.js new file mode 100644 index 0000000000..86c09735bb --- /dev/null +++ b/tests/lib/rules/template-require-valid-form-groups.js @@ -0,0 +1,128 @@ +const rule = require('../../../lib/rules/template-require-valid-form-groups'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +ruleTester.run('template-require-valid-form-groups', rule, { + valid: [ + ``, + ``, + ``, + + ``, + ``, + ``, + ], + invalid: [ + { + code: '', + output: null, + errors: [{ messageId: 'requireValidFormGroups' }, { messageId: 'requireValidFormGroups' }], + }, + { + code: '', + output: null, + errors: [{ messageId: 'requireValidFormGroups' }, { messageId: 'requireValidFormGroups' }], + }, + ], +}); + +const hbsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser/hbs'), + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +hbsRuleTester.run('template-require-valid-form-groups', rule, { + valid: [ + `
+ Preferred Mascot Version +
+ + +
+
`, + `
+
Preferred Mascot Version
+ + + + +
`, + `
+ + +
`, + ], + invalid: [ + { + code: '
Chicago ZoeyChicago Tom
', + output: null, + errors: [ + { + message: + 'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels', + }, + { + message: + 'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels', + }, + ], + }, + { + code: '
', + output: null, + errors: [ + { + message: + 'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels', + }, + { + message: + 'Grouped form controls should have appropriate semantics such as fieldset and legend or WAI-ARIA labels', + }, + ], + }, + ], +}); From 747a6269ca3205c030ede9d8e9cd8d76a1a4e8bd Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:25:24 -0400 Subject: [PATCH 2/2] Sync with template-lint --- .../template-require-valid-form-groups.md | 57 ++++++++++++------- .../template-require-valid-form-groups.js | 52 +++++++++++++++-- 2 files changed, 81 insertions(+), 28 deletions(-) diff --git a/docs/rules/template-require-valid-form-groups.md b/docs/rules/template-require-valid-form-groups.md index 7bc1f70eb6..3ee8551be1 100644 --- a/docs/rules/template-require-valid-form-groups.md +++ b/docs/rules/template-require-valid-form-groups.md @@ -4,53 +4,66 @@ Require grouped form controls to have appropriate semantics. -When multiple form controls are related, they should be grouped with either: +This rule requires appropriate semantics for grouped form controls. Correctly grouped +form controls will take one of two approaches: -- `
` and `` (preferred), or -- `role="group"` together with `aria-labelledby`. +- use `
` + `` (preferred) +- associate controls using WAI-ARIA (also acceptable) ## Examples -Examples of **incorrect** code for this rule: +This rule **forbids** the following: ```gjs ``` -Examples of **correct** code for this rule: +This rule **allows** the following: ```gjs ``` ```gjs ``` ## References - [Grouping Controls](https://www.w3.org/WAI/tutorials/forms/grouping/) -- [The fieldset element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset) +- [The Field Set element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset) diff --git a/tests/lib/rules/template-require-valid-form-groups.js b/tests/lib/rules/template-require-valid-form-groups.js index 86c09735bb..7f7427201f 100644 --- a/tests/lib/rules/template-require-valid-form-groups.js +++ b/tests/lib/rules/template-require-valid-form-groups.js @@ -15,6 +15,19 @@ ruleTester.run('template-require-valid-form-groups', rule, { +
+ + +
+
+ + +
`, ``, ``, ``, `