Skip to content

Commit

Permalink
feat: new rule aria-hidden-focus
Browse files Browse the repository at this point in the history
  • Loading branch information
jeeyyy committed Sep 27, 2018
1 parent 031f798 commit 624ddbd
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 5 deletions.
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 element 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
15 changes: 15 additions & 0 deletions lib/checks/aria/aria-hidden-focus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { dom } = axe.commons;

let elements = [node];

if (node.children.length) {
const children = Array.prototype.slice.call(node.querySelectorAll('*'));
elements = elements.concat(children);
}

const result = elements.every(element => {
const output = dom.isFocusable(element, false) === false;
return output;
});

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",
"evaluate": "aria-hidden-focus.js",
"metadata": {
"impact": "serious",
"messages": {
"pass": "No focusable elements under aria-hidden element",
"fail": "aria-hidden=true element must not contain focusable elements"
}
}
}
11 changes: 6 additions & 5 deletions lib/commons/dom/is-focusable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
/**
* Determines if focusing has been disabled on an element.
* @param {HTMLElement} el The HTMLElement
* @param {Boolean} screenReader Default(true), When provided, will evaluate visibility from the perspective of a screen reader
* @return {Boolean} Whether focusing has been disabled on an element.
*/
function focusDisabled(el) {
function focusDisabled(el, screenReader = true) {
return (
el.disabled ||
(!dom.isVisible(el, true) && el.nodeName.toUpperCase() !== 'AREA')
(!dom.isVisible(el, screenReader) && el.nodeName.toUpperCase() !== 'AREA')
);
}

Expand All @@ -19,12 +20,12 @@ function focusDisabled(el) {
* @memberof axe.commons.dom
* @instance
* @param {HTMLElement} el The HTMLElement
* @param {Boolean} screenReader Default(true), When provided, will evaluate visibility from the perspective of a screen reader
* @return {Boolean} The element's focusability status
*/

dom.isFocusable = function(el) {
'use strict';
if (focusDisabled(el)) {
dom.isFocusable = function(el, screenReader = true) {
if (focusDisabled(el, screenReader)) {
return false;
} else if (dom.isNativelyFocusable(el)) {
return true;
Expand Down
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\"]",
"excludeHidden": false,
"tags": [
"cat.name-role-value",
"wcag2a",
"wcag412"
],
"metadata": {
"description": "Ensures aria-hidden element do not contain focusable elements",
"help": "ARIA hidden element must not contain focusable elements"
},
"all": [],
"any": [
"aria-hidden-focus"
],
"none": []
}
86 changes: 86 additions & 0 deletions test/checks/aria/aria-hidden-focus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
describe('aria-hidden-focus', function() {
'use strict';

var fixture = document.getElementById('fixture');
var check = undefined;
var checkContext = axe.testUtils.MockCheckContext();

before(function() {
check = checks['aria-hidden-focus'];
checkContext._data = null;
});

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

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

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() {
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 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() {
fixture.innerHTML =
'<div id="target" aria-hidden="true"><input type="text"/></div>';
var node = fixture.querySelector('#target');
var actual = check.evaluate.call(checkContext, node);
assert.isFalse(actual);
});

it('returns false when aria-hidden=false does not negate aria-hidden true', function() {
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.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);
});
});
24 changes: 24 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,24 @@
<!-- 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"><a href="/" style="position:absolute; top:-999em">Link</a></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>
<div id='violation3' aria-hidden="true">
<div aria-hidden="false">
<button tabindex="-1">Some button</button>
</div>
</div>
<p id='violation4' tabindex="0" aria-hidden="true">Some text</p>
<details id='violation5' aria-hidden="true">
<summary>Some button</summary>
<p>Some details</p>
</details>
38 changes: 38 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,38 @@
{
"description": "aria-hidden-focus tests",
"rule": "aria-hidden-focus",
"violations": [
[
"#violation1"
],
[
"#violation2"
],
[
"#violation3"
],
[
"#violation4"
],
[
"#violation5"
]
],
"passes": [
[
"#pass1"
],
[
"#pass2"
],
[
"#pass3"
],
[
"#pass4"
],
[
"#pass5"
]
]
}

0 comments on commit 624ddbd

Please sign in to comment.