From 89a6f1db7ca2ad2c792e6d9fb8ac57c5fbbc905d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Mon, 27 Apr 2026 21:26:30 +0200 Subject: [PATCH 1/2] feat: add template-no-noninteractive-element-to-interactive-role --- README.md | 69 +++---- ...interactive-element-to-interactive-role.md | 49 +++++ ...interactive-element-to-interactive-role.js | 188 ++++++++++++++++++ lib/utils/interactive-roles.js | 32 +-- ...interactive-element-to-interactive-role.js | 184 +++++++++++++++++ 5 files changed, 462 insertions(+), 60 deletions(-) create mode 100644 docs/rules/template-no-noninteractive-element-to-interactive-role.md create mode 100644 lib/rules/template-no-noninteractive-element-to-interactive-role.js create mode 100644 tests/lib/rules/template-no-noninteractive-element-to-interactive-role.js diff --git a/README.md b/README.md index 406ed89301..9001e0437d 100644 --- a/README.md +++ b/README.md @@ -256,40 +256,41 @@ To disable a rule for an entire `.gjs`/`.gts` file, use a regular ESLint file-le ### Accessibility -| Name                                            | Description | 💼 | 🔧 | 💡 | -| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- | :- | :- | :- | -| [template-link-href-attributes](docs/rules/template-link-href-attributes.md) | require href attribute on link elements | 📋 | | | -| [template-no-abstract-roles](docs/rules/template-no-abstract-roles.md) | disallow abstract ARIA roles | 📋 | | | -| [template-no-accesskey-attribute](docs/rules/template-no-accesskey-attribute.md) | disallow accesskey attribute | 📋 | 🔧 | | -| [template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md) | disallow aria-hidden on body element | 📋 | 🔧 | | -| [template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md) | disallow ARIA roles, states, and properties on elements that do not support them | 📋 | | | -| [template-no-autofocus-attribute](docs/rules/template-no-autofocus-attribute.md) | disallow autofocus attribute | 📋 | 🔧 | | -| [template-no-duplicate-landmark-elements](docs/rules/template-no-duplicate-landmark-elements.md) | disallow duplicate landmark elements without unique labels | 📋 | | | -| [template-no-empty-headings](docs/rules/template-no-empty-headings.md) | disallow empty heading elements | 📋 | | | -| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | 📋 | | | -| [template-no-invalid-aria-attributes](docs/rules/template-no-invalid-aria-attributes.md) | disallow invalid aria-* attributes | 📋 | | | -| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | 📋 | | | -| [template-no-invalid-link-text](docs/rules/template-no-invalid-link-text.md) | disallow invalid or uninformative link text content | 📋 | | | -| [template-no-invalid-link-title](docs/rules/template-no-invalid-link-title.md) | disallow invalid title attributes on link elements | 📋 | | | -| [template-no-invalid-role](docs/rules/template-no-invalid-role.md) | disallow invalid ARIA roles | 📋 | | | -| [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-no-positive-tabindex](docs/rules/template-no-positive-tabindex.md) | disallow positive tabindex values | 📋 | | | -| [template-no-redundant-role](docs/rules/template-no-redundant-role.md) | disallow redundant role attributes | 📋 | 🔧 | | -| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | 📋 | 🔧 | | -| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | 📋 | | | -| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | 📋 | 🔧 | | -| [template-require-context-role](docs/rules/template-require-context-role.md) | require ARIA roles to be used in appropriate context | 📋 | | | -| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | 📋 | | | -| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | 📋 | | | -| [template-require-lang-attribute](docs/rules/template-require-lang-attribute.md) | require lang attribute on html element | 📋 | | | -| [template-require-mandatory-role-attributes](docs/rules/template-require-mandatory-role-attributes.md) | require mandatory ARIA attributes for ARIA roles | 📋 | | | -| [template-require-media-caption](docs/rules/template-require-media-caption.md) | require captions for audio and video elements | 📋 | | | -| [template-require-presentational-children](docs/rules/template-require-presentational-children.md) | require presentational elements to only contain presentational children | 📋 | | | -| [template-require-valid-alt-text](docs/rules/template-require-valid-alt-text.md) | require valid alt text for images and other elements | 📋 | | | -| [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 | 📋 | | | +| Name                                                   | Description | 💼 | 🔧 | 💡 | +| :----------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- | :- | :- | :- | +| [template-link-href-attributes](docs/rules/template-link-href-attributes.md) | require href attribute on link elements | 📋 | | | +| [template-no-abstract-roles](docs/rules/template-no-abstract-roles.md) | disallow abstract ARIA roles | 📋 | | | +| [template-no-accesskey-attribute](docs/rules/template-no-accesskey-attribute.md) | disallow accesskey attribute | 📋 | 🔧 | | +| [template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md) | disallow aria-hidden on body element | 📋 | 🔧 | | +| [template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md) | disallow ARIA roles, states, and properties on elements that do not support them | 📋 | | | +| [template-no-autofocus-attribute](docs/rules/template-no-autofocus-attribute.md) | disallow autofocus attribute | 📋 | 🔧 | | +| [template-no-duplicate-landmark-elements](docs/rules/template-no-duplicate-landmark-elements.md) | disallow duplicate landmark elements without unique labels | 📋 | | | +| [template-no-empty-headings](docs/rules/template-no-empty-headings.md) | disallow empty heading elements | 📋 | | | +| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | 📋 | | | +| [template-no-invalid-aria-attributes](docs/rules/template-no-invalid-aria-attributes.md) | disallow invalid aria-* attributes | 📋 | | | +| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | 📋 | | | +| [template-no-invalid-link-text](docs/rules/template-no-invalid-link-text.md) | disallow invalid or uninformative link text content | 📋 | | | +| [template-no-invalid-link-title](docs/rules/template-no-invalid-link-title.md) | disallow invalid title attributes on link elements | 📋 | | | +| [template-no-invalid-role](docs/rules/template-no-invalid-role.md) | disallow invalid ARIA roles | 📋 | | | +| [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-noninteractive-element-to-interactive-role](docs/rules/template-no-noninteractive-element-to-interactive-role.md) | disallow non-interactive elements from being assigned interactive ARIA roles | | | | +| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | 📋 | | | +| [template-no-positive-tabindex](docs/rules/template-no-positive-tabindex.md) | disallow positive tabindex values | 📋 | | | +| [template-no-redundant-role](docs/rules/template-no-redundant-role.md) | disallow redundant role attributes | 📋 | 🔧 | | +| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | 📋 | 🔧 | | +| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | 📋 | | | +| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | 📋 | 🔧 | | +| [template-require-context-role](docs/rules/template-require-context-role.md) | require ARIA roles to be used in appropriate context | 📋 | | | +| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | 📋 | | | +| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | 📋 | | | +| [template-require-lang-attribute](docs/rules/template-require-lang-attribute.md) | require lang attribute on html element | 📋 | | | +| [template-require-mandatory-role-attributes](docs/rules/template-require-mandatory-role-attributes.md) | require mandatory ARIA attributes for ARIA roles | 📋 | | | +| [template-require-media-caption](docs/rules/template-require-media-caption.md) | require captions for audio and video elements | 📋 | | | +| [template-require-presentational-children](docs/rules/template-require-presentational-children.md) | require presentational elements to only contain presentational children | 📋 | | | +| [template-require-valid-alt-text](docs/rules/template-require-valid-alt-text.md) | require valid alt text for images and other elements | 📋 | | | +| [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-no-noninteractive-element-to-interactive-role.md b/docs/rules/template-no-noninteractive-element-to-interactive-role.md new file mode 100644 index 0000000000..b65f0f2a18 --- /dev/null +++ b/docs/rules/template-no-noninteractive-element-to-interactive-role.md @@ -0,0 +1,49 @@ +# ember/template-no-noninteractive-element-to-interactive-role + + + +Disallow non-interactive HTML elements from being assigned interactive ARIA roles. + +Assigning an interactive role (`button`, `checkbox`, `menuitem`, ...) to an element with inherent non-interactive semantics (headings, landmarks, text structure, lists, tables, forms) creates a widget with no supporting behavior — focus, keyboard activation, and state handling must all be added manually, and the mismatch is easy to get wrong. + +## Scope + +The set of non-interactive elements is sourced from [`axobject-query`](https://github.com/A11yance/axobject-query) — the same AX-tree-derived data used by [`eslint-plugin-jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y), [`@angular-eslint/eslint-plugin-template`](https://github.com/angular-eslint/angular-eslint), and others. It includes headings (`

`–`

`), landmarks (`
`, `