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(nested-interactive): add focusable descendants as related nodes #3261

Merged
merged 3 commits into from
Nov 12, 2021
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
52 changes: 30 additions & 22 deletions lib/checks/keyboard/no-focusable-content-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import isFocusable from '../../commons/dom/is-focusable';

export default function noFocusableContentEvaluate(node, options, virtualNode) {
if (!virtualNode.children) {
return undefined;
}

try {
const focusableDescendants = getFocusableDescendants(virtualNode);

if (!focusableDescendants.length) {
return true;
}

const notHiddenElements = focusableDescendants.filter(
usesUnreliableHidingStrategy
);

if (notHiddenElements.length > 0) {
this.data({ messageKey: 'notHidden' });
this.relatedNodes(notHiddenElements);
} else {
this.relatedNodes(focusableDescendants);
}

return false;
} catch (e) {
return undefined;
}
}

function getFocusableDescendants(vNode) {
if (!vNode.children) {
if (vNode.props.nodeType === 1) {
Expand All @@ -11,7 +40,7 @@ function getFocusableDescendants(vNode) {

const retVal = [];
vNode.children.forEach(child => {
if(isFocusable(child)) {
if (isFocusable(child)) {
retVal.push(child);
} else {
retVal.push(...getFocusableDescendants(child));
Expand All @@ -24,24 +53,3 @@ function usesUnreliableHidingStrategy(vNode) {
const tabIndex = parseInt(vNode.attr('tabindex'), 10);
return !isNaN(tabIndex) && tabIndex < 0;
}

function noFocusableContentEvaluate(node, options, virtualNode) {
if (!virtualNode.children) {
return undefined;
}
try {
const focusableDescendants = getFocusableDescendants(virtualNode);
if(focusableDescendants.length > 0) {
const notHiddenElements = focusableDescendants.filter(usesUnreliableHidingStrategy);
if(notHiddenElements.length > 0) {
this.data({ messageKey: 'notHidden' });
this.relatedNodes(notHiddenElements);
}
}
return focusableDescendants.length === 0;
} catch (e) {
return undefined;
}
}

export default noFocusableContentEvaluate;
11 changes: 11 additions & 0 deletions lib/core/utils/check-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,18 @@ function checkHelper(checkResult, options, resolve, reject) {
checkResult.data = data;
},
relatedNodes(nodes) {
if (!window.Node) {
return;
}

nodes = nodes instanceof window.Node ? [nodes] : toArray(nodes);

if (
!nodes.every(node => node instanceof window.Node || node.actualNode)
) {
return;
}

checkResult.relatedNodes = nodes.map(element => {
return new DqElement(element, options);
});
Expand Down
31 changes: 25 additions & 6 deletions test/checks/keyboard/no-focusable-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,45 @@ describe('no-focusable-content tests', function() {
});

it('should return false if element has focusable content', function() {
var vNode = queryFixture(
var params = checkSetup(
'<button id="target"><span tabindex="0">Hello</span></button>'
);
assert.isFalse(noFocusableContent(null, null, vNode));

assert.isFalse(noFocusableContent.apply(checkContext, params));
assert.deepEqual(checkContext._data, null);
assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]);
});

it('should return false if element has natively focusable content', function() {
var vNode = queryFixture(
var params = checkSetup(
'<button id="target"><a href="foo.html">Hello</a></button>'
);
assert.isFalse(noFocusableContent(null, null, vNode));

assert.isFalse(noFocusableContent.apply(checkContext, params));
assert.deepEqual(checkContext._data, null);
assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]);
});


it('should add each focusable child as related nodes', function() {
var params = checkSetup(
'<button id="target"><span tabindex="0">Hello</span><a href="foo.html">Hello</a></button>'
);

assert.isFalse(noFocusableContent.apply(checkContext, params));
assert.deepEqual(checkContext._data, null);
assert.deepEqual(checkContext._relatedNodes, [
params[2].children[0],
params[2].children[1]
]);
});

it('should return false if element has natively focusable content with negative tabindex', function() {
var params = checkSetup(
'<button id="target"><a href="foo.html" tabindex="-1">Hello</a></button>'
);
axe.utils.getFlattenedTree(document.documentElement);
assert.isFalse(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, { messageKey: 'notHidden' });
assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]);
});

});
11 changes: 11 additions & 0 deletions test/core/utils/check-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ describe('axe.utils.checkHelper', function() {
assert.equal(target.relatedNodes[0].element, fixture.children[0]);
assert.equal(target.relatedNodes[1].element, fixture.children[1]);
});
it('should noop for non-node-like objects', function() {
var target = {},
helper = axe.utils.checkHelper(target, noop);
var nodes = new axe.SerialVirtualNode({
nodeName: 'div'
});
assert.doesNotThrow(function() {
helper.relatedNodes(nodes);
});
assert.lengthOf(target.relatedNodes, 0);
});
});
});
});