Skip to content

Commit 6b35b8c

Browse files
committed
fix(template-no-interactive-element-to-noninteractive-role): scope-aware skip + docs + tests (Copilot review)
- Switch rule to isNativeElement(node, sourceCode) so scope-bound lowercase tags (`const button = BtnCmp; <button>`) are skipped as components. - Add a strict-mode valid test case for that shadowing scenario. - Clarify is-native-element JSDoc (byte-identical to PR #50 canonical, drops the stale 'template-no-noninteractive-tabindex' example). - Update COMPOSITE_WIDGET_CHILDREN comment in interactive-roles.js to say 'planned future use' — no consumer wires the nested-interactive exception in yet.
1 parent eac9ff1 commit 6b35b8c

4 files changed

Lines changed: 26 additions & 15 deletions

File tree

lib/rules/template-no-interactive-element-to-noninteractive-role.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { roles, elementRoles } = require('aria-query');
22
const { AXObjects, elementAXObjects } = require('axobject-query');
33
const { INTERACTIVE_ROLES } = require('../utils/interactive-roles');
4-
const { isComponentInvocation } = require('../utils/is-component-invocation');
4+
const { isNativeElement } = require('../utils/is-native-element');
55

66
// Interactive-element derivation. Mirrors jsx-a11y's layered approach:
77
// 1. Primary signal — aria-query's `elementRoles`: an element is inherently
@@ -229,10 +229,15 @@ module.exports = {
229229
},
230230

231231
create(context) {
232+
const sourceCode = context.sourceCode || context.getSourceCode();
232233
return {
233234
GlimmerElementNode(node) {
234-
// Skip component invocations — the rule targets native HTML elements.
235-
if (isComponentInvocation(node)) {
235+
// Only run on native HTML elements — in strict GJS a lowercase tag can
236+
// be shadowed by an in-scope local binding (e.g. `const button = Btn;
237+
// <button>`) and that's a component invocation, not a native element.
238+
// `isNativeElement` combines the authoritative html/svg/mathml tag
239+
// lists with scope-shadowing detection.
240+
if (!isNativeElement(node, sourceCode)) {
236241
return;
237242
}
238243

lib/utils/interactive-roles.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ module.exports.INTERACTIVE_ROLES = buildInteractiveRoleSet();
3535
// `row` owns `gridcell` / `columnheader` / `rowheader`, so `grid` transitively
3636
// allows all of them).
3737
//
38-
// This drives the nested-interactive exception so canonical composite-widget
39-
// patterns (`listbox > option`, `tablist > tab`, `tree > treeitem`,
40-
// `grid > row > gridcell`, `radiogroup > radio`, etc.) are not flagged.
38+
// Planned future use: a nested-interactive exception so canonical composite-
39+
// widget patterns (`listbox > option`, `tablist > tab`, `tree > treeitem`,
40+
// `grid > row > gridcell`, `radiogroup > radio`, etc.) would not be flagged.
41+
// No consumer wires this in yet — export is kept stable for the pending
42+
// rule update. Computation is cheap enough to leave eager rather than
43+
// lazy-getter-ify it.
4144
module.exports.COMPOSITE_WIDGET_CHILDREN = buildCompositeWidgetChildren();
4245

4346
function buildInteractiveRoleSet() {

lib/utils/is-native-element.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,17 @@ const ELEMENT_TAGS = new Set([...htmlTags, ...svgTags, ...mathmlTagNames]);
2121
* MathML spec registries, reached via the `html-tags` / `svg-tags` /
2222
* `mathml-tag-names` packages). It is NOT the same as:
2323
*
24-
* - "native accessibility" / "widget-ness" — see `interactive-roles.js`
25-
* (aria-query widget taxonomy; an ARIA-tree-semantics question)
26-
* - "native interactive content" / "focus behavior" — see
27-
* `html-interactive-content.js` (HTML §3.2.5.2.7; an HTML-content-model
28-
* question about which tags can be nested inside what)
24+
* - "native accessibility" / "widget-ness" — an ARIA-tree-semantics
25+
* question (for example, whether something maps to a widget role)
26+
* - "native interactive content" / "focus behavior" — an HTML content-model
27+
* question about which elements are considered interactive in the spec
2928
* - "natively focusable" / sequential-focus — see HTML §6.6.3
3029
*
3130
* This util answers only: "is this tag a first-class built-in element of one
3231
* of the three markup-language standards, rather than a component invocation
33-
* or a shadowed local binding?" Callers compose it with the other utils
34-
* above when they need a more specific question (see e.g. `template-no-
35-
* noninteractive-tabindex`, which consults both this and
36-
* `html-interactive-content`).
32+
* or a shadowed local binding?" Callers should combine it with whatever
33+
* accessibility, interactivity, or focusability checks they need for more
34+
* specific questions.
3735
*
3836
* Returns false for:
3937
* - components (PascalCase, dotted, @-prefixed, this.-prefixed, ::-namespaced —

tests/lib/rules/template-no-interactive-element-to-noninteractive-role.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ ruleTester.run('template-no-interactive-element-to-noninteractive-role', rule, {
3030
// Components — rule skips (not a DOM element).
3131
'<template><CustomBtn role="article" /></template>',
3232

33+
// Scope-bound lowercase tag — `<button>` here is a local binding, so it
34+
// resolves to a component invocation (not a native interactive element)
35+
// and the rule must skip it even with a non-interactive role attribute.
36+
'const button = ButtonComponent;\n<template><button role="article" /></template>',
37+
3338
// Unknown role — rule skips.
3439
'<template><button role="fakerole">Click</button></template>',
3540

0 commit comments

Comments
 (0)