Skip to content

Commit c2fce7e

Browse files
committed
docs+tests: fix comment/doc accuracy for escape-hatch rule (Copilot review)
1 parent 206f639 commit c2fce7e

3 files changed

Lines changed: 17 additions & 12 deletions

File tree

docs/rules/template-no-invalid-interactive.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ Examples of **correct** code for this rule:
5151
An element opts out of this rule's handler-on-non-interactive check in two cases:
5252

5353
- **`aria-hidden="true"`** (including valueless / empty / `{{true}}` / `{{"true"}}` forms) — the author has explicitly removed the element from the accessibility tree, so a "non-interactive element with handler" is moot; AT users won't encounter it either way. Explicit `aria-hidden="false"` / `{{false}}` still flags.
54-
- **`role="presentation"` / `role="none"`** — the author asserts the element is decorative. Matches [jsx-a11y's `isPresentationRole`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/util/hasPresentationRole.js) behavior.
54+
- **`role="presentation"` / `role="none"`** — the author asserts the element is decorative. We accept `role="presentation"` and `role="none"` (case-insensitive, first-token of a space-separated role list) — a deliberate superset of [jsx-a11y's exact-match `isPresentationRole`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/util/hasPresentationRole.js) for consistency with [WAI-ARIA 1.2 §4.1](https://www.w3.org/TR/wai-aria-1.2/#host_general_role) role-fallback semantics.
5555

5656
The valueless `aria-hidden` case (e.g. `<div aria-hidden>`) is [genuinely contested](https://www.scottohara.me/blog/2018/05/05/hidden-vs-none.html) — jsx-a11y, vue-a11y, axe-core, and the WAI-ARIA spec take four different positions on whether it counts as "hidden". This rule leans toward fewer false positives: flagging a handler on an author-decorated element creates friction more often than it catches real bugs.
5757

5858
### Related checks outside this rule's scope
5959

60-
- **`role="presentation"` on focusable elements** — per [WAI-ARIA 1.2 §4.6 Conflict Resolution](https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none), browsers ignore `role="presentation"` on focusable elements. [axe-core's `presentation-role-conflict`](https://dequeuniversity.com/rules/axe/4.10/presentation-role-conflict) flags this pattern as an authoring error. This rule's escape hatch does NOT mask the conflict`isInteractive(node)` catches focusable elements (anchors with href, inputs, elements with tabindex, etc.) via the interactive-content check before the escape hatch matters, so handler-on-focusable-interactive cases aren't silenced here. If you want to catch the authoring error (role=presentation on a focusable element, which has no effect), layer axe-core or a dedicated rule on top.
60+
- **`role="presentation"` on focusable elements** — per [WAI-ARIA 1.2 §4.6 Conflict Resolution](https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none), browsers ignore `role="presentation"` on focusable elements. [axe-core's `presentation-role-conflict`](https://dequeuniversity.com/rules/axe/4.10/presentation-role-conflict) flags this pattern as an authoring error. This rule does not detect the conflict: the escape hatch check runs before `isInteractive(node)`, so a focusable element with `role="presentation"` returns early and is silently exempted. Interactive-handler cases on plain `<button>` / `<a href>` etc. are still flagged normally when they lack the presentation/none opt-out. If you want to catch the authoring error itself (role=presentation on a focusable element, which has no effect at runtime), layer axe-core or a dedicated rule on top.
6161
- **Click handler on a non-focusable decorated element** — e.g. `<div role="presentation" {{on "click"}}>`. Our escape hatch silences this by design (jsx-a11y-compat). [axe-core's `click-events-have-key-events`](https://dequeuniversity.com/rules/axe/4.10/click-events-have-key-events) is the complementary check. If you want strictness, layer it on top.
6262

6363
## Options

tests/audit/no-static-element-interactions/peer-parity.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
// Audit fixture — peer-plugin parity for `ember/template-no-invalid-interactive`.
1+
// Audit fixture — translates peer-plugin test cases into assertions against
2+
// our rule. Runs as part of the default Vitest suite (via the `tests/**/*.js`
3+
// include glob) and serves double-duty: (1) auditable record of peer-parity
4+
// divergences, (2) regression coverage pinning CURRENT behavior. Each case
5+
// encodes what OUR rule does today; divergences from upstream plugins are
6+
// annotated as `DIVERGENCE —`. Peer-only constructs that can't be translated
7+
// to Ember templates (JSX spread props, Vue v-bind, Angular `$event`,
8+
// undefined-handler expression analysis) are marked `AUDIT-SKIP`.
29
//
3-
// Our rule covers the combined concerns of TWO jsx-a11y rules:
10+
// Our rule (`ember/template-no-invalid-interactive`) covers the combined
11+
// concerns of TWO jsx-a11y rules:
412
// - `no-static-element-interactions` (div/span/etc. + onClick, no role)
513
// - `no-noninteractive-element-interactions` (article/p/main/etc. + onClick)
614
// plus the single vue rule `no-static-element-interactions`.
715
//
8-
// This file does NOT run in CI; it encodes CURRENT behavior of our rule so
9-
// that executing it reports pass. Each case is annotated with the peer rule
10-
// it was translated from and any divergence.
11-
//
1216
// Source files (context/ checkouts):
1317
// - eslint-plugin-jsx-a11y-main/__tests__/src/rules/no-static-element-interactions-test.js
1418
// - eslint-plugin-jsx-a11y-main/__tests__/src/rules/no-noninteractive-element-interactions-test.js
@@ -117,9 +121,10 @@ ruleTester.run('audit:no-invalid-interactive (gts)', rule, {
117121
'<template><img onerror={{this.h}} alt="x" /></template>',
118122

119123
// <form onSubmit> — parity as above.
120-
// <iframe onLoad> — jsx-a11y no-noninteractive: alwaysValid.
121-
// DIVERGENCE (noted in invalid section): our rule does NOT allow onload on iframe;
122-
// ELEMENT_ALLOWED_EVENTS only covers form + img. See invalid-section cross-ref.
124+
// <iframe onLoad> — jsx-a11y no-noninteractive: alwaysValid. PARITY.
125+
// Ours: `iframe` is in NATIVE_INTERACTIVE_ELEMENTS, so isInteractive()
126+
// returns true and the rule early-returns. See invalid-section cross-ref
127+
// at line 517 for the full parity assertion.
123128

124129
// =========================================================================
125130
// Bucket C — vue/no-static-element-interactions (valid)

tests/lib/rules/template-no-invalid-interactive.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ ruleTester.run('template-no-invalid-interactive', rule, {
234234
],
235235
},
236236
{
237-
// Explicit `aria-hidden="false"` is the unambiguous opt-out — still flags.
237+
// aria-hidden="false" is opt-in to exposure — rule still flags non-interactive + handler.
238238
code: '<template><div aria-hidden="false" onclick={{this.h}}></div></template>',
239239
output: null,
240240
errors: [{ messageId: 'noInvalidInteractive' }],

0 commit comments

Comments
 (0)