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() { '
Click Here

Introduction

' ); - 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() { '

Introduction

' ); - 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() { '

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 Here

Introduction

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

Introduction

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

Introduction

' ); - 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() { '
branding
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() { '
branding
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() { '
branding
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() { '
branding
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() { '
Content
Content
' ); - 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() { '
foo
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( '

This is random content.

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

This is random content.

' ); - 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() { '

This is random content.

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() { '

This is random content.

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() { '

This is random content.

' ); - 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() { '

This is random content.

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() { '

This is random content.

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

This is random content.

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

This is random content.

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

This is random content.

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( '

This is random content.

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

This is random content.

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

This is random content.

' ); - 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() { '

This is random content.

Introduction

' ); - assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs)); + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('supports options.regionMatcher', function() { + var checkArgs = checkSetup( + '

This is random content.

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