diff --git a/lib/checks/keyboard/focusable-no-name-evaluate.js b/lib/checks/keyboard/focusable-no-name-evaluate.js index 78b8f90c32..5da1dc08a8 100644 --- a/lib/checks/keyboard/focusable-no-name-evaluate.js +++ b/lib/checks/keyboard/focusable-no-name-evaluate.js @@ -2,12 +2,16 @@ import { isFocusable } from '../../commons/dom'; import { accessibleTextVirtual } from '../../commons/text'; function focusableNoNameEvaluate(node, options, virtualNode) { - var tabIndex = node.getAttribute('tabindex'), - inFocusOrder = isFocusable(node) && tabIndex > -1; - if (!inFocusOrder) { - return false; + try { + const tabIndex = virtualNode.attr('tabindex'); + const inFocusOrder = isFocusable(virtualNode) && tabIndex > -1; + if (!inFocusOrder) { + return false; + } + return !accessibleTextVirtual(virtualNode); + } catch (e) { + return undefined; } - return !accessibleTextVirtual(virtualNode); } export default focusableNoNameEvaluate; diff --git a/lib/checks/keyboard/focusable-no-name.json b/lib/checks/keyboard/focusable-no-name.json index ed401ad79d..81708f47a3 100644 --- a/lib/checks/keyboard/focusable-no-name.json +++ b/lib/checks/keyboard/focusable-no-name.json @@ -5,7 +5,8 @@ "impact": "serious", "messages": { "pass": "Element is not in tab order or has accessible text", - "fail": "Element is in tab order and does not have accessible text" + "fail": "Element is in tab order and does not have accessible text", + "incomplete": "Unable to determine if element has an accessible name" } } } diff --git a/lib/commons/dom/focus-disabled.js b/lib/commons/dom/focus-disabled.js index e89036db1c..258793445e 100644 --- a/lib/commons/dom/focus-disabled.js +++ b/lib/commons/dom/focus-disabled.js @@ -1,14 +1,30 @@ +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; import isHiddenWithCSS from './is-hidden-with-css'; /** * Determines if focusing has been disabled on an element. - * @param {HTMLElement} el The HTMLElement + * @param {HTMLElement|VirtualNode} el The HTMLElement * @return {Boolean} Whether focusing has been disabled on an element. */ function focusDisabled(el) { - return ( - el.disabled || (el.nodeName.toUpperCase() !== 'AREA' && isHiddenWithCSS(el)) - ); + const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el); + + if (vNode.hasAttr('disabled')) { + return true; + } + + if (vNode.props.nodeName !== 'area') { + // if the virtual node does not have an actual node, treat it + // as not hidden + if (!vNode.actualNode) { + return false; + } + + return isHiddenWithCSS(vNode.actualNode); + } + + return false; } export default focusDisabled; diff --git a/lib/commons/dom/is-focusable.js b/lib/commons/dom/is-focusable.js index f83cc75155..f92da32c5e 100644 --- a/lib/commons/dom/is-focusable.js +++ b/lib/commons/dom/is-focusable.js @@ -1,5 +1,7 @@ import focusDisabled from './focus-disabled'; import isNativelyFocusable from './is-natively-focusable'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; /** * Determines if an element is focusable @@ -11,14 +13,15 @@ import isNativelyFocusable from './is-natively-focusable'; */ function isFocusable(el) { - 'use strict'; - if (focusDisabled(el)) { + const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el); + + if (focusDisabled(vNode)) { return false; - } else if (isNativelyFocusable(el)) { + } else if (isNativelyFocusable(vNode)) { return true; } // check if the tabindex is specified and a parseable number - var tabindex = el.getAttribute('tabindex'); + var tabindex = vNode.attr('tabindex'); if (tabindex && !isNaN(parseInt(tabindex, 10))) { return true; } diff --git a/lib/commons/dom/is-natively-focusable.js b/lib/commons/dom/is-natively-focusable.js index 4374034008..ce3944c470 100644 --- a/lib/commons/dom/is-natively-focusable.js +++ b/lib/commons/dom/is-natively-focusable.js @@ -1,3 +1,5 @@ +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree, querySelectorAll } from '../../core/utils'; import focusDisabled from './focus-disabled'; /** @@ -5,34 +7,33 @@ import focusDisabled from './focus-disabled'; * @method isNativelyFocusable * @memberof axe.commons.dom * @instance - * @param {HTMLElement} el The HTMLElement + * @param {HTMLElement|VirtualNode} el The HTMLElement * @return {Boolean} True if the element is in the focus order but wouldn't be * if its tabindex were removed. Else, false. */ function isNativelyFocusable(el) { - /* eslint indent: 0*/ - 'use strict'; + const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el); - if (!el || focusDisabled(el)) { + if (!vNode || focusDisabled(vNode)) { return false; } - switch (el.nodeName.toUpperCase()) { - case 'A': - case 'AREA': - if (el.href) { + switch (vNode.props.nodeName) { + case 'a': + case 'area': + if (vNode.hasAttr('href')) { return true; } break; - case 'INPUT': - return el.type !== 'hidden'; - case 'TEXTAREA': - case 'SELECT': - case 'SUMMARY': - case 'BUTTON': + case 'input': + return vNode.props.type !== 'hidden'; + case 'textarea': + case 'select': + case 'summary': + case 'button': return true; - case 'DETAILS': - return !el.querySelector('summary'); + case 'details': + return !querySelectorAll(vNode, 'summary').length; } return false; } diff --git a/lib/commons/text/title-text.js b/lib/commons/text/title-text.js index ab9d7b6c32..9e76b3d624 100644 --- a/lib/commons/text/title-text.js +++ b/lib/commons/text/title-text.js @@ -1,29 +1,33 @@ import matches from '../matches/matches'; import getRole from '../aria/get-role'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; const alwaysTitleElements = ['iframe']; /** * Get title text - * @param {HTMLElement}node the node to verify + * @param {HTMLElement|VirtualNode}node the node to verify * @return {String} */ function titleText(node) { - node = node.actualNode || node; - if (node.nodeType !== 1 || !node.hasAttribute('title')) { + const vNode = + node instanceof AbstractVirtualNode ? node : getNodeFromTree(node); + + if (vNode.props.nodeType !== 1 || !node.hasAttr('title')) { return ''; } // Some elements return the title even with role=presentation // This does appear in any spec, but its remarkably consistent if ( - !matches(node, alwaysTitleElements) && - ['none', 'presentation'].includes(getRole(node)) + !matches(vNode, alwaysTitleElements) && + ['none', 'presentation'].includes(getRole(vNode)) ) { return ''; } - return node.getAttribute('title'); + return vNode.attr('title'); } export default titleText; diff --git a/test/checks/keyboard/focusable-no-name.js b/test/checks/keyboard/focusable-no-name.js index 2d32e8bddf..20702d4f41 100644 --- a/test/checks/keyboard/focusable-no-name.js +++ b/test/checks/keyboard/focusable-no-name.js @@ -76,4 +76,96 @@ describe('focusable-no-name', function() { ); } ); + + describe('Serial Virtual Node', function() { + it('should pass if tabindex < 0', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'a', + attributes: { + tabindex: '-1', + href: '#' + } + }); + + assert.isFalse( + axe.testUtils.getCheckEvaluate('focusable-no-name')( + null, + {}, + serialNode + ) + ); + }); + + it('should pass element is not natively focusable', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'span', + attributes: { + role: 'link', + href: '#' + } + }); + + assert.isFalse( + axe.testUtils.getCheckEvaluate('focusable-no-name')( + null, + {}, + serialNode + ) + ); + }); + + it('should fail if element is tabbable with no name - native', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'a', + attributes: { + href: '#' + } + }); + serialNode.children = []; + + assert.isTrue( + axe.testUtils.getCheckEvaluate('focusable-no-name')( + null, + {}, + serialNode + ) + ); + }); + + it('should return undefined if element is tabbable with no name nor children - native', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'a', + attributes: { + href: '#' + } + }); + + assert.isUndefined( + axe.testUtils.getCheckEvaluate('focusable-no-name')( + null, + {}, + serialNode + ) + ); + }); + + it('should pass if the element is tabbable but has an accessible name', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'a', + attributes: { + href: '#', + title: 'Hello' + } + }); + serialNode.children = []; + + assert.isFalse( + axe.testUtils.getCheckEvaluate('focusable-no-name')( + null, + {}, + serialNode + ) + ); + }); + }); }); diff --git a/test/commons/dom/is-focusable.js b/test/commons/dom/is-focusable.js index 5a91162b63..c63fd9988e 100644 --- a/test/commons/dom/is-focusable.js +++ b/test/commons/dom/is-focusable.js @@ -17,6 +17,7 @@ describe('is-focusable', function() { } var fixtureSetup = axe.testUtils.fixtureSetup; + var flatTreeSetup = axe.testUtils.flatTreeSetup; describe('dom.isFocusable', function() { 'use strict'; @@ -30,6 +31,7 @@ describe('is-focusable', function() { it('should return true for visible, enabled textareas', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isTrue(axe.commons.dom.isFocusable(el)); }); @@ -37,6 +39,7 @@ describe('is-focusable', function() { it('should return true for visible, enabled selects', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isTrue(axe.commons.dom.isFocusable(el)); }); @@ -44,6 +47,7 @@ describe('is-focusable', function() { it('should return true for visible, enabled buttons', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isTrue(axe.commons.dom.isFocusable(el)); }); @@ -51,6 +55,7 @@ describe('is-focusable', function() { it('should return true for visible, enabled, non-hidden inputs', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isTrue(axe.commons.dom.isFocusable(el)); }); @@ -58,6 +63,7 @@ describe('is-focusable', function() { it('should return false for disabled elements', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isFalse(axe.commons.dom.isFocusable(el)); }); @@ -65,6 +71,7 @@ describe('is-focusable', function() { it('should return false for hidden inputs', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isFalse(axe.commons.dom.isFocusable(el)); }); @@ -72,6 +79,7 @@ describe('is-focusable', function() { it('should return false for hidden inputs with tabindex', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isFalse(axe.commons.dom.isFocusable(el)); }); @@ -80,6 +88,7 @@ describe('is-focusable', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isFalse(axe.commons.dom.isFocusable(el)); }); @@ -87,6 +96,7 @@ describe('is-focusable', function() { it('should return false for disabled buttons with tabindex', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isFalse(axe.commons.dom.isFocusable(el)); }); @@ -95,6 +105,7 @@ describe('is-focusable', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isFalse(axe.commons.dom.isFocusable(el)); }); @@ -102,6 +113,7 @@ describe('is-focusable', function() { it('should return true for an anchor with an href', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isTrue(axe.commons.dom.isFocusable(el)); }); @@ -109,6 +121,7 @@ describe('is-focusable', function() { it('should return false for an anchor with no href', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isFalse(axe.commons.dom.isFocusable(el)); }); @@ -116,6 +129,7 @@ describe('is-focusable', function() { it('should return true for a div with a tabindex with spaces', function() { fixture.innerHTML = '
'; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isTrue(axe.commons.dom.isFocusable(el)); }); @@ -123,6 +137,7 @@ describe('is-focusable', function() { it('should return true for a div with a tabindex', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isTrue(axe.commons.dom.isFocusable(el)); }); @@ -130,6 +145,7 @@ describe('is-focusable', function() { it('should return false for a div with a non-numeric tabindex', function() { fixture.innerHTML = ''; var el = document.getElementById('target'); + flatTreeSetup(fixture); assert.isFalse(axe.commons.dom.isFocusable(el)); }); @@ -138,6 +154,7 @@ describe('is-focusable', function() { fixture.innerHTML = 'Detail
Detail
Detail
Detail
Detail
Detail