Skip to content

Commit

Permalink
fix(focusable-no-name): work with serial virtual nodes (#2399)
Browse files Browse the repository at this point in the history
* fix(focusable-no-name): work with serial virtual nodes

* allow to incomplete
  • Loading branch information
straker authored Jul 20, 2020
1 parent 18c22fd commit 1ef3066
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 36 deletions.
14 changes: 9 additions & 5 deletions lib/checks/keyboard/focusable-no-name-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
3 changes: 2 additions & 1 deletion lib/checks/keyboard/focusable-no-name.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
24 changes: 20 additions & 4 deletions lib/commons/dom/focus-disabled.js
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 7 additions & 4 deletions lib/commons/dom/is-focusable.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}
Expand Down
33 changes: 17 additions & 16 deletions lib/commons/dom/is-natively-focusable.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node';
import { getNodeFromTree, querySelectorAll } from '../../core/utils';
import focusDisabled from './focus-disabled';

/**
* Determines if an element is focusable without considering its tabindex
* @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;
}
Expand Down
16 changes: 10 additions & 6 deletions lib/commons/text/title-text.js
Original file line number Diff line number Diff line change
@@ -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;
92 changes: 92 additions & 0 deletions test/checks/keyboard/focusable-no-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
);
});
});
});
Loading

0 comments on commit 1ef3066

Please sign in to comment.