diff --git a/lib/checks/navigation/region-evaluate.js b/lib/checks/navigation/region-evaluate.js
index e0251a3901..45a6d576be 100644
--- a/lib/checks/navigation/region-evaluate.js
+++ b/lib/checks/navigation/region-evaluate.js
@@ -1,6 +1,7 @@
import * as dom from '../../commons/dom';
import * as aria from '../../commons/aria';
import * as text from '../../commons/text';
+import matches from '../../commons/matches';
import { matchesSelector } from '../../core/utils';
import cache from '../../core/base/cache';
@@ -13,7 +14,7 @@ const implicitLandmarks = landmarkRoles
.filter(r => r !== null);
// Check if the current element is a landmark
-function isRegion(virtualNode) {
+function isRegion(virtualNode, options) {
const node = virtualNode.actualNode;
const explicitRole = aria.getRole(node, { noImplicit: true });
const ariaLive = (node.getAttribute('aria-live') || '').toLowerCase().trim();
@@ -30,6 +31,11 @@ function isRegion(virtualNode) {
return explicitRole === 'dialog' || landmarkRoles.includes(explicitRole);
}
+ // Check if node matches an option
+ if (options.regionMatcher && matches(virtualNode, options.regionMatcher)) {
+ return true;
+ }
+
// Check if the node matches any of the CSS selectors of implicit landmarks
return implicitLandmarks.some(implicitSelector => {
let matches = matchesSelector(node, implicitSelector);
@@ -46,11 +52,11 @@ function isRegion(virtualNode) {
/**
* Find all visible elements not wrapped inside a landmark or skiplink
*/
-function findRegionlessElms(virtualNode) {
+function findRegionlessElms(virtualNode, options) {
const node = virtualNode.actualNode;
// End recursion if the element is a landmark, skiplink, or hidden content
if (
- isRegion(virtualNode) ||
+ isRegion(virtualNode, options) ||
(dom.isSkipLink(virtualNode.actualNode) &&
dom.getElementByReference(virtualNode.actualNode, 'href')) ||
!dom.isVisible(node, true)
@@ -77,7 +83,7 @@ function findRegionlessElms(virtualNode) {
} else {
return virtualNode.children
.filter(({ actualNode }) => actualNode.nodeType === 1)
- .map(findRegionlessElms)
+ .map(vNode => findRegionlessElms(vNode, options))
.reduce((a, b) => a.concat(b), []); // flatten the results
}
}
@@ -89,7 +95,7 @@ function regionEvaluate(node, options, virtualNode) {
}
const tree = axe._tree;
- regionlessNodes = findRegionlessElms(tree[0])
+ regionlessNodes = findRegionlessElms(tree[0], options)
// Find first parent marked as having region descendant (or body) and
// return the node right before it as the "outer" element
.map(vNode => {
diff --git a/test/checks/navigation/region.js b/test/checks/navigation/region.js
index 6f1b5f7f64..1adc81b1a6 100644
--- a/test/checks/navigation/region.js
+++ b/test/checks/navigation/region.js
@@ -5,6 +5,7 @@ describe('region', function() {
var shadowSupport = axe.testUtils.shadowSupport;
var checkSetup = axe.testUtils.checkSetup;
var fixtureSetup = axe.testUtils.fixtureSetup;
+ var checkEvaluate = axe.testUtils.getCheckEvaluate('region');
var checkContext = new axe.testUtils.MockCheckContext();
@@ -18,7 +19,7 @@ describe('region', function() {
'
'
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return false when img content is outside the region', function() {
@@ -26,7 +27,7 @@ describe('region', function() {
'Introduction
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return true when textless text content is outside the region', function() {
@@ -34,7 +35,7 @@ describe('region', function() {
'Introduction
'
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return true when wrapper content is outside the region', function() {
@@ -42,7 +43,7 @@ describe('region', function() {
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return true when invisible content is outside the region', function() {
@@ -50,7 +51,7 @@ describe('region', function() {
'Click Here
Introduction
'
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return true when there is a skiplink', function() {
@@ -58,7 +59,7 @@ describe('region', function() {
'Click HereIntroduction
'
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return true when there is an Angular skiplink', function() {
@@ -66,7 +67,7 @@ describe('region', function() {
'Click HereIntroduction
'
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return false when there is a non-region element', function() {
@@ -74,7 +75,7 @@ describe('region', function() {
'This is random content.
Introduction
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return false when there is a non-skiplink', function() {
@@ -82,7 +83,7 @@ describe('region', function() {
'Click HereIntroduction
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('should return true if the non-region element is a script', function() {
@@ -90,7 +91,7 @@ describe('region', function() {
'Content
'
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should considered aria labelled elements as content', function() {
@@ -98,7 +99,7 @@ describe('region', function() {
'Content
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('should allow native header elements', function() {
@@ -106,7 +107,7 @@ describe('region', function() {
'Content '
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should allow native main elements', function() {
@@ -114,7 +115,7 @@ describe('region', function() {
'Content '
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should allow native aside elements', function() {
@@ -122,7 +123,7 @@ describe('region', function() {
'Content '
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('should allow native footer elements', function() {
@@ -130,7 +131,7 @@ describe('region', function() {
'Content '
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('ignores native landmark elements with an overwriting role', function() {
@@ -138,7 +139,7 @@ describe('region', function() {
'ContentContent
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('returns false for content outside of form tags with accessible names', function() {
@@ -146,7 +147,7 @@ describe('region', function() {
'Text
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('ignores unlabeled forms as they are not landmarks', function() {
@@ -154,21 +155,21 @@ describe('region', function() {
'Content
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('treats with aria label as landmarks', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('treats role=forms with aria label as landmarks', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('treats forms without aria label as not a landmarks', function() {
@@ -176,7 +177,7 @@ describe('region', function() {
'Content
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('treats forms with an empty aria label as not a landmarks', function() {
@@ -184,7 +185,7 @@ describe('region', function() {
'Content
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('treats forms with non empty titles as landmarks', function() {
@@ -192,7 +193,7 @@ describe('region', function() {
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('treats forms with empty titles not as landmarks', function() {
@@ -200,7 +201,7 @@ describe('region', function() {
'Content
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('treats ARIA forms with no label or title as landmarks', function() {
@@ -208,63 +209,63 @@ describe('region', function() {
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('allows content in aria-live=assertive', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('allows content in aria-live=polite', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('does not allow content in aria-live=off', function() {
var checkArgs = checkSetup(
'Content
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
});
it('allows content in aria-live=assertive with explicit role set', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('allows content in aria-live=polite with explicit role set', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('allows content in implicit aria-live role alert', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('allows content in implicit aria-live role log', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('allows content in implicit aria-live role status', function() {
var checkArgs = checkSetup(
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('treats role=dialog elements as regions', function() {
@@ -272,7 +273,7 @@ describe('region', function() {
''
);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
it('returns the outermost element as the error', function() {
@@ -280,7 +281,22 @@ describe('region', function() {
'Introduction
'
);
- assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
+ });
+
+ it('supports options.regionMatcher', function() {
+ var checkArgs = checkSetup(
+ 'Content
',
+ {
+ regionMatcher: {
+ attributes: {
+ 'aria-live': 'off'
+ }
+ }
+ }
+ );
+
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
(shadowSupport.v1 ? it : xit)('should test Shadow tree content', function() {
@@ -292,7 +308,7 @@ describe('region', function() {
// fixture is the outermost element
assert.isFalse(
- checks.region.evaluate.call(
+ checkEvaluate.call(
checkContext,
virutalNode.actualNode,
null,
@@ -308,7 +324,7 @@ describe('region', function() {
shadow.innerHTML = '
';
var checkArgs = checkSetup(div);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});
(shadowSupport.v1 ? it : xit)(
@@ -324,7 +340,7 @@ describe('region', function() {
var virutalNode = axe.utils.getNodeFromTree(div.querySelector('#target'));
assert.isFalse(
- checks.region.evaluate.call(
+ checkEvaluate.call(
checkContext,
virutalNode.actualNode,
null,
@@ -344,7 +360,7 @@ describe('region', function() {
'skiplink
';
var checkArgs = checkSetup(div);
- assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 0);
}
);