Skip to content

Commit c5d5fe5

Browse files
committed
refactor: align with #37's HTML-content-model authority split
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.
1 parent d879cef commit c5d5fe5

5 files changed

Lines changed: 297 additions & 259 deletions

lib/rules/template-click-events-have-key-events.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { dom } = require('aria-query');
22
const { isComponentInvocation } = require('../utils/is-component-invocation');
3-
const { isNativeInteractive } = require('../utils/native-interactive-elements');
3+
const { isHtmlInteractiveContent } = require('../utils/html-interactive-content');
44

55
const KEYBOARD_EVENT_NAMES = new Set(['keydown', 'keyup', 'keypress']);
66

@@ -15,7 +15,7 @@ function getAttrTextValue(attr) {
1515
return undefined;
1616
}
1717

18-
// Adapter matching the `isNativeInteractive` util's expected signature:
18+
// Adapter matching the `isHtmlInteractiveContent` util's expected signature:
1919
// `(node, attrName) -> string | undefined` for static attribute text values.
2020
function getTextAttrValue(node, attrName) {
2121
return getAttrTextValue(findAttr(node, attrName));
@@ -139,7 +139,21 @@ module.exports = {
139139
return;
140140
}
141141

142-
if (isNativeInteractive(node, getTextAttrValue)) {
142+
if (isHtmlInteractiveContent(node, getTextAttrValue)) {
143+
return;
144+
}
145+
146+
// Elements outside HTML §3.2.5.2.7 that are nonetheless ARIA widgets
147+
// or conventionally interactive surfaces — click-without-key on them
148+
// isn't what this rule targets. The HTML-content-model util covers
149+
// the spec-normative list; these are the ARIA-widget / convention
150+
// additions (see `html-interactive-content.js` docstring for why the
151+
// two authorities diverge).
152+
// - <canvas>: drawing/game surface (axobject-query: CanvasRole).
153+
// - <option>: ARIA role="option" (widget).
154+
// - <datalist>: ARIA role="listbox" (widget).
155+
const lowerTag = node.tag.toLowerCase();
156+
if (lowerTag === 'canvas' || lowerTag === 'option' || lowerTag === 'datalist') {
143157
return;
144158
}
145159

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
3+
/**
4+
* HTML "interactive content" classification, authoritative per
5+
* [HTML Living Standard §3.2.5.2.7 Interactive content]
6+
* (https://html.spec.whatwg.org/multipage/dom.html#interactive-content):
7+
*
8+
* a (if the href attribute is present), audio (if the controls attribute
9+
* is present), button, details, embed, iframe, img (if the usemap
10+
* attribute is present), input (if the type attribute is not in the
11+
* Hidden state), label, select, textarea, video (if the controls
12+
* attribute is present).
13+
*
14+
* Plus <summary>, which is not in §3.2.5.2.7 but is keyboard-activatable per
15+
* [§4.11.2 The summary element](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-summary-element).
16+
*
17+
* This is the HTML-content-model authority — it answers "does the HTML spec
18+
* prohibit nesting this inside an interactive parent?" It does NOT answer
19+
* "is this an ARIA widget for AT semantics?" (see `interactive-roles.js`
20+
* for that). The two questions diverge on rows like <label> (HTML: yes;
21+
* ARIA: structure role), <canvas> (HTML: no; ARIA: widget per axobject),
22+
* and <option>/<datalist> (HTML: no; ARIA: widgets). Rules that need
23+
* "interactive for any reason" should compose both authorities.
24+
*/
25+
26+
const UNCONDITIONAL_INTERACTIVE_TAGS = new Set([
27+
'button',
28+
'details',
29+
'embed',
30+
'iframe',
31+
'label',
32+
'select',
33+
'summary',
34+
'textarea',
35+
]);
36+
37+
/**
38+
* Determine whether a Glimmer element node is HTML-interactive content per
39+
* §3.2.5.2.7 (+ summary).
40+
*
41+
* @param {object} node Glimmer ElementNode (has a string `tag`).
42+
* @param {Function} getTextAttrValue Helper (node, attrName) -> string | undefined
43+
* returning the static text value of an
44+
* attribute, or undefined for dynamic / missing.
45+
* @param {object} [options]
46+
* @param {boolean} [options.ignoreUsemap=false] Treat `<img usemap>` as NOT interactive.
47+
* Consumed by rules with an `ignoreUsemap`
48+
* config option that lets authors opt out
49+
* of image-map-based interactivity.
50+
* @returns {boolean}
51+
*/
52+
function isHtmlInteractiveContent(node, getTextAttrValue, options = {}) {
53+
const rawTag = node && node.tag;
54+
if (typeof rawTag !== 'string' || rawTag.length === 0) {
55+
return false;
56+
}
57+
const tag = rawTag.toLowerCase();
58+
59+
if (UNCONDITIONAL_INTERACTIVE_TAGS.has(tag)) {
60+
return true;
61+
}
62+
63+
// input — interactive unless type="hidden"
64+
if (tag === 'input') {
65+
const type = getTextAttrValue(node, 'type');
66+
return type !== 'hidden';
67+
}
68+
69+
// a — interactive only when href is present
70+
if (tag === 'a') {
71+
return hasAttribute(node, 'href');
72+
}
73+
74+
// img — interactive only when usemap is present (image map)
75+
if (tag === 'img') {
76+
if (options.ignoreUsemap) {
77+
return false;
78+
}
79+
return hasAttribute(node, 'usemap');
80+
}
81+
82+
// audio / video — interactive only when controls is present
83+
if (tag === 'audio' || tag === 'video') {
84+
return hasAttribute(node, 'controls');
85+
}
86+
87+
return false;
88+
}
89+
90+
function hasAttribute(node, name) {
91+
return Boolean(node.attributes && node.attributes.some((a) => a.name === name));
92+
}
93+
94+
module.exports = { isHtmlInteractiveContent };

lib/utils/native-interactive-elements.js

Lines changed: 0 additions & 89 deletions
This file was deleted.

0 commit comments

Comments
 (0)