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 message for negative tabindex #3194

Merged
merged 42 commits into from
Nov 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2467b3a
refactor(checks/navigation): improve `internal-link-present-evaluate`
dan-tripp Aug 20, 2021
bde2684
Merge branch 'dequelabs:develop' into develop
dan-tripp Sep 2, 2021
9f996bc
test commit 1
dan-tripp Sep 3, 2021
c011d2c
test commit 2
dan-tripp Sep 3, 2021
a386166
test commit 3
dan-tripp Sep 3, 2021
428e015
Merge branch 'dan-test-branch-1' into develop
dan-tripp Sep 3, 2021
31b19a3
Revert "Merge branch 'dan-test-branch-1' into develop"
dan-tripp Sep 3, 2021
ac32146
Revert "test commit 1"
dan-tripp Sep 3, 2021
6e389a1
Merge branch 'dequelabs:develop' into develop
dan-tripp Sep 10, 2021
739f035
Merge branch 'dequelabs:develop' into develop
dan-tripp Sep 15, 2021
30f0e01
fix(rule): allow "tabindex=-1" for rules "aria-text" and "nested-inte…
dan-tripp Sep 24, 2021
9caefd7
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 1, 2021
41fd8e2
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 3, 2021
7aeac06
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 8, 2021
c37be71
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 9, 2021
2896478
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 14, 2021
02cfb18
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 22, 2021
f2ae004
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 23, 2021
b687d45
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 27, 2021
0729846
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 28, 2021
b257208
Merge branch 'dequelabs:develop' into develop
dan-tripp Oct 31, 2021
86ac327
Revert "fix(rule): allow "tabindex=-1" for rules "aria-text" and "nes…
dan-tripp Oct 1, 2021
f974955
refactor(check): split no-focusable-content rule into three.
dan-tripp Oct 3, 2021
373d691
fix(rule): add custom message for nested-interactive.
dan-tripp Oct 7, 2021
deea093
style(rule): fix lint errors
dan-tripp Oct 8, 2021
deac42b
fix(no-focusable-content-for-frame): fix locale files
dan-tripp Oct 8, 2021
1a980bc
refactor(check): undo recent check rename
dan-tripp Oct 9, 2021
c141a58
refactor(check): undo recent split of checks
dan-tripp Oct 9, 2021
9eed890
refactor(check): rename local variable
dan-tripp Oct 9, 2021
6bd74ea
fix(rule): make no-focusable-content not consult aria-hidden
dan-tripp Oct 9, 2021
ac048be
fix(checks/keyboard): make no-focusable-content use messageKey the ri…
dan-tripp Oct 9, 2021
540eb79
refactor(check): misc. renaming and refactoring
dan-tripp Oct 14, 2021
801f687
Updating description / messageKey text as per https://github.com/dequ…
dan-tripp Oct 23, 2021
46ca22a
add unit test for frame-focusable-content check
dan-tripp Oct 27, 2021
c5c8234
update the no-focusable-content unit tests to add a test to capture n…
dan-tripp Oct 27, 2021
76dda77
update the nested-interactive integration test to add a failure for e…
dan-tripp Oct 27, 2021
c48c783
fix(no-focusable-content-for-frame): fix locale files
dan-tripp Oct 8, 2021
4c7662e
Update lib/rules/nested-interactive.json
dan-tripp Oct 27, 2021
9aa0e0e
Update lib/checks/keyboard/no-focusable-content.json
dan-tripp Oct 27, 2021
376540b
fixing botched merge
dan-tripp Oct 28, 2021
ba1e4e6
update rule-descriptions.md
dan-tripp Nov 1, 2021
f785edb
fix locale files
dan-tripp Nov 2, 2021
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
112 changes: 56 additions & 56 deletions doc/rule-descriptions.md

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions lib/checks/keyboard/frame-focusable-content-evaluate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import isFocusable from '../../commons/dom/is-focusable';

function focusableDescendants(vNode) {
if (isFocusable(vNode)) {
return true;
}

if (!vNode.children) {
if (vNode.props.nodeType === 1) {
throw new Error('Cannot determine children');
}

return false;
}

return vNode.children.some(child => {
return focusableDescendants(child);
});
}

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

try {
return !virtualNode.children.some(child => {
return focusableDescendants(child);
});
} catch (e) {
return undefined;
}
}

export default frameFocusableContentEvaluate;
2 changes: 1 addition & 1 deletion lib/checks/keyboard/frame-focusable-content.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "frame-focusable-content",
"evaluate": "no-focusable-content-evaluate",
"evaluate": "frame-focusable-content-evaluate",
"metadata": {
"impact": "serious",
"messages": {
Expand Down
40 changes: 26 additions & 14 deletions lib/checks/keyboard/no-focusable-content-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
import isFocusable from '../../commons/dom/is-focusable';

function focusableDescendants(vNode) {
if (isFocusable(vNode)) {
return true;
}

function getFocusableDescendants(vNode) {
if (!vNode.children) {
if (vNode.props.nodeType === 1) {
throw new Error('Cannot determine children');
}

return false;
return [];
}

return vNode.children.some(child => {
return focusableDescendants(child);
const retVal = [];
vNode.children.forEach(child => {
if(isFocusable(child)) {
retVal.push(child);
} else {
retVal.push(...getFocusableDescendants(child));
}
});
return retVal;
}

function noFocusbleContentEvaluate(node, options, virtualNode) {
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 {
return !virtualNode.children.some(child => {
return focusableDescendants(child);
});
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 noFocusbleContentEvaluate;
export default noFocusableContentEvaluate;
5 changes: 4 additions & 1 deletion lib/checks/keyboard/no-focusable-content.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"impact": "serious",
"messages": {
"pass": "Element does not have focusable descendants",
"fail": "Element has focusable descendants",
"fail": {
"default": "Element has focusable descendants",
"notHidden": "Using a negative tabindex on an element inside an interactive control does not prevent a screen reader from focusing the element (even with 'aria-hidden=true')"
},
"incomplete": "Could not determine if element has descendants"
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/core/base/metadata-function-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import focusableModalOpenEvaluate from '../../checks/keyboard/focusable-modal-op
import focusableNoNameEvaluate from '../../checks/keyboard/focusable-no-name-evaluate';
import focusableNotTabbableEvaluate from '../../checks/keyboard/focusable-not-tabbable-evaluate';
import landmarkIsTopLevelEvaluate from '../../checks/keyboard/landmark-is-top-level-evaluate';
import frameFocusableContentEvaluate from '../../checks/keyboard/frame-focusable-content-evaluate';
import noFocusableContentEvaluate from '../../checks/keyboard/no-focusable-content-evaluate';
import tabindexEvaluate from '../../checks/keyboard/tabindex-evaluate';

Expand Down Expand Up @@ -273,6 +274,7 @@ const metadataFunctionMap = {
'focusable-no-name-evaluate': focusableNoNameEvaluate,
'focusable-not-tabbable-evaluate': focusableNotTabbableEvaluate,
'landmark-is-top-level-evaluate': landmarkIsTopLevelEvaluate,
'frame-focusable-content-evaluate': frameFocusableContentEvaluate,
'no-focusable-content-evaluate': noFocusableContentEvaluate,
'tabindex-evaluate': tabindexEvaluate,

Expand Down
2 changes: 1 addition & 1 deletion lib/rules/nested-interactive.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"tags": ["cat.keyboard", "wcag2a", "wcag412"],
"actIds": ["307n5z"],
"metadata": {
"description": "Ensure controls are not nested as they are not announced by screen readers",
"description": "Ensures interactive controls are not nested as they are not always announced by screen readers or can cause focus problems for assistive technologies",
"help": "Interactive controls must not be nested"
},
"all": [],
Expand Down
41 changes: 41 additions & 0 deletions test/checks/keyboard/frame-focusable-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
describe('frame-focusable-content tests', function() {
var fixture = document.querySelector('#fixture');
var queryFixture = axe.testUtils.queryFixture;
var frameFocusableContent = axe.testUtils.getCheckEvaluate(
'frame-focusable-content'
);

afterEach(function() {
fixture.innerHTML = '';
});

it('should return true if element has no focusable content', function() {
var vNode = queryFixture('<button id="target"><span>Hello</span></button>');
assert.isTrue(frameFocusableContent(null, null, vNode));
});

it('should return true if element is empty', function() {
var vNode = queryFixture('<button id="target"></button>');
assert.isTrue(frameFocusableContent(null, null, vNode));
});

it('should return true if element only has text content', function() {
var vNode = queryFixture('<button id="target">Hello</button>');
assert.isTrue(frameFocusableContent(null, null, vNode));
});

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

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

});
14 changes: 14 additions & 0 deletions test/checks/keyboard/no-focusable-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ describe('no-focusable-content tests', function() {
var noFocusableContent = axe.testUtils.getCheckEvaluate(
'no-focusable-content'
);
var checkSetup = axe.testUtils.checkSetup;
var check = checks['no-focusable-content'];
var checkContext = new axe.testUtils.MockCheckContext();

afterEach(function() {
fixture.innerHTML = '';
checkContext.reset();
});

it('should return true if element has no focusable content', function() {
Expand Down Expand Up @@ -37,4 +41,14 @@ describe('no-focusable-content tests', function() {
);
assert.isFalse(noFocusableContent(null, null, vNode));
});

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' });
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<div role="tab" id="fail3"><button id="pass6">fail</button></div>
<div role="checkbox" id="fail4"><a href="foo.html">fail</a></div>
<div role="radio" id="fail5"><span tabindex="0">fail</span></div>
<div role="radio" id="fail6"><button id="pass7" tabindex="-1">not really hidden</button></div>
<div role="radio" id="fail7"><button aria-hidden="true" tabindex="-1">not really hidden</button></div>

<a id="ignored1" href="foo.html">ignored</a>
<span id="ignored2">ignored</span>
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
{
"description": "nested-interactive tests",
"rule": "nested-interactive",
"violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"], ["#fail5"]],
"violations": [
["#fail1"],
["#fail2"],
["#fail3"],
["#fail4"],
["#fail5"],
["#fail6"],
["#fail7"]
],
"passes": [
["#pass1"],
["#pass2"],
["#pass3"],
["#pass4"],
["#pass5"],
["#pass6"]
["#pass6"],
["#pass7"]
]
}