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

feat: rule aria-hidden-focus #1166

Merged
merged 37 commits into from
Jan 9, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
624ddbd
feat: new rule aria-hidden-focus
jeeyyy Sep 27, 2018
212d0da
fix: revert changes to isFocusable. To be tackled by issue #1208
jeeyyy Oct 28, 2018
8fa937b
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Oct 28, 2018
35816f5
refactor: based on review comments
jeeyyy Nov 8, 2018
a60921c
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Nov 8, 2018
861428a
fix: update aria-hidden focus check and add tests
jeeyyy Nov 8, 2018
1525e77
test: add shadowDOM tests
jeeyyy Nov 8, 2018
e3e644b
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Nov 13, 2018
e37f902
fix: update check and tests based on code review comments
jeeyyy Nov 14, 2018
4aae0ae
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Nov 14, 2018
addaa32
fix: update var name to retrigger build
jeeyyy Nov 14, 2018
16fc190
fix: add matches checks and tests based on review
jeeyyy Nov 19, 2018
1fe6929
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Nov 20, 2018
57d1488
Merge branch 'develop' into new-rule-aria-hidden-focus
WilcoFiers Nov 20, 2018
34798ef
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Nov 20, 2018
24998c1
fix: split check to focusable and tabbable
jeeyyy Nov 22, 2018
1bfd9d3
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Nov 22, 2018
1a6a9b7
test: update assertions and fix breaking tests
jeeyyy Nov 22, 2018
edbf77f
fix: update based on review
jeeyyy Nov 27, 2018
d230af9
refactor: update based on code review
jeeyyy Nov 27, 2018
8ade68d
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Nov 27, 2018
3a86bc6
fix: matches comparison check
jeeyyy Nov 27, 2018
0ae71a3
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Nov 27, 2018
507e43e
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Dec 4, 2018
8c3b1ae
fix: update messages for checks
jeeyyy Dec 4, 2018
6a4bc5d
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Dec 7, 2018
aacd0c7
docs: update rule descriptions
jeeyyy Dec 7, 2018
7ea18ce
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Dec 11, 2018
19c29fe
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Dec 12, 2018
0f8565f
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Jan 2, 2019
fa7ce5d
fix: cache values in virtualNode for improved performance
jeeyyy Jan 2, 2019
983d747
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Jan 3, 2019
8a5da97
refactor: enhance vNode with getters
jeeyyy Jan 3, 2019
787b22a
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Jan 3, 2019
c7e8142
Merge branch 'develop' into new-rule-aria-hidden-focus
jeeyyy Jan 7, 2019
eaebf1b
refactor: change checks to have no relatedNodes for aria-hidden-focus
jeeyyy Jan 8, 2019
ca109d5
Merge branch 'develop' into new-rule-aria-hidden-focus
WilcoFiers Jan 9, 2019
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
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
| aria-allowed-role | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | true |
| aria-dpub-role-fallback | Ensures unsupported DPUB roles are only used on elements with implicit fallback roles | Moderate | cat.aria, wcag2a, wcag131 | true |
| aria-hidden-body | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | true |
| aria-hidden-focus | Ensures aria-hidden elements do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412 | true |
| aria-required-attr | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | true |
| aria-required-children | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | true |
| aria-required-parent | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | true |
Expand Down
36 changes: 36 additions & 0 deletions lib/checks/aria/aria-hidden-focus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
function getElmsIncludingShadowDOM(node, elms = []) {
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
// Note: can be moved to a utility fn, when the need arises
const nodes = node.querySelectorAll('*');

if (!nodes || (!nodes.length && axe.utils.isShadowRoot(node))) {
getElmsIncludingShadowDOM(node.shadowRoot, elms);
}

for (let i = 0, el; (el = nodes[i]); ++i) {
elms.push(el);
if (axe.utils.isShadowRoot(el)) {
getElmsIncludingShadowDOM(el.shadowRoot, elms);
}
}

return elms;
}

const children = getElmsIncludingShadowDOM(node);
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
const elements = [node].concat(children);
const result = elements.every(el => {
const isElFocusable = axe.commons.dom.isFocusable(el);
let tabIndex = el.getAttribute('tabindex');
tabIndex =
tabIndex && !isNaN(parseInt(tabIndex, 10)) ? parseInt(tabIndex) : null;
if (isElFocusable && (tabIndex && tabIndex < 0)) {
return true;
}
return isElFocusable === false;
});

if (!result) {
this.relatedNodes(children);
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
}

return result;
11 changes: 11 additions & 0 deletions lib/checks/aria/aria-hidden-focus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "aria-hidden-focus",
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
"evaluate": "aria-hidden-focus.js",
"metadata": {
"impact": "serious",
"messages": {
"pass": "No focusable elements under aria-hidden element",
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
"fail": "aria-hidden=true element must not contain focusable elements"
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
19 changes: 19 additions & 0 deletions lib/rules/aria-hidden-focus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"id": "aria-hidden-focus",
"selector": "[aria-hidden=\"true\"]",
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
"excludeHidden": false,
"tags": [
"cat.name-role-value",
"wcag2a",
"wcag412"
],
"metadata": {
"description": "Ensures aria-hidden elements do not contain focusable elements",
"help": "ARIA hidden element must not contain focusable elements"
},
"all": [],
"any": [
"aria-hidden-focus"
],
"none": []
}
117 changes: 117 additions & 0 deletions test/checks/aria/aria-hidden-focus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
describe('aria-hidden-focus', function() {
'use strict';

var fixture = document.getElementById('fixture');
var check = undefined;
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
var checkContext = axe.testUtils.MockCheckContext();
var shadowSupported = axe.testUtils.shadowSupport.v1;

before(function() {
check = checks['aria-hidden-focus'];
checkContext._data = null;
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
});

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

// pass
it('returns true when content not focusable by default', function() {
fixture.innerHTML = '<p id="target" aria-hidden="true">Some text</p>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isTrue(actual);
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
});

it('returns true when content hidden through CSS', function() {
fixture.innerHTML =
'<div id="target" aria-hidden="true"><a href="/" style="display:none">Link</a></div>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isTrue(actual);
});

it('returns true when content made unfocusable through tabindex', function() {
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
fixture.innerHTML =
'<div id="target" aria-hidden="true">' +
'<button tabindex="-1">Some button</button>' +
'</div>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isTrue(actual);
});

it('returns true when content made unfocusable through disabled', function() {
fixture.innerHTML = '<input id="target" disabled aria-hidden="true" />';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isTrue(actual);
});

it('returns true when aria-hidden=false does not negate aria-hidden true', function() {
// Note: aria-hidden can't be reset once you've set it to true on an ancestor
fixture.innerHTML =
'<div id="target" aria-hidden="true"><div aria-hidden="false"><button tabindex="-1">Some button</button></div></div>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isTrue(actual);
});

jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
(shadowSupported ? it : xit)(
'returns true when content hidden through CSS inside shadowDOM',
function() {
fixture.innerHTML = '<div id="target"></div>';
var node = fixture.querySelector('#target');
var shadow = node.attachShadow({ mode: 'open' });
shadow.innerHTML =
'<div aria-hidden="true"><a href="/" style="display:none">Link</a></div>';
var actual = check.evaluate.call(checkContext, node);
assert.isTrue(actual);
}
);

// fail
it('returns false when focusable off screen link', function() {
fixture.innerHTML =
'<div id="target" aria-hidden="true"><a href="/" style="position:absolute; top:-999em">Link</a></div>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isFalse(actual);
});

it('returns false when focusable form field, incorrectly disabled', function() {
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
fixture.innerHTML =
'<div id="target" aria-hidden="true"><input type="text" aria-disabled="true"/></div>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isFalse(actual);
});

it('returns false when focusable content through tabindex', function() {
fixture.innerHTML =
'<p id="target" tabindex="0" aria-hidden="true">Some text</p>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isFalse(actual);
});

it('returns false when focusable summary element', function() {
fixture.innerHTML =
'<details id="target" aria-hidden="true"><summary>Some button</summary><p>Some details</p></details>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isFalse(actual);
});

(shadowSupported ? it : xit)(
'returns false when focusable content through tabindex inside shadowDOM',
function() {
fixture.innerHTML = '<div id="target"></div>';
var node = fixture.querySelector('#target');
var shadow = node.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p tabindex="0" aria-hidden="true">Some text</p>';
var actual = check.evaluate.call(checkContext, node);
assert.isFalse(actual);
}
);
});
23 changes: 23 additions & 0 deletions test/integration/rules/aria-hidden-focus/aria-hidden-focus.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- Pass -->
<p id='pass1' aria-hidden="true">Some text</p>
<div id='pass2' aria-hidden="true"><a href="/" style="display:none">Link</a></div>
<div id='pass3' aria-hidden="true"><button tabindex="-1">Some button</button></div>
<input id='pass4' disabled aria-hidden="true" />
<div id='pass5' aria-hidden="true">
<div aria-hidden="false">
<button tabindex="-1">Some button</button>
</div>
</div>

<!-- Fail -->
<div id='violation1' aria-hidden="true">
<a href="/" style="position:absolute; top:-999em">Link</a>
</div>
<div id='violation2' aria-hidden="true">
<input aria-disabled="true" />
</div>
<p id='violation3' tabindex="0" aria-hidden="true">Some text</p>
<details id='violation4' aria-hidden="true">
<summary>Some button</summary>
<p>Some details</p>
</details>
35 changes: 35 additions & 0 deletions test/integration/rules/aria-hidden-focus/aria-hidden-focus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"description": "aria-hidden-focus tests",
"rule": "aria-hidden-focus",
"violations": [
[
"#violation1"
],
[
"#violation2"
],
[
"#violation3"
],
[
"#violation4"
]
],
"passes": [
[
"#pass1"
],
[
"#pass2"
],
[
"#pass3"
],
[
"#pass4"
],
[
"#pass5"
]
]
}