refactor: extract html-interactive-content util (HTML §3.2.5.2.7 authority)#37
refactor: extract html-interactive-content util (HTML §3.2.5.2.7 authority)#37
html-interactive-content util (HTML §3.2.5.2.7 authority)#37Conversation
🏎️ Benchmark Comparison
Full mitata output |
Migrate template-no-aria-hidden-on-focusable to the shared utility helpers introduced by PR #31 (isComponentInvocation) and PR #37 (isNativeInteractive): - isFocusable() now delegates the native-focusable-tag check to isNativeInteractive(node, getTextAttrValue). The local INHERENTLY_FOCUSABLE_TAGS set and the inline a[href] branch are removed. - hasFocusableDescendant()'s opaque-tag skip (added in G5.1) now uses isComponentInvocation(child) in place of the inline isOpaqueTag predicate; the local isOpaqueTag helper is removed. Behavior delta (spec-correct FN fix): - Previously <video controls> and <audio controls> were absent from the local INHERENTLY_FOCUSABLE_TAGS, so <div aria-hidden="true"><video controls></video></div> was VALID. - isNativeInteractive returns true for audio[controls] / video[controls] (browsers only render focusable media UI when controls is present). Such patterns are now FLAGGED under noAriaHiddenOnAncestorOfFocusable, and the element directly (<video controls aria-hidden="true">) is FLAGGED under noAriaHiddenOnFocusable. - Audio/video without controls remain VALID (no native focusable UI). Tests: new invalid cases for audio/video with controls directly aria-hidden and as descendants of aria-hidden wrappers, in both gts and hbs suites. New valid cases for audio/video without controls to pin the conditional behavior.
Replace the rule's inline INHERENTLY_FOCUSABLE_TAGS set and ad-hoc tag checks with the two shared utils: - lib/utils/is-component-invocation.js (from PR #31) - lib/utils/native-interactive-elements.js (from PR #37) Both files (and their tests) are copied bit-identically from their source branches so parity is preserved while those PRs remain open. Behavior deltas introduced by the util swap ------------------------------------------- The prior inline set was {button, details, embed, iframe, input, select, summary, textarea}. The shared util covers the same set plus several additional native-interactive tags that were previously false negatives: - option, datalist, object, canvas — now recognized as native-interactive - area[href] — now recognized (symmetric with a[href]) - audio[controls], video[controls] — now recognized (per HTML-AAM / browser reality; keyboard-operable transport controls) Net effect: `role="presentation"` / `role="none"` on any of the above is now flagged where it wasn't before. All of these are spec-correct FN fixes (WAI-ARIA 1.2 §4.6 conflict resolution applies the same way once the element is acknowledged as focusable). Tests added for representative new cases: - <video controls role="presentation"> — flags (gts + hbs) - <audio controls role="none"> — flags (gts) - <area href="/x" role="presentation"> — flags (gts) - <video role="presentation"> (no controls) — valid (still not focusable) No deltas for <label>: it was not in the prior INHERENTLY_FOCUSABLE_TAGS set and it is not in the shared util either, so behavior is unchanged. Component-invocation handling is now an explicit early-return via isComponentInvocation(node), which also excludes named-arg (<@slot>), this-path (<this.widget>), and dot-path (<foo.bar>) invocations that were previously only excluded incidentally by the tag-lowercase lookup.
f5ee868 to
9ba18c6
Compare
Same alignment story as the is-native-element swap in the previous commit: copy #37's lib/utils/native-interactive-elements.js + test byte-for-byte so the two PRs can land in either order without conflict. Behavior change: <object> is no longer classified as interactive. axobject-query has no entry for <object>, and no authoritative source backs "interactive by default" for it — prior inclusion was based on a misattributed axobject-query citation. The <object tabindex="0"> valid-test is removed accordingly (it would now flag, which is the intended new behavior; invalid-case coverage for <object tabindex> belongs in a dedicated test once we decide how to frame the guidance).
…uthority
Replace lib/utils/native-interactive-elements.js with
lib/utils/html-interactive-content.js. The new util is strictly scoped
to HTML Living Standard §3.2.5.2.7 Interactive Content (plus <summary>
per §4.11.2):
button, details, embed, iframe, label, select, summary, textarea
+ a[href], input[!type=hidden], img[usemap], audio[controls], video[controls]
The previous util mixed HTML interactive-content semantics with
axobject-query widget-taxonomy semantics, giving it no single spec
authority to cite. Edge cases (label, object+usemap, canvas, option,
datalist) were adjudicated via hand-waving ("no-false-positive bias")
because neither authority alone justified the list.
This commit commits to HTML §3.2.5.2.7 as the sole authority for the
util. ARIA-widget-role concerns remain in each rule's hardcoded
INTERACTIVE_ROLES set (separate authority, separate concern).
Behavior changes:
- <label> is interactive again (upstream ember-template-lint parity
restored). <label><input><input></label> flags multi-labelable-child
via the existing rule-level label-child-counting logic.
- <object usemap> is interactive via rule-level special case (not in
§3.2.5.2.7 but upstream-parity).
- <canvas> is interactive via rule-level defensive addition (not in
§3.2.5.2.7 but authors commonly wire for drawing/game UI).
- <option>, <datalist> are no longer interactive (they were #37 prior
defensive additions, not in HTML §3.2.5.2.7; rules wanting them can
consult aria-query widgets separately).
- <area[href]> is no longer interactive via this util (not in §3.2.5.2.7;
rules wanting it should use the ARIA widget-role authority).
Test updates mirror these changes — restored label-multi-child and
object-usemap INVALID cases; removed option/datalist/canvas-defensive
valid cases.
Supersedes the "decision table" framing of the previous PR body — see
updated PR description for the authority-split rationale.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
html-interactive-content util (HTML §3.2.5.2.7 authority)
Import INTERACTIVE_ROLES and COMPOSITE_WIDGET_CHILDREN from the shared lib/utils/interactive-roles.js util (introduced in #27 — byte-identical copy here so either PR can land first without conflict). Drop the hardcoded 19-role set previously duplicated inline in each rule. Behavior changes: - ARIA widget role set expands from 19 to 35 roles — picks up menubar, menu, listbox, tree, tablist, grid, treegrid, radiogroup, alertdialog, progressbar, and other widget-descended roles in aria-query's taxonomy that the hardcoded list missed. - tooltip is no longer treated as interactive. Per WAI-ARIA 1.2 §5.3.3, tooltip is a document-structure role, not a widget. #27's util reflects this (tooltip explicitly excluded). Old <div role="tooltip" onclick> test moves from valid to invalid — spec-correct. - Composite-widget nesting exception expanded via COMPOSITE_WIDGET_CHILDREN. Canonical APG patterns (<ul role="menubar"><li role="menuitem">..., <ul role="listbox"><li role="option">..., grid/row/gridcell, treegrid, radiogroup/radio) no longer flag as nested-interactive. Previously the rule only handled menuitem-in-menuitem explicitly. Pairs with #37's HTML-content-model authority split to complete the two-authority architecture: HTML §3.2.5.2.7 via html-interactive-content, ARIA widget taxonomy via interactive-roles. Each util cites one authority honestly; rules compose both.
There was a problem hiding this comment.
Pull request overview
This PR refactors interactive-element classification by separating HTML “interactive content” (per HTML Living Standard §3.2.5.2.7) from ARIA widget-role interactivity (per aria-query taxonomy), then migrates template-no-invalid-interactive and template-no-nested-interactive to use the new shared utilities.
Changes:
- Added
lib/utils/html-interactive-content.jsand migrated rules to consult it for spec-aligned HTML interactivity (plus<summary>). - Added
lib/utils/interactive-roles.jsderivingINTERACTIVE_ROLES(and composite-widget nesting allowances) fromaria-query, with tests that pin expected role-set behavior. - Updated rule test suites to match the new authority split and defensive special cases (
<canvas>,<object usemap>).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/utils/html-interactive-content.js | New spec-aligned HTML interactive-content classifier with ignoreUsemap support. |
| lib/utils/interactive-roles.js | New derived ARIA widget-role set and composite-widget child map used by rules. |
| lib/rules/template-no-nested-interactive.js | Migrates tag/role logic to new utils; adds composite-widget exception handling. |
| lib/rules/template-no-invalid-interactive.js | Migrates tag/role logic to new utils; preserves <canvas> behavior. |
| tests/lib/utils/html-interactive-content-test.js | Adds unit coverage for HTML interactive-content classification edge cases. |
| tests/lib/utils/interactive-roles-test.js | Adds unit coverage for derived interactive-role set and composite-widget mapping. |
| tests/lib/rules/template-no-nested-interactive.js | Adjusts rule fixtures for audio/video controls + label behavior + object/usemap special case. |
| tests/lib/rules/template-no-invalid-interactive.js | Adjusts rule fixtures for tooltip role handling and <canvas> defensive behavior. |
Comments suppressed due to low confidence (1)
lib/rules/template-no-nested-interactive.js:176
isInteractiveOnlyFromTabindexdoesn’t account for the rule-level<canvas>interactive carve-out. A<canvas tabindex="0">will be classified as “interactive only from tabindex” and won’t be pushed onto the interactive-parent stack, which can suppress nested-interactive reports inside it. Add atag === 'canvas'exclusion here to keep behavior consistent withisInteractive().
if (isHtmlInteractiveContent(node, getTextAttr, { ignoreUsemap })) {
return false;
}
const role = getTextAttr(node, 'role');
if (role && INTERACTIVE_ROLES.has(role)) {
return false;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
lib/rules/template-no-nested-interactive.js:192
isInteractiveOnlyFromTabindextreats anyusemapattribute (img/object) as making the element non-tabindex-only, even whenignoreUsemapis enabled, and it also doesn't account for the rule-level<canvas>interactive carve-out. This can misclassify elements as “interactive for more than tabindex” and push them onto the interactive stack, potentially creating incorrect nested-interactive reports. Consider (a) honoringignoreUsemapfor bothimgandobjecthere, and (b) returningfalsefortag === 'canvas'so canvas+tabindex isn't treated as tabindex-only.
if (hasAttr(node, 'contenteditable')) {
const ce = getTextAttr(node, 'contenteditable');
if (ce === undefined || ce === null || ce.trim().toLowerCase() !== 'false') {
return false;
}
}
if ((tag === 'img' || tag === 'object') && hasAttr(node, 'usemap')) {
return false;
}
return hasAttr(node, 'tabindex');
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
lib/rules/template-no-nested-interactive.js:200
isInteractiveOnlyFromTabindextreats anyusemapattribute as a non-tabindex source of interactivity even whenignoreUsemap/ignoreUsemapAttributeis enabled. This breaks the option’s intent: e.g. withignoreUsemap=true, an<img usemap ... tabindex="0">should be considered tabindex-only (and not pushed ontointeractiveStack), but the current check returnsfalsedue solely tousemapbeing present. Gate thisusemapexclusion on!ignoreUsemap(and align with the object+usemap special case above).
if (hasAttr(node, 'contenteditable')) {
const ce = getTextAttr(node, 'contenteditable');
if (ce === undefined || ce === null || ce.trim().toLowerCase() !== 'false') {
return false;
}
}
if ((tag === 'img' || tag === 'object') && hasAttr(node, 'usemap')) {
return false;
}
return hasAttr(node, 'tabindex');
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Import INTERACTIVE_ROLES and COMPOSITE_WIDGET_CHILDREN from the shared lib/utils/interactive-roles.js util (introduced in #27 — byte-identical copy here so either PR can land first without conflict). Drop the hardcoded 19-role set previously duplicated inline in each rule. Behavior changes: - ARIA widget role set expands from 19 to 35 roles — picks up menubar, menu, listbox, tree, tablist, grid, treegrid, radiogroup, alertdialog, progressbar, and other widget-descended roles in aria-query's taxonomy that the hardcoded list missed. - tooltip is no longer treated as interactive. Per WAI-ARIA 1.2 §5.3.3, tooltip is a document-structure role, not a widget. #27's util reflects this (tooltip explicitly excluded). Old <div role="tooltip" onclick> test moves from valid to invalid — spec-correct. - Composite-widget nesting exception expanded via COMPOSITE_WIDGET_CHILDREN. Canonical APG patterns (<ul role="menubar"><li role="menuitem">..., <ul role="listbox"><li role="option">..., grid/row/gridcell, treegrid, radiogroup/radio) no longer flag as nested-interactive. Previously the rule only handled menuitem-in-menuitem explicitly. Pairs with #37's HTML-content-model authority split to complete the two-authority architecture: HTML §3.2.5.2.7 via html-interactive-content, ARIA widget taxonomy via interactive-roles. Each util cites one authority honestly; rules compose both.
…e empty-string (Copilot review)
… menu-item role variants (Copilot review)
…-invalid-interactive
e2aa02a to
aecd9ed
Compare
Migrate template-no-aria-hidden-on-focusable to the shared utility helpers introduced by PR #31 (isComponentInvocation) and PR #37 (isNativeInteractive): - isFocusable() now delegates the native-focusable-tag check to isNativeInteractive(node, getTextAttrValue). The local INHERENTLY_FOCUSABLE_TAGS set and the inline a[href] branch are removed. - hasFocusableDescendant()'s opaque-tag skip (added in G5.1) now uses isComponentInvocation(child) in place of the inline isOpaqueTag predicate; the local isOpaqueTag helper is removed. Behavior delta (spec-correct FN fix): - Previously <video controls> and <audio controls> were absent from the local INHERENTLY_FOCUSABLE_TAGS, so <div aria-hidden="true"><video controls></video></div> was VALID. - isNativeInteractive returns true for audio[controls] / video[controls] (browsers only render focusable media UI when controls is present). Such patterns are now FLAGGED under noAriaHiddenOnAncestorOfFocusable, and the element directly (<video controls aria-hidden="true">) is FLAGGED under noAriaHiddenOnFocusable. - Audio/video without controls remain VALID (no native focusable UI). Tests: new invalid cases for audio/video with controls directly aria-hidden and as descendants of aria-hidden wrappers, in both gts and hbs suites. New valid cases for audio/video without controls to pin the conditional behavior.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace the rule's inline INHERENTLY_FOCUSABLE_TAGS set and ad-hoc tag checks with the two shared utils: - lib/utils/is-component-invocation.js (from PR #31) - lib/utils/native-interactive-elements.js (from PR #37) Both files (and their tests) are copied bit-identically from their source branches so parity is preserved while those PRs remain open. Behavior deltas introduced by the util swap ------------------------------------------- The prior inline set was {button, details, embed, iframe, input, select, summary, textarea}. The shared util covers the same set plus several additional native-interactive tags that were previously false negatives: - option, datalist, object, canvas — now recognized as native-interactive - area[href] — now recognized (symmetric with a[href]) - audio[controls], video[controls] — now recognized (per HTML-AAM / browser reality; keyboard-operable transport controls) Net effect: `role="presentation"` / `role="none"` on any of the above is now flagged where it wasn't before. All of these are spec-correct FN fixes (WAI-ARIA 1.2 §4.6 conflict resolution applies the same way once the element is acknowledged as focusable). Tests added for representative new cases: - <video controls role="presentation"> — flags (gts + hbs) - <audio controls role="none"> — flags (gts) - <area href="/x" role="presentation"> — flags (gts) - <video role="presentation"> (no controls) — valid (still not focusable) No deltas for <label>: it was not in the prior INHERENTLY_FOCUSABLE_TAGS set and it is not in the shared util either, so behavior is unchanged. Component-invocation handling is now an explicit early-return via isComponentInvocation(node), which also excludes named-arg (<@slot>), this-path (<this.widget>), and dot-path (<foo.bar>) invocations that were previously only excluded incidentally by the tag-lowercase lookup.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Same alignment story as the is-native-element swap in the previous commit: copy #37's lib/utils/native-interactive-elements.js + test byte-for-byte so the two PRs can land in either order without conflict. Behavior change: <object> is no longer classified as interactive. axobject-query has no entry for <object>, and no authoritative source backs "interactive by default" for it — prior inclusion was based on a misattributed axobject-query citation. The <object tabindex="0"> valid-test is removed accordingly (it would now flag, which is the intended new behavior; invalid-case coverage for <object tabindex> belongs in a dedicated test once we decide how to frame the guidance).
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
…yFromTabindex; update docs canvas is unconditionally interactive (added by PR #37 for drawing/game-UI parity with upstream ember-template-lint). isInteractiveOnlyFromTabindex was missing a canvas guard, so <canvas tabindex="0"> was incorrectly treated as tabindex-only and never pushed onto the interactive stack, meaning nested interactive content inside a canvas+tabindex was silently allowed. Also updates docs to list audio[controls], video[controls], and canvas as interactive, and expands the Special Cases section to document the ARIA composite-widget hierarchy exception introduced alongside this refactor.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Migrate template-no-aria-hidden-on-focusable to the shared utility helpers introduced by PR #31 (isComponentInvocation) and PR #37 (isNativeInteractive): - isFocusable() now delegates the native-focusable-tag check to isNativeInteractive(node, getTextAttrValue). The local INHERENTLY_FOCUSABLE_TAGS set and the inline a[href] branch are removed. - hasFocusableDescendant()'s opaque-tag skip (added in G5.1) now uses isComponentInvocation(child) in place of the inline isOpaqueTag predicate; the local isOpaqueTag helper is removed. Behavior delta (spec-correct FN fix): - Previously <video controls> and <audio controls> were absent from the local INHERENTLY_FOCUSABLE_TAGS, so <div aria-hidden="true"><video controls></video></div> was VALID. - isNativeInteractive returns true for audio[controls] / video[controls] (browsers only render focusable media UI when controls is present). Such patterns are now FLAGGED under noAriaHiddenOnAncestorOfFocusable, and the element directly (<video controls aria-hidden="true">) is FLAGGED under noAriaHiddenOnFocusable. - Audio/video without controls remain VALID (no native focusable UI). Tests: new invalid cases for audio/video with controls directly aria-hidden and as descendants of aria-hidden wrappers, in both gts and hbs suites. New valid cases for audio/video without controls to pin the conditional behavior.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Replace the rule's inline INHERENTLY_FOCUSABLE_TAGS set and ad-hoc tag checks with the two shared utils: - lib/utils/is-component-invocation.js (from PR #31) - lib/utils/native-interactive-elements.js (from PR #37) Both files (and their tests) are copied bit-identically from their source branches so parity is preserved while those PRs remain open. Behavior deltas introduced by the util swap ------------------------------------------- The prior inline set was {button, details, embed, iframe, input, select, summary, textarea}. The shared util covers the same set plus several additional native-interactive tags that were previously false negatives: - option, datalist, object, canvas — now recognized as native-interactive - area[href] — now recognized (symmetric with a[href]) - audio[controls], video[controls] — now recognized (per HTML-AAM / browser reality; keyboard-operable transport controls) Net effect: `role="presentation"` / `role="none"` on any of the above is now flagged where it wasn't before. All of these are spec-correct FN fixes (WAI-ARIA 1.2 §4.6 conflict resolution applies the same way once the element is acknowledged as focusable). Tests added for representative new cases: - <video controls role="presentation"> — flags (gts + hbs) - <audio controls role="none"> — flags (gts) - <area href="/x" role="presentation"> — flags (gts) - <video role="presentation"> (no controls) — valid (still not focusable) No deltas for <label>: it was not in the prior INHERENTLY_FOCUSABLE_TAGS set and it is not in the shared util either, so behavior is unchanged. Component-invocation handling is now an explicit early-return via isComponentInvocation(node), which also excludes named-arg (<@slot>), this-path (<this.widget>), and dot-path (<foo.bar>) invocations that were previously only excluded incidentally by the tag-lowercase lookup.
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Same alignment story as the is-native-element swap in the previous commit: copy #37's lib/utils/native-interactive-elements.js + test byte-for-byte so the two PRs can land in either order without conflict. Behavior change: <object> is no longer classified as interactive. axobject-query has no entry for <object>, and no authoritative source backs "interactive by default" for it — prior inclusion was based on a misattributed axobject-query citation. The <object tabindex="0"> valid-test is removed accordingly (it would now flag, which is the intended new behavior; invalid-case coverage for <object tabindex> belongs in a dedicated test once we decide how to frame the guidance).
Replace lib/utils/native-interactive-elements.js with lib/utils/html-interactive-content.js to match the canonical util introduced in #37. The new util cites HTML Living Standard §3.2.5.2.7 Interactive Content as its sole authority, resolving the previous mixed-authority approach that cited axobject-query's widget taxonomy for some rows and HTML spec for others. Byte-identical copy of #37's util + test across worktrees so the two PRs can land in either order without conflict.
Note
This is part of a series where Claude has audited
eslint-plugin-emberagainst jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate,ember-template-lint, and the HTML and WCAG specs.Summary
template-no-invalid-interactive,template-no-nested-interactive) each hand-maintain a "native-interactive HTML tag" list. Those lists have drifted against each other and — more importantly — against any single authoritative source: rows forlabel,object,canvas,details,summary,option,datalistwere adjudicated case-by-case without a consistent citation.<label>: HTML says interactive, ARIA says structure role;<canvas>: HTML doesn't list it, ARIA says widget; etc.). Picking one citation consistently resolves each row.lib/utils/html-interactive-content.js, scoped to HTML §3.2.5.2.7 as its sole authority. The rules keep their existingINTERACTIVE_ROLESset for the ARIA-widget-role authority (separate concern, separate citation). Rows that don't cleanly belong to either authority (<canvas>,<object usemap>) become explicit rule-level defensive additions with documented justification.Fix
New util
lib/utils/html-interactive-content.jsexportsisHtmlInteractiveContent(node, getTextAttrValue, options?):button,details,embed,iframe,label,select,textarea. Plussummaryas a rule-level addition: it is not in the §3.2.5.2.7 list, but §4.11.2 defines its activation behavior (toggling its parentdetails), so it acts interactive in practice.<a href>,<input>unlesstype=hidden,<img usemap>,<audio controls>,<video controls>.options.ignoreUsemap— exempts<img usemap>when rules pass this flag (preserves the existingignoreUsemapconfig option surface).Old
lib/utils/native-interactive-elements.jsis deleted.Rule migrations (
template-no-invalid-interactive,template-no-nested-interactive):isNativeInteractive→isHtmlInteractiveContent.<canvas>added as a rule-level defensive check (not in §3.2.5.2.7, but upstream ember-template-lint has it — canvas is commonly wired for drawing/game UI where event handlers are expected).<object usemap>stays as a rule-level special case intemplate-no-nested-interactive(not in §3.2.5.2.7 — but upstream flags it and browsers treat image-mapped<object>as clickable). Moved from the util to rule scope for honest citation.template-no-nested-interactive's existing label-multi-child detection (lines 180–189) now activates correctly because<label>is interactive again.<label><input><input></label>flags as "multiple interactive elements inside a single<label>" — matches upstream.Prior art
All four a11y-ESLint peers consult either aria-query, axobject-query, or a mix for interactivity classification. None cleanly separate HTML-content-model from ARIA-widget-tree authority the way this PR does, but several have sub-utilities for one or the other.
elementRoles(primary) + axobject-queryelementAXObjects(fallback). Unified util covering both authorities.src/util/isInteractiveElement.jselementRoles-based set.src/utils/isInteractiveElement.tselementRoles+ axobject-queryelementAXObjects.src/utils/is-interactive-element/index.tsprogressbarcarve-out.lib/utils/isInteractiveElement.jsinteractivemetadata field (boolean or callback). Closest spec-aligned external source.src/elements/html5.tsBehavior change vs master
<label>is HTML interactive content again (was dropped in the prior draft).<label><input><input></label>flags per upstream parity.<object usemap>still flags intemplate-no-nested-interactive(preserved via rule-level special case; previously via util inclusion).<canvas>still interactive in both rules (preserved via rule-level defensive; previously via util inclusion).<option>,<datalist>are no longer interactive in either rule (were the prior draft's defensive additions; dropped as they're not in §3.2.5.2.7). Rules wanting them should consult an ARIA-widget-role util separately.<area[href]>no longer interactive via this util (not in §3.2.5.2.7). Rules that need<area>coverage (e.g.template-no-role-presentation-on-focusable) now add rule-level defensive handling with an HTML-focus-navigation citation.Notes
template-no-nested-interactive,template-no-invalid-interactive. Also propagated in byte-identical form to in-flight PRs feat: add template-click-events-have-key-events #17, feat: add template-no-aria-hidden-on-focusable #19, feat: add template-no-interactive-element-to-noninteractive-role #20, feat: add template-no-role-presentation-on-focusable #22, feat: add template-no-noninteractive-tabindex #24 so they can land in any order without conflict.lib/utils/interactive-roles.jscovers the ARIA-widget-taxonomy authority, used by rules needing that question (a union of the two authorities is a composition in the rule, not in the util).INTERACTIVE_ROLESset into a shared util similar to refactor: derive INTERACTIVE_ROLES from aria-query taxonomy (+ widen menu-pattern exception) #27's. Not scoped here.