Skip to content

fix: template-no-invalid-interactive — honor role=presentation/none and aria-hidden#2754

Draft
johanrd wants to merge 2 commits intoember-cli:masterfrom
johanrd:fix/escape-hatch-in-invalid-interactive
Draft

fix: template-no-invalid-interactive — honor role=presentation/none and aria-hidden#2754
johanrd wants to merge 2 commits intoember-cli:masterfrom
johanrd:fix/escape-hatch-in-invalid-interactive

Conversation

@johanrd
Copy link
Copy Markdown
Contributor

@johanrd johanrd commented Apr 27, 2026

Note

This is part of a series where Claude has audited eslint-plugin-ember against jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate, ember-template-lint, and the HTML and WCAG specs.

Premise

Peer accessibility plugins honor two ARIA signals as opt-outs of the "interactive handler on non-interactive element" check:

Flagging these is a false positive. template-no-invalid-interactive now short-circuits on an explicit opt-out before the native/role-based interactivity probe runs.

Helper — hasNonInteractiveEscapeHatch(node)

Returns true iff the element carries any of:

  • role=\"presentation\" or role=\"none\" — case-insensitive, trimmed, first token of a space-separated role list. (Note: jsx-a11y's isPresentationRole does plain exact-match — our first-token handling is a deliberate superset, matching ARIA 1.2 §5.4 role-fallback semantics.)
  • aria-hidden in any plausibly-"hide" form — valueless, empty, \"true\" (case-insensitive), {{true}}, {{\"true\"}}.

Explicit aria-hidden=\"false\" / {{false}} still flags — it's the unambiguous opt-in.

Four ecosystem positions on valueless aria-hidden

The question "what does <div aria-hidden> (bare), aria-hidden=\"\" (empty), or aria-hidden={{false}} mean?" has no single authoritative answer. Four defensible positions exist:

# Source Interpretation Evidence
1 jsx-a11y Valueless → hidden Side effect of jsx-ast-utils coercing valueless JSX attrs to boolean true. Quirk: string aria-hidden=\"true\" is NOT recognized because \"true\" !== true. Not a deliberate ARIA interpretation.
2 vue-a11y Anything not literal \"false\" → hidden isHiddenFromScreenReader.ts: (value || \"\").toString() !== \"false\". Non-spec shortcut.
3 axe-core / W3C ACT Rules Valueless/empty → INCOMPLETE, needs author review axe-core PR #3635; W3C ACT Rule 6a7281 scopes out empty values as inapplicable.
4 WAI-ARIA 1.2 spec Valueless/empty → default undefined → not hidden §aria-hidden value table: value type true/false/undefined (default). Missing/empty resolves to default.

Design choice

This rule leans toward fewer false positives. For this rule, that means treating valueless / empty aria-hidden as an escape hatch — flagging a click handler on an author-decorated element creates friction more often than it catches a real bug. The author wrote aria-hidden for a reason; trust the signal.

Prior art

Plugin Rule Verified behavior
jsx-a11y no-static-element-interactions / no-noninteractive-element-interactions Short-circuits on isPresentationRole() and isHiddenFromScreenReader() before the static-element check.
vuejs-accessibility no-static-element-interactions Accepts role=\"presentation\" and aria-hidden=\"true\" on clickable elements as VALID.
@angular-eslint/template click-events-have-key-events Short-circuits on isPresentationRole(node) and isHiddenFromScreenReader(node) before the click-handler check.
lit-a11y No no-static-element-interactions-style rule; click-events-have-key-events.js does not honor role=\"presentation\" or aria-hidden.

Tests

16 new valid cases (presentation / none + every plausibly-"hide" aria-hidden form); 3 new invalid cases confirming aria-hidden=\"false\" / {{false}} are NOT opt-outs.

@johanrd johanrd marked this pull request as draft April 27, 2026 20:11
…en caveat in docs, fix isPresentationRole filename refs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant