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/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; } 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..5dcc99493b --- /dev/null +++ b/lib/commons/dom/is-in-tab-order.js @@ -0,0 +1,26 @@ +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; + } + + 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); +} 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..d427155efd --- /dev/null +++ b/test/commons/dom/is-in-tab-order.js @@ -0,0 +1,100 @@ +describe('dom.isInTabOrder', function () { + 'use strict'; + + 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('
'); + 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)); + }); + + // 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( + '' + ); + 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.children[0])); + }); + + it('should return false for natively focusable hidden element', function () { + var target = queryFixture(''); + assert.isFalse(isInTabOrder(target)); + }); + + it('should return for false hidden element with tabindex 1', function () { + var target = queryFixture(''); + assert.isFalse(isInTabOrder(target)); + }); +});