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..3ee8551be1
--- /dev/null
+++ b/docs/rules/template-require-valid-form-groups.md
@@ -0,0 +1,69 @@
+# ember/template-require-valid-form-groups
+
+
+
+Require grouped form controls to have appropriate semantics.
+
+This rule requires appropriate semantics for grouped form controls. Correctly grouped
+form controls will take one of two approaches:
+
+- use `
` + `` (preferred)
+- associate controls using WAI-ARIA (also acceptable)
+
+## Examples
+
+This rule **forbids** the following:
+
+```gjs
+
+
+ Chicago Zoey
+
+ Office Hours Tomster
+
+ A11y Zoey
+
+
+
+```
+
+This rule **allows** the following:
+
+```gjs
+
+
+
Preferred Mascot Version
+
Chicago Zoey
+
+
Office Hours Tomster
+
+
A11y Zoey
+
+
+
+```
+
+```gjs
+
+
+ Preferred Mascot Version
+
+ Chicago Zoey
+
+
+
+ Office Hours Tomster
+
+
+
+ A11y Zoey
+
+
+
+
+```
+
+## References
+
+- [Grouping Controls](https://www.w3.org/WAI/tutorials/forms/grouping/)
+- [The Field Set 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..7f7427201f
--- /dev/null
+++ b/tests/lib/rules/template-require-valid-form-groups.js
@@ -0,0 +1,168 @@
+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: [
+ `
+
+ Preferred Mascot Version
+
+ Chicago Zoey
+
+
+
+ Office Hours Tomster
+
+
+
+ A11y Zoey
+
+
+
+ `,
+ `
+
+
Preferred Mascot Version
+
Chicago Zoey
+
+
Office Hours Tomster
+
+
A11y Zoey
+
+
+ `,
+ `
+
+ Chicago Zoey
+
+
+ `,
+
+ `
+ Preferred Mascot Version
+
+ Chicago Zoey
+
+
+
+ Office Hours Tomster
+
+
+
+ A11y Zoey
+
+
+ `,
+ `
+
Preferred Mascot Version
+
Chicago Zoey
+
+
Office Hours Tomster
+
+
A11y Zoey
+
+
`,
+ `
+ Chicago Zoey
+
+
`,
+ ],
+ invalid: [
+ {
+ code: ' Chicago Zoey Chicago Tom
',
+ 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
+
+ Chicago Zoey
+
+
+
+ Office Hours Tomster
+
+
+
+ A11y Zoey
+
+
+ `,
+ `
+
Preferred Mascot Version
+
Chicago Zoey
+
+
Office Hours Tomster
+
+
A11y Zoey
+
+
`,
+ `
+ Chicago Zoey
+
+
`,
+ ],
+ invalid: [
+ {
+ code: ' Chicago Zoey Chicago 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',
+ },
+ ],
+ },
+ ],
+});