Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(focusable-no-name): work with serial virtual nodes #2399

Merged
merged 3 commits into from
Jul 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
straker marked this conversation as resolved.
Show resolved Hide resolved
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