Skip to content

Commit 0981317

Browse files
Merge pull request #2651 from johanrd/night_fix/template-no-invalid-interactive
Post-merge-review: Fix `template-no-invalid-interactive`: align interactive element detection with upstream
2 parents 56f913d + b2544ef commit 0981317

File tree

2 files changed

+71
-6
lines changed

2 files changed

+71
-6
lines changed

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ module.exports = {
6666
const ignoreUsemap = options.ignoreUsemap || false;
6767

6868
const NATIVE_INTERACTIVE_ELEMENTS = new Set([
69-
'a',
7069
'button',
7170
'canvas',
7271
'details',
@@ -75,6 +74,7 @@ module.exports = {
7574
'input',
7675
'label',
7776
'select',
77+
'summary',
7878
'textarea',
7979
]);
8080

@@ -87,16 +87,20 @@ module.exports = {
8787
'menuitemradio',
8888
'option',
8989
'radio',
90+
'scrollbar',
9091
'searchbox',
9192
'slider',
9293
'spinbutton',
9394
'switch',
9495
'tab',
9596
'textbox',
97+
'tooltip',
98+
'treeitem',
9699
'combobox',
97100
'gridcell',
98101
]);
99102

103+
// eslint-disable-next-line complexity
100104
function isInteractive(node) {
101105
const tag = node.tag?.toLowerCase();
102106
if (!tag) {
@@ -106,6 +110,17 @@ module.exports = {
106110
if (additionalInteractiveTags.has(tag)) {
107111
return true;
108112
}
113+
114+
// <a> is only interactive when it has href
115+
if (tag === 'a' && hasAttr(node, 'href')) {
116+
return true;
117+
}
118+
119+
// <audio>/<video> with controls attribute is interactive
120+
if ((tag === 'audio' || tag === 'video') && hasAttr(node, 'controls')) {
121+
return true;
122+
}
123+
109124
if (NATIVE_INTERACTIVE_ELEMENTS.has(tag)) {
110125
// Hidden input is not interactive
111126
if (tag === 'input') {
@@ -134,8 +149,8 @@ module.exports = {
134149
return true;
135150
}
136151

137-
// Check usemap
138-
if (!ignoreUsemap && hasAttr(node, 'usemap')) {
152+
// Check usemap (only on img/object)
153+
if (!ignoreUsemap && (tag === 'img' || tag === 'object') && hasAttr(node, 'usemap')) {
139154
return true;
140155
}
141156

@@ -158,8 +173,13 @@ module.exports = {
158173
return;
159174
}
160175

161-
// Skip components (PascalCase)
162-
if (/^[A-Z]/.test(node.tag)) {
176+
// Skip components (PascalCase, @-prefixed, this.-prefixed, path-based like foo.bar)
177+
if (
178+
/^[A-Z]/.test(node.tag) ||
179+
node.tag.startsWith('@') ||
180+
node.tag.startsWith('this.') ||
181+
node.tag.includes('.')
182+
) {
163183
return;
164184
}
165185

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ ruleTester.run('template-no-invalid-interactive', rule, {
1313
code: '<template><button onclick={{this.handleClick}}>Click</button></template>',
1414
output: null,
1515
},
16+
// <a> with href is interactive
1617
{
1718
filename: 'test.gjs',
18-
code: '<template><a onclick={{this.handleClick}}>Link</a></template>',
19+
code: '<template><a href="/about" onclick={{this.handleClick}}>Link</a></template>',
1920
output: null,
2021
},
2122
{
@@ -62,6 +63,26 @@ ruleTester.run('template-no-invalid-interactive', rule, {
6263
'<template><video {{on "pause" this.onPause}}></video></template>',
6364
'<template><img {{action "foo" on="load"}}></template>',
6465
'<template><img {{action "foo" on="error"}}></template>',
66+
67+
// <summary> is natively interactive
68+
'<template><summary onclick={{this.toggle}}>Details</summary></template>',
69+
70+
// ARIA widget roles: scrollbar, tooltip, treeitem
71+
'<template><div role="scrollbar" onclick={{this.scroll}}>Scroll</div></template>',
72+
'<template><div role="tooltip" onclick={{this.show}}>Tip</div></template>',
73+
'<template><div role="treeitem" onclick={{this.select}}>Node</div></template>',
74+
75+
// audio/video with controls are interactive
76+
'<template><audio controls onclick={{this.play}}></audio></template>',
77+
'<template><video controls onclick={{this.play}}></video></template>',
78+
79+
// usemap only makes img/object interactive
80+
'<template><img usemap="#map" onclick={{this.click}}></template>',
81+
82+
// Component invocations are skipped (not HTML elements)
83+
'<template><@someComponent onclick={{this.click}} /></template>',
84+
'<template><this.myComponent onclick={{this.click}} /></template>',
85+
'<template><ns.SomeWidget onclick={{this.click}} /></template>',
6586
],
6687

6788
invalid: [
@@ -140,5 +161,29 @@ ruleTester.run('template-no-invalid-interactive', rule, {
140161
output: null,
141162
errors: [{ messageId: 'noInvalidInteractive' }],
142163
},
164+
{
165+
// usemap on non-img/object does NOT make the element interactive
166+
code: '<template><div usemap="#map" onclick={{this.click}}>Content</div></template>',
167+
output: null,
168+
errors: [{ messageId: 'noInvalidInteractive' }],
169+
},
170+
{
171+
// audio/video without controls is NOT interactive
172+
code: '<template><audio onclick={{this.play}}></audio></template>',
173+
output: null,
174+
errors: [{ messageId: 'noInvalidInteractive' }],
175+
},
176+
{
177+
// <a> without href is NOT interactive
178+
filename: 'test.gjs',
179+
code: '<template><a onclick={{this.handleClick}}>Link</a></template>',
180+
output: null,
181+
errors: [
182+
{
183+
messageId: 'noInvalidInteractive',
184+
data: { tagName: 'a', handler: 'onclick' },
185+
},
186+
],
187+
},
143188
],
144189
});

0 commit comments

Comments
 (0)