Skip to content

Commit 642efb5

Browse files
committed
fix(template-no-invalid-interactive): align escape-hatch with ARIA spec + correct peer-plugin claims
Two corrections to the previous revision: 1. Valueless / empty-string `aria-hidden` is no longer treated as a non-interactive escape hatch. Per WAI-ARIA 1.2 §6.6 + aria-hidden value table, a missing or empty-string value resolves to the default `undefined` — NOT `true`. Only an explicit `aria-hidden="true"` (ASCII case-insensitive) or mustache-literal `{{true}}` opts out. This matches ember-cli#2717 / #19's spec-first resolution. 2. Code comment corrections. jsx-a11y's util is named `isPresentationRole`, not `hasPresentationRole`. The comment also claimed jsx-a11y's `isPresentationRole` does "first token of a space-separated role list" — it does not (jsx-a11y does plain `presentationRoles.has(rawValue)`, no trim/lowercase/split). Our first-token behavior is a deliberate superset, not parity. Moved `<div aria-hidden onclick>` and `<div aria-hidden="" onclick>` from the valid section to invalid. Added `<div aria-hidden="TRUE">` as additional valid coverage for the case-insensitive path.
1 parent 4e54def commit 642efb5

2 files changed

Lines changed: 38 additions & 21 deletions

File tree

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,19 @@ function isMustacheLiteralTrue(attr) {
2727
}
2828

2929
// Does this element carry a non-interactive escape hatch that peer a11y rules
30-
// (jsx-a11y, vuejs-accessibility) honor as an opt-out of interactivity checks?
31-
// - role="presentation" / role="none" (case-insensitive, trimmed; first token
32-
// of a space-separated role list, matching jsx-a11y's `hasPresentationRole`
33-
// / ARIA's role fallback semantics).
34-
// - aria-hidden as a boolean attribute (bare), as the literal string "true",
35-
// or as the mustache-literal `{{true}}`. aria-hidden="false" does NOT
36-
// qualify.
30+
// honor as an opt-out of interactivity checks?
31+
// - role="presentation" or role="none": the author has asserted the element
32+
// is decorative. Per WAI-ARIA 1.2 §4.6, user agents MUST ignore these
33+
// roles on focusable elements and expose the implicit role instead; but
34+
// for non-focusable static elements with handlers, the author's intent
35+
// to opt out of a11y semantics is what this rule honors. This rule
36+
// accepts the first token of a space-separated role list (a superset of
37+
// jsx-a11y's `isPresentationRole`, which does exact match).
38+
// - aria-hidden="true" (ASCII case-insensitive) or mustache-literal
39+
// `{{true}}`. Per WAI-ARIA 1.2 §6.6 + aria-hidden value table, valueless
40+
// and empty-string aria-hidden resolve to default `undefined` — NOT true
41+
// — so bare `<div aria-hidden>` does NOT qualify. Only an explicit true
42+
// opts out.
3743
function hasNonInteractiveEscapeHatch(node) {
3844
const roleAttr = findAttr(node, 'role');
3945
if (roleAttr?.value?.type === 'GlimmerTextNode') {
@@ -44,18 +50,14 @@ function hasNonInteractiveEscapeHatch(node) {
4450
}
4551

4652
const ariaHidden = findAttr(node, 'aria-hidden');
47-
if (ariaHidden) {
48-
// Bare `aria-hidden` (no value) is stored with an empty-chars GlimmerTextNode.
49-
if (ariaHidden.value?.type === 'GlimmerTextNode') {
50-
const chars = ariaHidden.value.chars;
51-
if (chars === '' || chars.trim().toLowerCase() === 'true') {
52-
return true;
53-
}
54-
}
55-
if (isMustacheLiteralTrue(ariaHidden)) {
53+
if (ariaHidden?.value?.type === 'GlimmerTextNode') {
54+
if (ariaHidden.value.chars.trim().toLowerCase() === 'true') {
5655
return true;
5756
}
5857
}
58+
if (isMustacheLiteralTrue(ariaHidden)) {
59+
return true;
60+
}
5961

6062
return false;
6163
}

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,20 @@ ruleTester.run('template-no-invalid-interactive', rule, {
107107
options: [{ ignoredTags: ['div'] }],
108108
},
109109

110-
// Non-interactive escape hatches — `role="presentation"` / `role="none"`
111-
// / `aria-hidden` opt the element out of interactive-role semantics.
112-
// Matches jsx-a11y (`hasPresentationRole`, aria-hidden handling in
113-
// `isInteractiveElement`) and vuejs-accessibility.
110+
// Non-interactive escape hatches:
111+
// - role="presentation"/"none" (per WAI-ARIA 1.2 §4.6 presentation-role
112+
// semantics — decorative element, no exposed semantics);
113+
// - aria-hidden="true" or {{true}} (per WAI-ARIA 1.2 §aria-hidden).
114+
// Valueless/empty aria-hidden resolves to default `undefined` per
115+
// §6.6 and does NOT qualify — flagged below in invalid.
114116
'<template><div role="presentation" onclick={{this.h}}></div></template>',
115117
'<template><div role="none" onclick={{this.h}}></div></template>',
116118
'<template><div role="presentation" {{on "click" this.h}}></div></template>',
117119
'<template><div role="none" {{action "foo"}}></div></template>',
118120
'<template><div aria-hidden="true" onclick={{this.h}}></div></template>',
119-
'<template><div aria-hidden onclick={{this.h}}></div></template>',
120121
'<template><div aria-hidden={{true}} onclick={{this.h}}></div></template>',
121122
'<template><div aria-hidden="true" {{on "click" this.h}}></div></template>',
123+
'<template><div aria-hidden="TRUE" onclick={{this.h}}></div></template>',
122124
// Case-insensitive / whitespace tolerance on role values.
123125
'<template><div role=" Presentation " onclick={{this.h}}></div></template>',
124126
'<template><div role="NONE" onclick={{this.h}}></div></template>',
@@ -229,6 +231,19 @@ ruleTester.run('template-no-invalid-interactive', rule, {
229231
output: null,
230232
errors: [{ messageId: 'noInvalidInteractive' }],
231233
},
234+
{
235+
// Valueless aria-hidden resolves to default `undefined` per WAI-ARIA
236+
// 1.2 §6.6 — not an opt-out.
237+
code: '<template><div aria-hidden onclick={{this.h}}></div></template>',
238+
output: null,
239+
errors: [{ messageId: 'noInvalidInteractive' }],
240+
},
241+
{
242+
// Empty-string aria-hidden — same default-undefined resolution.
243+
code: '<template><div aria-hidden="" onclick={{this.h}}></div></template>',
244+
output: null,
245+
errors: [{ messageId: 'noInvalidInteractive' }],
246+
},
232247
{
233248
// `role="note"` is neither presentation/none nor an interactive role.
234249
code: '<template><div role="note" onclick={{this.h}}></div></template>',

0 commit comments

Comments
 (0)