diff --git a/lib/checks/navigation/region.js b/lib/checks/navigation/region.js
index 4443bf535a..a956251dd2 100644
--- a/lib/checks/navigation/region.js
+++ b/lib/checks/navigation/region.js
@@ -1,34 +1,57 @@
-//jshint latedef: false
+const { dom, aria } = axe.commons;
-var landmarkRoles = axe.commons.aria.getRolesByType('landmark'),
- firstLink = node.querySelector('a[href]');
-
-function isSkipLink(n) {
- return firstLink &&
- axe.commons.dom.isFocusable(axe.commons.dom.getElementByReference(firstLink, 'href')) &&
- firstLink === n;
+// Return the skplink, if any
+function getSkiplink (virtualNode) {
+ const firstLink = axe.utils.querySelectorAll(virtualNode, 'a[href]')[0];
+ if (firstLink && axe.commons.dom.getElementByReference(firstLink.actualNode, 'href')) {
+ return firstLink.actualNode;
+ }
}
-function isLandmark(n) {
- var role = n.getAttribute('role');
- return role && (landmarkRoles.indexOf(role) !== -1);
+const skipLink = getSkiplink(virtualNode);
+const landmarkRoles = aria.getRolesByType('landmark');
+
+// Create a list of nodeNames that have a landmark as an implicit role
+const implicitLandmarks = landmarkRoles
+ .reduce((arr, role) => arr.concat(aria.implicitNodes(role)), [])
+ .filter(r => r !== null).map(r => r.toUpperCase());
+
+// Check if the current element is the skiplink
+function isSkipLink (node) {
+ return skipLink && skipLink === node;
}
-function checkRegion(n) {
- if (isLandmark(n)) { return null; }
- if (isSkipLink(n)) { return getViolatingChildren(n); }
- if (axe.commons.dom.isVisible(n, true) &&
- (axe.commons.text.visible(n, true, true) || axe.commons.dom.isVisualContent(n))) { return n; }
- return getViolatingChildren(n);
+// Check if the current element is a landmark
+function isLandmark (node) {
+ if (node.hasAttribute('role')) {
+ return landmarkRoles.includes(node.getAttribute('role').toLowerCase());
+ } else {
+ return implicitLandmarks.includes(node.nodeName.toUpperCase());
+ }
}
-function getViolatingChildren(n) {
- var children = axe.commons.utils.toArray(n.children);
- if (children.length === 0) { return []; }
- return children.map(checkRegion)
- .filter(function (c) { return c !== null; })
- .reduce(function (a, b) { return a.concat(b); }, []);
+
+/**
+ * Find all visible elements not wrapped inside a landmark or skiplink
+ */
+function findRegionlessElms (virtualNode) {
+ const node = virtualNode.actualNode;
+ // End recursion if the element is a landmark, skiplink, or hidden content
+ if (isLandmark(node) || isSkipLink(node) || !dom.isVisible(node, true)) {
+ return [];
+
+ // Return the node is a content element
+ } else if (dom.hasContent(node, /* noRecursion: */ true)) {
+ return [node];
+
+ // Recursively look at all child elements
+ } else {
+ return virtualNode.children.filter(({ actualNode }) => actualNode.nodeType === 1)
+ .map(findRegionlessElms)
+ .reduce((a, b) => a.concat(b), []); // flatten the results
+ }
}
-var v = getViolatingChildren(node);
-this.relatedNodes(v);
-return !v.length;
+var regionlessNodes = findRegionlessElms(virtualNode);
+this.relatedNodes(regionlessNodes);
+
+return regionlessNodes.length === 0;
diff --git a/lib/commons/dom/get-element-by-reference.js b/lib/commons/dom/get-element-by-reference.js
index 44027aeda3..aa24d35ca1 100644
--- a/lib/commons/dom/get-element-by-reference.js
+++ b/lib/commons/dom/get-element-by-reference.js
@@ -1,26 +1,20 @@
/*global dom */
dom.getElementByReference = function (node, attr) {
- 'use strict';
-
- var candidate,
- fragment = node.getAttribute(attr),
- doc = document;
+ let fragment = node.getAttribute(attr);
if (fragment && fragment.charAt(0) === '#') {
fragment = fragment.substring(1);
- candidate = doc.getElementById(fragment);
+ let candidate = document.getElementById(fragment);
if (candidate) {
return candidate;
}
- candidate = doc.getElementsByName(fragment);
+ candidate = document.getElementsByName(fragment);
if (candidate.length) {
return candidate[0];
}
-
}
-
return null;
};
\ No newline at end of file
diff --git a/lib/commons/dom/has-content.js b/lib/commons/dom/has-content.js
index 72bcfb449f..bd5407c033 100644
--- a/lib/commons/dom/has-content.js
+++ b/lib/commons/dom/has-content.js
@@ -19,7 +19,7 @@ function hasChildTextNodes (elm) {
* @param {Object} virtual DOM node
* @return boolean
*/
-dom.hasContent = function hasContent (elm) {
+dom.hasContent = function hasContent (elm, noRecursion) {
if (!elm.actualNode) {
elm = axe.utils.getNodeFromTree(axe._tree[0], elm);
}
@@ -31,8 +31,8 @@ dom.hasContent = function hasContent (elm) {
// It has an ARIA label
!!aria.label(elm) ||
// or one of it's descendants does
- elm.children.some(child => (
+ (!noRecursion && elm.children.some(child => (
child.actualNode.nodeType === 1 && dom.hasContent(child)
- ))
+ )))
);
};
diff --git a/test/checks/navigation/region.js b/test/checks/navigation/region.js
index 8bcad07f35..7a2c35328e 100644
--- a/test/checks/navigation/region.js
+++ b/test/checks/navigation/region.js
@@ -2,70 +2,62 @@ describe('region', function () {
'use strict';
var fixture = document.getElementById('fixture');
+ var shadowSupport = axe.testUtils.shadowSupport;
+ var checkSetup = axe.testUtils.checkSetup;
- var checkContext = {
- _relatedNodes: [],
- _data: null,
- data: function (d) {
- this._data = d;
- },
- relatedNodes: function (rn) {
- this._relatedNodes = rn;
- }
- };
+ var checkContext = new axe.testUtils.MockCheckContext();
afterEach(function () {
fixture.innerHTML = '';
- checkContext._relatedNodes = [];
- checkContext._data = null;
+ checkContext.reset();
});
it('should return true when all content is inside the region', function () {
- fixture.innerHTML = '
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks.region.evaluate.call(checkContext, node));
+ var checkArgs = checkSetup('');
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});
it('should return false when img content is outside the region', function () {
- fixture.innerHTML = 'Introduction
';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks.region.evaluate.call(checkContext, node));
+ var checkArgs = checkSetup('Introduction
');
+
+ assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 1);
});
it('should return true when textless text content is outside the region', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks.region.evaluate.call(checkContext, node));
+ var checkArgs = checkSetup('');
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});
it('should return true when wrapper content is outside the region', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks.region.evaluate.call(checkContext, node));
+ var checkArgs = checkSetup('');
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});
it('should return true when invisible content is outside the region', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks.region.evaluate.call(checkContext, node));
+ var checkArgs = checkSetup('');
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});
it('should return true when there is a skiplink', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks.region.evaluate.call(checkContext, node));
+ var checkArgs = checkSetup('');
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 0);
});
it('should return false when there is a non-region element', function () {
- fixture.innerHTML = 'This is random content.
Introduction
';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks.region.evaluate.call(checkContext, node));
+ var checkArgs = checkSetup('This is random content.
Introduction
');
+
+ assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 1);
});
@@ -74,9 +66,83 @@ describe('region', function () {
});
it('should return false when there is a non-skiplink', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks.region.evaluate.call(checkContext, node));
+ var checkArgs = checkSetup('');
+
+ assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._relatedNodes.length, 1);
});
+
+ it('should return true if the non-region element is a script', function () {
+ var checkArgs = checkSetup('');
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ });
+
+ it('should considered aria labelled elements as content', function () {
+ var checkArgs = checkSetup('');
+
+ assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.deepEqual(checkContext._relatedNodes, [
+ fixture.querySelector('div[aria-label]')
+ ]);
+ });
+
+ it('should allow native landmark elements', function () {
+ var checkArgs = checkSetup('');
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.lengthOf(checkContext._relatedNodes, 0);
+ });
+
+ it('ignores native landmark elements with an overwriting role', function () {
+ var checkArgs = checkSetup('
');
+
+ assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.lengthOf(checkContext._relatedNodes, 1);
+ assert.deepEqual(checkContext._relatedNodes, [fixture.querySelector('main')]);
+ });
+
+ (shadowSupport.v1 ? it : xit)('should test Shadow tree content', function () {
+ var div = document.createElement('div');
+ var shadow = div.attachShadow({ mode: 'open' });
+ shadow.innerHTML = 'Some text';
+ var checkArgs = checkSetup(div);
+
+ assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.deepEqual(checkContext._relatedNodes, [div]);
+ });
+
+ (shadowSupport.v1 ? it : xit)('should test slotted content', function () {
+ var div = document.createElement('div');
+ div.innerHTML = 'Some content';
+ var shadow = div.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '
';
+ var checkArgs = checkSetup(div);
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.lengthOf(checkContext._relatedNodes, 0);
+ });
+
+ (shadowSupport.v1 ? it : xit)('should ignore skiplink targets inside shadow trees', function () {
+ var div = document.createElement('div');
+ div.innerHTML = 'skiplinkContent
';
+
+ var shadow = div.querySelector('div').attachShadow({ mode: 'open' });
+ shadow.innerHTML = '
';
+ var checkArgs = checkSetup(div);
+
+ assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.deepEqual(checkContext._relatedNodes, [div.querySelector('a')]);
+ });
+
+ (shadowSupport.v1 ? it : xit)('should find the skiplink in shadow DOM', function () {
+ var div = document.createElement('div');
+ div.innerHTML = 'Content!';
+ var shadow = div.attachShadow({ mode: 'open' });
+ shadow.innerHTML = 'skiplink
';
+ var checkArgs = checkSetup(div);
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.lengthOf(checkContext._relatedNodes, 0);
+ });
});
diff --git a/test/commons/dom/has-content.js b/test/commons/dom/has-content.js
index 8b10bcbd88..68c6c6c0a4 100644
--- a/test/commons/dom/has-content.js
+++ b/test/commons/dom/has-content.js
@@ -77,6 +77,13 @@ describe('dom.hasContent', function () {
);
});
+ it('is false if noRecursion is true and the content is not in a child', function () {
+ fixture.innerHTML = ' text
';
+ tree = axe.utils.getFlattenedTree(fixture);
+
+ assert.isFalse(hasContent(axe.utils.querySelectorAll(tree, '#target')[0], true));
+ });
+
(shadowSupport ? it : xit)('looks at content of shadow dom elements', function () {
fixture.innerHTML = '';
var shadow = fixture.querySelector('#target').attachShadow({ mode: 'open' });
diff --git a/test/testutils.js b/test/testutils.js
index ab8cfc811c..543597248c 100644
--- a/test/testutils.js
+++ b/test/testutils.js
@@ -1,5 +1,23 @@
var testUtils = {};
+testUtils.MockCheckContext = function () {
+ 'use strict';
+ return {
+ _relatedNodes: [],
+ _data: null,
+ data: function (d) {
+ this._data = d;
+ },
+ relatedNodes: function (rn) {
+ this._relatedNodes = rn;
+ },
+ reset: function () {
+ this._data = null;
+ this._relatedNodes = [];
+ }
+ };
+};
+
testUtils.shadowSupport = (function(document) {
'use strict';
var v0 = document.body && typeof document.body.createShadowRoot === 'function',