From 8fe6af8fd011f40dba24d5af30f782d05e4c5fa3 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Wed, 24 Aug 2022 15:20:31 -0600 Subject: [PATCH 01/13] feat(is-in-tab-order): add commons.isInTabOrder() --- lib/commons/dom/index.js | 1 + lib/commons/dom/is-in-tab-order.js | 30 ++++++++++ test/commons/dom/is-in-tab-order.js | 85 +++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 lib/commons/dom/is-in-tab-order.js create mode 100644 test/commons/dom/is-in-tab-order.js diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 5f80f87ca6..0e23bfa913 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -24,6 +24,7 @@ export { default as isCurrentPageLink } from './is-current-page-link'; export { default as isFocusable } from './is-focusable'; export { default as isHiddenWithCSS } from './is-hidden-with-css'; export { default as isHTML5 } from './is-html5'; +export { default as isInTabOrder } from './is-in-tab-order'; export { default as isInTextBlock } from './is-in-text-block'; export { default as isModalOpen } from './is-modal-open'; export { default as isMultiline } from './is-multiline'; diff --git a/lib/commons/dom/is-in-tab-order.js b/lib/commons/dom/is-in-tab-order.js new file mode 100644 index 0000000000..4941030481 --- /dev/null +++ b/lib/commons/dom/is-in-tab-order.js @@ -0,0 +1,30 @@ +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; +import isFocusable from './is-focusable'; + +/** + * Determines if an element is focusable and able to be tabbed to. + * @method isInTabOrder + * @memberof axe.commons.dom + * @instance + * @param {HTMLElement} el The HTMLElement + * @return {Boolean} The element's tabindex status + */ +export default function isInTabOrder(el) { + const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el); + + if (vNode.props.nodeType !== 1) { + return false; + } + + var focusable = isFocusable(vNode); + var tabindex = vNode.attr('tabindex'); + + if (focusable && tabindex && parseInt(tabindex, 10) >= 0) { + return true; + } else if (focusable && isNaN(parseInt(tabindex, 10))) { + return true; + } + + return false; +} diff --git a/test/commons/dom/is-in-tab-order.js b/test/commons/dom/is-in-tab-order.js new file mode 100644 index 0000000000..654d7875dd --- /dev/null +++ b/test/commons/dom/is-in-tab-order.js @@ -0,0 +1,85 @@ +describe('dom.isInTabOrder', function () { + 'use strict'; + + var queryFixture = axe.testUtils.queryFixture; + var isInTabOrder = axe.commons.dom.isInTabOrder; + + it('should return false for presentation element with negative tabindex', function () { + var target = queryFixture('
'); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return true for presentation element with positive tabindex', function () { + var target = queryFixture('
'); + assert.isTrue(isInTabOrder(target)); + }); + + it('should return false for presentation element with tabindex not set', function () { + var target = queryFixture('
'); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return false for presentation element with tabindex set to non-parseable value', function () { + var target = queryFixture('
'); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return false for presentation element with tabindex not set and role of natively focusable element', function () { + var target = queryFixture('
'); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return true for natively focusable element with tabindex 0', function () { + var target = queryFixture(''); + assert.isTrue(isInTabOrder(target)); + }); + + it('should return true for natively focusable element with tabindex 1', function () { + var target = queryFixture(''); + assert.isTrue(isInTabOrder(target)); + }); + + it('should return false for natively focusable element with tabindex -1', function () { + var target = queryFixture(''); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return true for natively focusable element with tabindex not set', function () { + var target = queryFixture(''); + assert.isTrue(isInTabOrder(target)); + }); + + it('should return true for natively focusable element with tabindex set to empty string', function () { + var target = queryFixture(''); + assert.isTrue(isInTabOrder(target)); + }); + + it('should return true for natively focusable element with tabindex set to non-parseable value', function () { + var target = queryFixture( + '' + ); + assert.isTrue(isInTabOrder(target)); + }); + + it('should return false for disabled', function () { + var target = queryFixture(''); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return false for disabled natively focusable element with tabindex', function () { + var target = queryFixture( + '' + ); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return false for hidden inputs', function () { + var target = queryFixture(''); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return false for non-element nodes', function () { + var target = queryFixture('Hello World'); + assert.isFalse(isInTabOrder(target)); + }); +}); From 27b2b78db8e141e2aec19b2d0ae35f1b0c03a495 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Wed, 24 Aug 2022 16:09:43 -0600 Subject: [PATCH 02/13] fix IE / refactor --- lib/commons/dom/is-in-tab-order.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/commons/dom/is-in-tab-order.js b/lib/commons/dom/is-in-tab-order.js index 4941030481..9e3bd93f43 100644 --- a/lib/commons/dom/is-in-tab-order.js +++ b/lib/commons/dom/is-in-tab-order.js @@ -17,14 +17,7 @@ export default function isInTabOrder(el) { return false; } - var focusable = isFocusable(vNode); - var tabindex = vNode.attr('tabindex'); + const tabindex = parseInt(vNode.attr('tabindex', 10)); - if (focusable && tabindex && parseInt(tabindex, 10) >= 0) { - return true; - } else if (focusable && isNaN(parseInt(tabindex, 10))) { - return true; - } - - return false; + return isFocusable(vNode) && (isNaN(tabindex) || tabindex > -1); } From efae2c367792fdf60562fab0ed0f2b25eda33f40 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Wed, 24 Aug 2022 16:37:01 -0600 Subject: [PATCH 03/13] skip test for IE11 --- test/commons/dom/is-in-tab-order.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/commons/dom/is-in-tab-order.js b/test/commons/dom/is-in-tab-order.js index 654d7875dd..f91946d198 100644 --- a/test/commons/dom/is-in-tab-order.js +++ b/test/commons/dom/is-in-tab-order.js @@ -3,6 +3,7 @@ describe('dom.isInTabOrder', function () { var queryFixture = axe.testUtils.queryFixture; var isInTabOrder = axe.commons.dom.isInTabOrder; + var isIE11 = axe.testUtils.isIE11; it('should return false for presentation element with negative tabindex', function () { var target = queryFixture('
'); @@ -49,10 +50,14 @@ describe('dom.isInTabOrder', function () { assert.isTrue(isInTabOrder(target)); }); - it('should return true for natively focusable element with tabindex set to empty string', function () { - var target = queryFixture(''); - assert.isTrue(isInTabOrder(target)); - }); + // IE11 returns a negative tabindex for elements with tabindex set to an empty string, rather than create false positives, skip it + (isIE11 ? xit : it)( + 'should return true for natively focusable element with tabindex set to empty string', + function () { + var target = queryFixture(''); + assert.isTrue(isInTabOrder(target)); + } + ); it('should return true for natively focusable element with tabindex set to non-parseable value', function () { var target = queryFixture( From 0c8243d3c6b0445bb9dbcd57d72ebf64a022bc3a Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Thu, 25 Aug 2022 22:52:42 -0600 Subject: [PATCH 04/13] Apply suggestions from code review Co-authored-by: Wilco Fiers --- lib/commons/dom/is-in-tab-order.js | 5 ++++- test/commons/dom/is-in-tab-order.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/commons/dom/is-in-tab-order.js b/lib/commons/dom/is-in-tab-order.js index 9e3bd93f43..5dcc99493b 100644 --- a/lib/commons/dom/is-in-tab-order.js +++ b/lib/commons/dom/is-in-tab-order.js @@ -18,6 +18,9 @@ export default function isInTabOrder(el) { } const tabindex = parseInt(vNode.attr('tabindex', 10)); + if (tabindex <= -1) { + return false; // Elements with tabindex=-1 are never in the tab order + } - return isFocusable(vNode) && (isNaN(tabindex) || tabindex > -1); + return isFocusable(vNode); } diff --git a/test/commons/dom/is-in-tab-order.js b/test/commons/dom/is-in-tab-order.js index f91946d198..3befacac24 100644 --- a/test/commons/dom/is-in-tab-order.js +++ b/test/commons/dom/is-in-tab-order.js @@ -87,4 +87,14 @@ describe('dom.isInTabOrder', function () { var target = queryFixture('Hello World'); assert.isFalse(isInTabOrder(target)); }); + + it('should return false natively focusable hidden element', function () { + var target = queryFixture(''); + assert.isTrue(isInTabOrder(target)); + }); + + it('should return false hidden element with tabindex 1', function () { + var target = queryFixture(''); + assert.isTrue(isInTabOrder(target)); + }); }); From b12541fb144026aae0a01b728a8096f68d001d31 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Thu, 25 Aug 2022 23:24:18 -0600 Subject: [PATCH 05/13] assert should match description --- test/commons/dom/is-in-tab-order.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/commons/dom/is-in-tab-order.js b/test/commons/dom/is-in-tab-order.js index 3befacac24..f30096b0d5 100644 --- a/test/commons/dom/is-in-tab-order.js +++ b/test/commons/dom/is-in-tab-order.js @@ -90,11 +90,11 @@ describe('dom.isInTabOrder', function () { it('should return false natively focusable hidden element', function () { var target = queryFixture(''); - assert.isTrue(isInTabOrder(target)); + assert.isFalse(isInTabOrder(target)); }); it('should return false hidden element with tabindex 1', function () { var target = queryFixture(''); - assert.isTrue(isInTabOrder(target)); + assert.isFalse(isInTabOrder(target)); }); }); From 79ad5f9852911fdb61c865c38cf50ecddb61db53 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Thu, 25 Aug 2022 23:40:36 -0600 Subject: [PATCH 06/13] convert to isInTabOrder --- lib/commons/dom/get-tabbable-elements.js | 8 ++------ lib/commons/dom/inserted-into-focus-order.js | 10 ++-------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/commons/dom/get-tabbable-elements.js b/lib/commons/dom/get-tabbable-elements.js index 2836635643..1ee00db649 100644 --- a/lib/commons/dom/get-tabbable-elements.js +++ b/lib/commons/dom/get-tabbable-elements.js @@ -1,3 +1,4 @@ +import { isInTabOrder } from '.'; import { querySelectorAll } from '../../core/utils'; /** @@ -12,12 +13,7 @@ function getTabbableElements(virtualNode) { const nodeAndDescendents = querySelectorAll(virtualNode, '*'); const tabbableElements = nodeAndDescendents.filter(vNode => { - const isFocusable = vNode.isFocusable; - let tabIndex = vNode.actualNode.getAttribute('tabindex'); - tabIndex = - tabIndex && !isNaN(parseInt(tabIndex, 10)) ? parseInt(tabIndex) : null; - - return tabIndex ? isFocusable && tabIndex >= 0 : isFocusable; + return isInTabOrder(vNode); }); return tabbableElements; diff --git a/lib/commons/dom/inserted-into-focus-order.js b/lib/commons/dom/inserted-into-focus-order.js index 1eb927a891..f38e90aa2a 100644 --- a/lib/commons/dom/inserted-into-focus-order.js +++ b/lib/commons/dom/inserted-into-focus-order.js @@ -1,4 +1,4 @@ -import isFocusable from './is-focusable'; +import isInTabOrder from './is-in-tab-order'; import isNativelyFocusable from './is-natively-focusable'; /** @@ -12,13 +12,7 @@ import isNativelyFocusable from './is-natively-focusable'; * if its tabindex were removed. Else, false. */ function insertedIntoFocusOrder(el) { - const tabIndex = parseInt(el.getAttribute('tabindex'), 10); - - // an element that has an invalid tabindex will return 0 or -1 based on - // if it is natively focusable or not, which will always be false for this - // check as NaN is not > 1 - // @see https://www.w3.org/TR/html51/editing.html#the-tabindex-attribute - return tabIndex > -1 && isFocusable(el) && !isNativelyFocusable(el); + return isInTabOrder(el) && !isNativelyFocusable(el); } export default insertedIntoFocusOrder; From 08073f7c2143aeda417917aa3065e9cfdfd40992 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Fri, 26 Aug 2022 08:41:57 -0600 Subject: [PATCH 07/13] use isInTabOrder --- lib/checks/keyboard/frame-focusable-content-evaluate.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/checks/keyboard/frame-focusable-content-evaluate.js b/lib/checks/keyboard/frame-focusable-content-evaluate.js index 5c746c9347..0b48657c46 100644 --- a/lib/checks/keyboard/frame-focusable-content-evaluate.js +++ b/lib/checks/keyboard/frame-focusable-content-evaluate.js @@ -1,4 +1,4 @@ -import isFocusable from '../../commons/dom/is-focusable'; +import { isInTabOrder } from '../../commons/dom'; export default function frameFocusableContentEvaluate( node, @@ -19,8 +19,7 @@ export default function frameFocusableContentEvaluate( } function focusableDescendants(vNode) { - const tabIndex = parseInt(vNode.attr('tabindex'), 10); - if ((isNaN(tabIndex) || tabIndex > -1) && isFocusable(vNode)) { + if (isInTabOrder(vNode)) { return true; } From 096c6b5a891bd81c3578ad48b385ecb740f2e1a3 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Fri, 26 Aug 2022 11:40:08 -0600 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> --- test/commons/dom/is-in-tab-order.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/commons/dom/is-in-tab-order.js b/test/commons/dom/is-in-tab-order.js index f30096b0d5..adaab08641 100644 --- a/test/commons/dom/is-in-tab-order.js +++ b/test/commons/dom/is-in-tab-order.js @@ -88,12 +88,12 @@ describe('dom.isInTabOrder', function () { assert.isFalse(isInTabOrder(target)); }); - it('should return false natively focusable hidden element', function () { + it('should return false for natively focusable hidden element', function () { var target = queryFixture(''); assert.isFalse(isInTabOrder(target)); }); - it('should return false hidden element with tabindex 1', function () { + it('should return for false hidden element with tabindex 1', function () { var target = queryFixture(''); assert.isFalse(isInTabOrder(target)); }); From 270cd7c00b39c08d08bd165445ce888bcf6254a9 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Fri, 26 Aug 2022 11:46:45 -0600 Subject: [PATCH 09/13] assert against text node --- test/commons/dom/is-in-tab-order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commons/dom/is-in-tab-order.js b/test/commons/dom/is-in-tab-order.js index adaab08641..d427155efd 100644 --- a/test/commons/dom/is-in-tab-order.js +++ b/test/commons/dom/is-in-tab-order.js @@ -85,7 +85,7 @@ describe('dom.isInTabOrder', function () { it('should return false for non-element nodes', function () { var target = queryFixture('Hello World'); - assert.isFalse(isInTabOrder(target)); + assert.isFalse(isInTabOrder(target.children[0])); }); it('should return false for natively focusable hidden element', function () { From 585eff01e8fa817d4fd0491d51cb01a377153508 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Fri, 26 Aug 2022 14:35:02 -0600 Subject: [PATCH 10/13] use isInTabOrder --- lib/checks/keyboard/focusable-element-evaluate.js | 7 ++----- lib/checks/keyboard/no-focusable-content-evaluate.js | 8 ++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/checks/keyboard/focusable-element-evaluate.js b/lib/checks/keyboard/focusable-element-evaluate.js index 53784aa1e1..fa7c88b09e 100644 --- a/lib/checks/keyboard/focusable-element-evaluate.js +++ b/lib/checks/keyboard/focusable-element-evaluate.js @@ -1,3 +1,4 @@ +import { isInTabOrder } from '../../commons/dom'; import { closest } from '../../core/utils'; function focusableElementEvaluate(node, options, virtualNode) { @@ -14,11 +15,7 @@ function focusableElementEvaluate(node, options, virtualNode) { return true; } - const isFocusable = virtualNode.isFocusable; - let tabIndex = parseInt(virtualNode.attr('tabindex'), 10); - tabIndex = !isNaN(tabIndex) ? tabIndex : null; - - return tabIndex ? isFocusable && tabIndex >= 0 : isFocusable; + return isInTabOrder(virtualNode); // contenteditable is focusable when it is an empty string (whitespace // is not considered empty) or "true". if the value is "false" diff --git a/lib/checks/keyboard/no-focusable-content-evaluate.js b/lib/checks/keyboard/no-focusable-content-evaluate.js index f72bad79d7..30436ee8e7 100644 --- a/lib/checks/keyboard/no-focusable-content-evaluate.js +++ b/lib/checks/keyboard/no-focusable-content-evaluate.js @@ -1,5 +1,6 @@ import isFocusable from '../../commons/dom/is-focusable'; import { getRole, getRoleType } from '../../commons/aria'; +import { isInTabOrder } from '../../commons/dom'; export default function noFocusableContentEvaluate(node, options, virtualNode) { if (!virtualNode.children) { @@ -14,7 +15,7 @@ export default function noFocusableContentEvaluate(node, options, virtualNode) { } const notHiddenElements = focusableDescendants.filter( - usesUnreliableHidingStrategy + vNode => !isInTabOrder(vNode) ); if (notHiddenElements.length > 0) { @@ -50,8 +51,3 @@ function getFocusableDescendants(vNode) { }); return retVal; } - -function usesUnreliableHidingStrategy(vNode) { - const tabIndex = parseInt(vNode.attr('tabindex'), 10); - return !isNaN(tabIndex) && tabIndex < 0; -} From f96a5a4ea94222562e1ea81d432b001f00a0f820 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Mon, 29 Aug 2022 14:55:39 -0600 Subject: [PATCH 11/13] revert --- lib/commons/dom/get-tabbable-elements.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/commons/dom/get-tabbable-elements.js b/lib/commons/dom/get-tabbable-elements.js index 1ee00db649..2836635643 100644 --- a/lib/commons/dom/get-tabbable-elements.js +++ b/lib/commons/dom/get-tabbable-elements.js @@ -1,4 +1,3 @@ -import { isInTabOrder } from '.'; import { querySelectorAll } from '../../core/utils'; /** @@ -13,7 +12,12 @@ function getTabbableElements(virtualNode) { const nodeAndDescendents = querySelectorAll(virtualNode, '*'); const tabbableElements = nodeAndDescendents.filter(vNode => { - return isInTabOrder(vNode); + const isFocusable = vNode.isFocusable; + let tabIndex = vNode.actualNode.getAttribute('tabindex'); + tabIndex = + tabIndex && !isNaN(parseInt(tabIndex, 10)) ? parseInt(tabIndex) : null; + + return tabIndex ? isFocusable && tabIndex >= 0 : isFocusable; }); return tabbableElements; From 667f5eed807b7bff0cbd33a4327da49b3fd56881 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Mon, 29 Aug 2022 14:56:20 -0600 Subject: [PATCH 12/13] revert --- lib/checks/keyboard/no-focusable-content-evaluate.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/checks/keyboard/no-focusable-content-evaluate.js b/lib/checks/keyboard/no-focusable-content-evaluate.js index 30436ee8e7..f72bad79d7 100644 --- a/lib/checks/keyboard/no-focusable-content-evaluate.js +++ b/lib/checks/keyboard/no-focusable-content-evaluate.js @@ -1,6 +1,5 @@ import isFocusable from '../../commons/dom/is-focusable'; import { getRole, getRoleType } from '../../commons/aria'; -import { isInTabOrder } from '../../commons/dom'; export default function noFocusableContentEvaluate(node, options, virtualNode) { if (!virtualNode.children) { @@ -15,7 +14,7 @@ export default function noFocusableContentEvaluate(node, options, virtualNode) { } const notHiddenElements = focusableDescendants.filter( - vNode => !isInTabOrder(vNode) + usesUnreliableHidingStrategy ); if (notHiddenElements.length > 0) { @@ -51,3 +50,8 @@ function getFocusableDescendants(vNode) { }); return retVal; } + +function usesUnreliableHidingStrategy(vNode) { + const tabIndex = parseInt(vNode.attr('tabindex'), 10); + return !isNaN(tabIndex) && tabIndex < 0; +} From a3dec8249f2e80a8b5e97835e7d64dcef95a7c9f Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Mon, 29 Aug 2022 15:00:07 -0600 Subject: [PATCH 13/13] revert --- lib/commons/dom/inserted-into-focus-order.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/commons/dom/inserted-into-focus-order.js b/lib/commons/dom/inserted-into-focus-order.js index f38e90aa2a..1eb927a891 100644 --- a/lib/commons/dom/inserted-into-focus-order.js +++ b/lib/commons/dom/inserted-into-focus-order.js @@ -1,4 +1,4 @@ -import isInTabOrder from './is-in-tab-order'; +import isFocusable from './is-focusable'; import isNativelyFocusable from './is-natively-focusable'; /** @@ -12,7 +12,13 @@ import isNativelyFocusable from './is-natively-focusable'; * if its tabindex were removed. Else, false. */ function insertedIntoFocusOrder(el) { - return isInTabOrder(el) && !isNativelyFocusable(el); + const tabIndex = parseInt(el.getAttribute('tabindex'), 10); + + // an element that has an invalid tabindex will return 0 or -1 based on + // if it is natively focusable or not, which will always be false for this + // check as NaN is not > 1 + // @see https://www.w3.org/TR/html51/editing.html#the-tabindex-attribute + return tabIndex > -1 && isFocusable(el) && !isNativelyFocusable(el); } export default insertedIntoFocusOrder;