diff --git a/lib/checks/navigation/region-evaluate.js b/lib/checks/navigation/region-evaluate.js index 0e14b8a168..0e20b472cb 100644 --- a/lib/checks/navigation/region-evaluate.js +++ b/lib/checks/navigation/region-evaluate.js @@ -1,4 +1,5 @@ import * as dom from '../../commons/dom'; +import { hasChildTextNodes } from '../../commons/dom/has-content-virtual'; import { getRole } from '../../commons/aria'; import * as standards from '../../commons/standards'; import matches from '../../commons/matches'; @@ -45,7 +46,10 @@ function getRegionlessNodes(options) { */ function findRegionlessElms(virtualNode, options) { const node = virtualNode.actualNode; - // End recursion if the element is a landmark, skiplink, or hidden content + // End recursion if the element is... + // - a landmark + // - a skiplink + // - hidden content if ( getRole(virtualNode) === 'button' || isRegion(virtualNode, options) || @@ -73,7 +77,8 @@ function findRegionlessElms(virtualNode, options) { // @see https://github.com/dequelabs/axe-core/issues/2049 } else if ( node !== document.body && - dom.hasContent(node, /* noRecursion: */ true) + dom.hasContent(node, /* noRecursion: */ true) && + !isShallowlyHidden(virtualNode) ) { return [virtualNode]; @@ -86,6 +91,14 @@ function findRegionlessElms(virtualNode, options) { } } +function isShallowlyHidden(virtualNode) { + // The element itself is not visible to screen readers, but its descendants might be + return ( + ['none', 'presentation'].includes(getRole(virtualNode)) && + !hasChildTextNodes(virtualNode) + ); +} + // Check if the current element is a landmark function isRegion(virtualNode, options) { const node = virtualNode.actualNode; diff --git a/test/checks/navigation/region.js b/test/checks/navigation/region.js index fb4d13b3e9..2620661d9d 100644 --- a/test/checks/navigation/region.js +++ b/test/checks/navigation/region.js @@ -46,14 +46,87 @@ describe('region', function () { assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); }); - it('should return false when img content is outside the region', function () { - var checkArgs = checkSetup( - '

Introduction

' - ); + it('should return false when img content is outside the region, no alt attribute at all', function () { + const checkArgs = checkSetup(` + +
Content
+ `); + + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('should return true when img content outside of the region is decorative, via an empty alt attr', function () { + const checkArgs = checkSetup(` + +
Content
+ `); + + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('should return true when img content outside of the region is explicitly decorative, via a presentation role', function () { + const checkArgs = checkSetup(` + +
Content
+ `); + + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('should return false when img content outside of the region is focusable (implicit role=img)', function () { + const checkArgs = checkSetup(` + +
Content
+ `); + + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('should return false when img content outside of the region has a global aria attribute (implicit role=img)', function () { + const checkArgs = checkSetup(` + +
Content
+ `); + + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('should return true when canvas role=none', function () { + const checkArgs = checkSetup(` + +
Content
+ `); + + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('should return false when object has an aria-label', function () { + const checkArgs = checkSetup(` + +
Content
+ `); assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); }); + it('should return false when a non-landmark has text content but a role=none', function () { + const checkArgs = checkSetup(` +
apples
+
Content
+ `); + + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); + }); + + it('should return true when a non-landmark does NOT have text content and a role=none', function () { + const checkArgs = checkSetup(` +
+
Content
+ `); + + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + }); + it('should return true when textless text content is outside the region', function () { var checkArgs = checkSetup( '

Introduction

' @@ -166,6 +239,15 @@ describe('region', function () { assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); }); + it('ignores native landmark elements with an overwriting role with a nested child', function () { + var checkArgs = checkSetup(` +

Content

+
Content
+ `); + + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); + }); + it('returns false for content outside of form tags with accessible names', function () { var checkArgs = checkSetup( '

Text

' diff --git a/test/integration/full/region/region-fail.html b/test/integration/full/region/region-fail.html index 79fc58c0fb..6f44a2eada 100644 --- a/test/integration/full/region/region-fail.html +++ b/test/integration/full/region/region-fail.html @@ -40,6 +40,22 @@

This is a header.

+
+ +
+
+ +
+
+ +
+
+ +
+
+
apples
+
+ This should be ignored
diff --git a/test/integration/full/region/region-fail.js b/test/integration/full/region/region-fail.js index a1fd2952a6..09e7ae9906 100644 --- a/test/integration/full/region/region-fail.js +++ b/test/integration/full/region/region-fail.js @@ -15,12 +15,40 @@ describe('region fail test', function () { }); describe('violations', function () { - it('should find one', function () { - assert.lengthOf(results.violations[0].nodes, 1); + it('should find all violations', function () { + assert.lengthOf(results.violations[0].nodes, 6); }); it('should find wrapper', function () { assert.deepEqual(results.violations[0].nodes[0].target, ['#wrapper']); }); + + it('should find image without an alt tag', function () { + assert.deepEqual(results.violations[0].nodes[1].target, ['#img-no-alt']); + }); + + it('should find focusable image', function () { + assert.deepEqual(results.violations[0].nodes[2].target, [ + '#img-focusable' + ]); + }); + + it('should find image with global aria attr', function () { + assert.deepEqual(results.violations[0].nodes[3].target, [ + '#img-aria-global' + ]); + }); + + it('should find object with a label', function () { + assert.deepEqual(results.violations[0].nodes[4].target, [ + '#labeled-object' + ]); + }); + + it('should find div with an role of none', function () { + assert.deepEqual(results.violations[0].nodes[5].target, [ + '#none-role-div' + ]); + }); }); }); diff --git a/test/integration/full/region/region-pass.html b/test/integration/full/region/region-pass.html index 3c1a881117..1e739bb390 100644 --- a/test/integration/full/region/region-pass.html +++ b/test/integration/full/region/region-pass.html @@ -20,6 +20,16 @@ + + + + + + + +
+ +

This is a header.