Skip to content

Commit d615e74

Browse files
committed
fix: first-recognized-token for presentation/none escape-hatch; fix helper name consistency
1 parent 10bfee5 commit d615e74

2 files changed

Lines changed: 12 additions & 7 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ 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. 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.
54+
- **`role="presentation"` / `role="none"`** — the author asserts the element is decorative. We accept `role="presentation"` and `role="none"` (case-insensitive, first recognized token of a space-separated role list per WAI-ARIA first-recognized-token rule) — a deliberate superset of [jsx-a11y's exact-match `hasPresentationRole`](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

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
'use strict';
2-
1+
const { roles } = require('aria-query');
32
const { isNativeElement } = require('../utils/is-native-element');
43
const { isHtmlInteractiveContent } = require('../utils/html-interactive-content');
54
const { INTERACTIVE_ROLES } = require('../utils/interactive-roles');
@@ -23,8 +22,11 @@ function getTextAttr(node, name) {
2322
// Does this element carry a non-interactive escape hatch that opts it out
2423
// of the interactive-handler check?
2524
// - role="presentation" or role="none": author asserts the element is
26-
// decorative. We accept the first token of a space-separated role list
27-
// (a superset of jsx-a11y's exact-match `isPresentationRole`).
25+
// decorative. We find the first recognized ARIA role token in the
26+
// space-separated list (per WAI-ARIA first-recognized-token rule) and
27+
// check whether it is "presentation" or "none" — matching jsx-a11y's
28+
// hasPresentationRole semantics while correctly handling unknown leading
29+
// tokens such as role="foo none".
2830
// - aria-hidden in any plausibly-"hide" form — valueless, empty-string,
2931
// "true" (case-insensitive), `{{true}}`, `{{"true"}}`.
3032
//
@@ -36,8 +38,11 @@ function getTextAttr(node, name) {
3638
function hasNonInteractiveEscapeHatch(node) {
3739
const roleAttr = findAttr(node, 'role');
3840
if (roleAttr?.value?.type === 'GlimmerTextNode') {
39-
const token = roleAttr.value.chars.trim().toLowerCase().split(/\s+/u)[0];
40-
if (token === 'presentation' || token === 'none') {
41+
const tokens = roleAttr.value.chars.trim().toLowerCase().split(/\s+/u);
42+
// WAI-ARIA first-recognized-token: skip unknown tokens, use the first
43+
// one that aria-query recognizes as a valid role.
44+
const firstRecognized = tokens.find((t) => roles.get(t) !== undefined);
45+
if (firstRecognized === 'presentation' || firstRecognized === 'none') {
4146
return true;
4247
}
4348
}

0 commit comments

Comments
 (0)