Skip to content

Commit 04216f3

Browse files
committed
refactor(template-interactive-supports-focus): extract isSuppressedFromFocus helper to deduplicate tabindex/contenteditable carve-outs
1 parent 85b5b74 commit 04216f3

1 file changed

Lines changed: 22 additions & 20 deletions

File tree

lib/rules/template-interactive-supports-focus.js

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ function isComponentInvocation(tag) {
6969
// inherent-focusability we'd otherwise grant the tag.
7070
const DISABLABLE_FORM_CONTROLS = new Set(['button', 'input', 'select', 'textarea', 'fieldset']);
7171

72+
// True when the UA ignores otherwise-focusing attributes (`tabindex`,
73+
// `contenteditable`) on this element because the element is itself removed
74+
// from sequential focus navigation by HTML semantics:
75+
// - disabled form controls (HTML §4.10.18.5)
76+
// - <input type="hidden"> (no rendered element)
77+
function isSuppressedFromFocus(node, tag, getTextAttrValueFn) {
78+
if (DISABLABLE_FORM_CONTROLS.has(tag) && findAttr(node, 'disabled')) {
79+
return true;
80+
}
81+
if (tag === 'input') {
82+
const type = getTextAttrValueFn(findAttr(node, 'type'));
83+
if (typeof type === 'string' && type.trim().toLowerCase() === 'hidden') {
84+
return true;
85+
}
86+
}
87+
return false;
88+
}
89+
7290
// Is the element inherently focusable without needing tabindex?
7391
function isInherentlyFocusable(node) {
7492
const tag = node.tag?.toLowerCase();
@@ -209,32 +227,16 @@ module.exports = {
209227
// HTML attribute names are case-insensitive, so accept `tabindex` or
210228
// any other casing (e.g. `tabIndex`, the React-style camelCase).
211229
const hasTabindex = node.attributes?.some((a) => a.name?.toLowerCase() === 'tabindex');
212-
if (hasTabindex) {
213-
const disabled = DISABLABLE_FORM_CONTROLS.has(tag) && findAttr(node, 'disabled');
214-
let hiddenInput = false;
215-
if (tag === 'input') {
216-
const type = getTextAttrValue(findAttr(node, 'type'));
217-
hiddenInput = typeof type === 'string' && type.trim().toLowerCase() === 'hidden';
218-
}
219-
if (!disabled && !hiddenInput) {
220-
return;
221-
}
230+
if (hasTabindex && !isSuppressedFromFocus(node, tag, getTextAttrValue)) {
231+
return;
222232
}
223233

224234
// contenteditable also makes an element focusable, with the same
225235
// HTML-spec carve-outs as tabindex: the UA ignores it on disabled
226236
// form controls (HTML §4.10.18.5) and on <input type="hidden">
227237
// (no rendered element to edit), so the a11y conflict still stands.
228-
if (isContentEditable(node)) {
229-
const disabled = DISABLABLE_FORM_CONTROLS.has(tag) && findAttr(node, 'disabled');
230-
let hiddenInput = false;
231-
if (tag === 'input') {
232-
const type = getTextAttrValue(findAttr(node, 'type'));
233-
hiddenInput = typeof type === 'string' && type.trim().toLowerCase() === 'hidden';
234-
}
235-
if (!disabled && !hiddenInput) {
236-
return;
237-
}
238+
if (isContentEditable(node) && !isSuppressedFromFocus(node, tag, getTextAttrValue)) {
239+
return;
238240
}
239241

240242
context.report({

0 commit comments

Comments
 (0)