Skip to content

Commit 656f2f4

Browse files
committed
fix: normalize <input type>, plumb ignoreUsemap, align contenteditable empty-string (Copilot review)
1 parent 74511fc commit 656f2f4

4 files changed

Lines changed: 37 additions & 13 deletions

File tree

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ module.exports = {
8787
// HTML §3.2.5.2.7 interactive content (authoritative for content-model;
8888
// handles label/button/etc. + conditional a[href], input[!hidden],
8989
// img[usemap], audio/video[controls]).
90-
if (isHtmlInteractiveContent(node, getTextAttr)) {
90+
if (isHtmlInteractiveContent(node, getTextAttr, { ignoreUsemap })) {
9191
return true;
9292
}
9393

@@ -109,10 +109,15 @@ module.exports = {
109109
return true;
110110
}
111111

112-
// Check contenteditable
113-
const ce = getTextAttr(node, 'contenteditable');
114-
if (ce && ce !== 'false') {
115-
return true;
112+
// Check contenteditable. Per HTML spec, valid keywords are "true",
113+
// "false", "plaintext-only", and the empty string (which is the default
114+
// state, equivalent to "true"). So the attribute enables editing unless
115+
// its value is "false".
116+
if (hasAttr(node, 'contenteditable')) {
117+
const ce = getTextAttr(node, 'contenteditable');
118+
if (ce === undefined || ce === null || ce.trim().toLowerCase() !== 'false') {
119+
return true;
120+
}
116121
}
117122

118123
// Check usemap (only on img/object)

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,15 @@ module.exports = {
139139
return true;
140140
}
141141

142-
// Check contenteditable
143-
const ce = getTextAttr(node, 'contenteditable');
144-
if (ce && ce !== 'false') {
145-
return true;
142+
// Check contenteditable. Per HTML spec, valid keywords are "true",
143+
// "false", "plaintext-only", and the empty string (which is the default
144+
// state, equivalent to "true"). So the attribute enables editing unless
145+
// its value is "false".
146+
if (hasAttr(node, 'contenteditable')) {
147+
const ce = getTextAttr(node, 'contenteditable');
148+
if (ce === undefined || ce === null || ce.trim().toLowerCase() !== 'false') {
149+
return true;
150+
}
146151
}
147152

148153
// <object usemap> — not in HTML §3.2.5.2.7 but upstream ember-template-lint
@@ -175,9 +180,11 @@ module.exports = {
175180
if (role && INTERACTIVE_ROLES.has(role)) {
176181
return false;
177182
}
178-
const ce = getTextAttr(node, 'contenteditable');
179-
if (ce && ce !== 'false') {
180-
return false;
183+
if (hasAttr(node, 'contenteditable')) {
184+
const ce = getTextAttr(node, 'contenteditable');
185+
if (ce === undefined || ce === null || ce.trim().toLowerCase() !== 'false') {
186+
return false;
187+
}
181188
}
182189
if ((tag === 'img' || tag === 'object') && hasAttr(node, 'usemap')) {
183190
return false;

lib/utils/html-interactive-content.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function isHtmlInteractiveContent(node, getTextAttrValue, options = {}) {
6363
// input — interactive unless type="hidden"
6464
if (tag === 'input') {
6565
const type = getTextAttrValue(node, 'type');
66-
return type !== 'hidden';
66+
return type === undefined || type === null || type.trim().toLowerCase() !== 'hidden';
6767
}
6868

6969
// a — interactive only when href is present

tests/lib/utils/html-interactive-content-test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ describe('isHtmlInteractiveContent', () => {
5353
isHtmlInteractiveContent(makeNode('input', { type: 'hidden' }), getTextAttrValue)
5454
).toBe(false);
5555
});
56+
57+
it('is NOT interactive when type="HIDDEN" (case-insensitive)', () => {
58+
expect(
59+
isHtmlInteractiveContent(makeNode('input', { type: 'HIDDEN' }), getTextAttrValue)
60+
).toBe(false);
61+
});
62+
63+
it('is NOT interactive when type=" hidden " (whitespace-trimmed)', () => {
64+
expect(
65+
isHtmlInteractiveContent(makeNode('input', { type: ' hidden ' }), getTextAttrValue)
66+
).toBe(false);
67+
});
5668
});
5769

5870
describe('<a>', () => {

0 commit comments

Comments
 (0)