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 + +``` + +This rule **allows** the following: + +```gjs + +``` + +```gjs + +``` + +## 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: [ + ``, + ``, + ``, + + ``, + ``, + ``, + ], + 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', + }, + ], + }, + ], +});