Skip to content

Commit 3b0a361

Browse files
ahmedhalacstraker
authored andcommitted
fix: consistently parse tabindex, following HTML 5 spec (#4637)
Ensures tabindex is correctly parsed as an integer using regex instead of parseInt to comply with HTML standards. Closes #4632
1 parent 0740980 commit 3b0a361

13 files changed

+93
-31
lines changed

lib/checks/keyboard/focusable-no-name-evaluate.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { isFocusable } from '../../commons/dom';
1+
import { isInTabOrder } from '../../commons/dom';
22
import { accessibleTextVirtual } from '../../commons/text';
33

44
function focusableNoNameEvaluate(node, options, virtualNode) {
5-
const tabIndex = virtualNode.attr('tabindex');
6-
const inFocusOrder = isFocusable(virtualNode) && tabIndex > -1;
7-
if (!inFocusOrder) {
5+
if (!isInTabOrder(virtualNode)) {
86
return false;
97
}
108

lib/checks/keyboard/no-focusable-content-evaluate.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import isFocusable from '../../commons/dom/is-focusable';
22
import { getRoleType } from '../../commons/aria';
3+
import { parseTabindex } from '../../core/utils';
34

45
export default function noFocusableContentEvaluate(node, options, virtualNode) {
56
if (!virtualNode.children) {
@@ -51,6 +52,6 @@ function getFocusableDescendants(vNode) {
5152
}
5253

5354
function usesUnreliableHidingStrategy(vNode) {
54-
const tabIndex = parseInt(vNode.attr('tabindex'), 10);
55-
return !isNaN(tabIndex) && tabIndex < 0;
55+
const tabIndex = parseTabindex(vNode.attr('tabindex'));
56+
return tabIndex !== null && tabIndex < 0;
5657
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { parseTabindex } from '../../core/utils';
2+
13
function tabindexEvaluate(node, options, virtualNode) {
2-
const tabIndex = parseInt(virtualNode.attr('tabindex'), 10);
4+
const tabIndex = parseTabindex(virtualNode.attr('tabindex'));
35

46
// an invalid tabindex will either return 0 or -1 (based on the element) so
57
// will never be above 0
68
// @see https://www.w3.org/TR/html51/editing.html#the-tabindex-attribute
7-
return isNaN(tabIndex) ? true : tabIndex <= 0;
9+
return tabIndex === null || tabIndex <= 0;
810
}
911

1012
export default tabindexEvaluate;

lib/commons/dom/get-tabbable-elements.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { querySelectorAll } from '../../core/utils';
2+
import { parseTabindex } from '../../core/utils';
23

34
/**
45
* Get all elements (including given node) that are part of the tab order
@@ -13,11 +14,9 @@ function getTabbableElements(virtualNode) {
1314

1415
const tabbableElements = nodeAndDescendents.filter(vNode => {
1516
const isFocusable = vNode.isFocusable;
16-
let tabIndex = vNode.actualNode.getAttribute('tabindex');
17-
tabIndex =
18-
tabIndex && !isNaN(parseInt(tabIndex, 10)) ? parseInt(tabIndex) : null;
17+
const tabIndex = parseTabindex(vNode.actualNode.getAttribute('tabindex'));
1918

20-
return tabIndex ? isFocusable && tabIndex >= 0 : isFocusable;
19+
return tabIndex !== null ? isFocusable && tabIndex >= 0 : isFocusable;
2120
});
2221

2322
return tabbableElements;

lib/commons/dom/inserted-into-focus-order.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import isFocusable from './is-focusable';
22
import isNativelyFocusable from './is-natively-focusable';
3+
import { parseTabindex } from '../../core/utils';
34

45
/**
56
* Determines if an element is in the focus order, but would not be if its
@@ -12,7 +13,7 @@ import isNativelyFocusable from './is-natively-focusable';
1213
* if its tabindex were removed. Else, false.
1314
*/
1415
function insertedIntoFocusOrder(el) {
15-
const tabIndex = parseInt(el.getAttribute('tabindex'), 10);
16+
const tabIndex = parseTabindex(el.getAttribute('tabindex'));
1617

1718
// an element that has an invalid tabindex will return 0 or -1 based on
1819
// if it is natively focusable or not, which will always be false for this

lib/commons/dom/is-focusable.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import focusDisabled from './focus-disabled';
22
import isNativelyFocusable from './is-natively-focusable';
33
import { nodeLookup } from '../../core/utils';
4+
import { parseTabindex } from '../../core/utils';
45

56
/**
67
* Determines if an element is keyboard or programmatically focusable.
@@ -23,10 +24,6 @@ export default function isFocusable(el) {
2324
return true;
2425
}
2526
// check if the tabindex is specified and a parseable number
26-
const tabindex = vNode.attr('tabindex');
27-
if (tabindex && !isNaN(parseInt(tabindex, 10))) {
28-
return true;
29-
}
30-
31-
return false;
27+
const tabindex = parseTabindex(vNode.attr('tabindex'));
28+
return tabindex !== null;
3229
}

lib/commons/dom/is-in-tab-order.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { nodeLookup } from '../../core/utils';
22
import isFocusable from './is-focusable';
3+
import { parseTabindex } from '../../core/utils';
34

45
/**
56
* Determines if an element is focusable and able to be tabbed to.
@@ -16,7 +17,7 @@ export default function isInTabOrder(el) {
1617
return false;
1718
}
1819

19-
const tabindex = parseInt(vNode.attr('tabindex', 10));
20+
const tabindex = parseTabindex(vNode.attr('tabindex'));
2021
if (tabindex <= -1) {
2122
return false; // Elements with tabindex=-1 are never in the tab order
2223
}

lib/core/base/context/create-frame-context.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { parseTabindex } from '../../utils';
2+
13
export function createFrameContext(frame, { focusable, page }) {
24
return {
35
node: frame,
@@ -11,12 +13,8 @@ export function createFrameContext(frame, { focusable, page }) {
1113
}
1214

1315
function frameFocusable(frame) {
14-
const tabIndex = frame.getAttribute('tabindex');
15-
if (!tabIndex) {
16-
return true;
17-
}
18-
const int = parseInt(tabIndex, 10);
19-
return isNaN(int) || int >= 0;
16+
const tabIndex = parseTabindex(frame.getAttribute('tabindex'));
17+
return tabIndex === null || tabIndex >= 0;
2018
}
2119

2220
function getBoundingSize(domNode) {

lib/core/utils/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export { default as objectHasOwn } from './object-has-own';
7171
export { default as parseCrossOriginStylesheet } from './parse-crossorigin-stylesheet';
7272
export { default as parseSameOriginStylesheet } from './parse-sameorigin-stylesheet';
7373
export { default as parseStylesheet } from './parse-stylesheet';
74+
export { default as parseTabindex } from './parse-tabindex';
7475
export { default as performanceTimer } from './performance-timer';
7576
export { pollyfillElementsFromPoint } from './pollyfill-elements-from-point';
7677
export { default as preloadCssom } from './preload-cssom';

lib/core/utils/parse-tabindex.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Parses a tabindex value to return an integer if valid, or null if invalid.
3+
* @method parseTabindex
4+
* @memberof axe.utils
5+
* @param {string|null} str
6+
* @return {number|null}
7+
*/
8+
function parseTabindex(value) {
9+
if (typeof value !== 'string') {
10+
return null;
11+
}
12+
13+
// spec: https://html.spec.whatwg.org/#rules-for-parsing-integers
14+
const match = value.trim().match(/^([-+]?\d+)/);
15+
if (match) {
16+
return Number(match[1]);
17+
}
18+
19+
return null;
20+
}
21+
22+
export default parseTabindex;

0 commit comments

Comments
 (0)