Skip to content

Commit f8c918d

Browse files
authored
fix(region): Decorative images ignored by region rule (#4412)
Issue was around [decorative images](https://www.w3.org/WAI/tutorials/images/decorative/), specifically 1 pixel wide/tall marketing tracking images, that get added outside of regions failing the region rule. An easy and fairly robust solution [the issue opener agreed with](#4145 (comment)) was ignoring images with `alt=''` and so that's what this PR implements. Dev notes: - Added `isPresentationGraphic/1` check which currently only handles `alt=''` and ignores them for the region rule - role='presentation' test added as well, but this already worked previously prior to this code change - svg I believe is handled differently already, there's a test called `treats svg elements as regions` so the new function doesn't check for svg's fix: #4145
2 parents 19bde94 + 738e9e2 commit f8c918d

File tree

5 files changed

+157
-8
lines changed

5 files changed

+157
-8
lines changed

lib/checks/navigation/region-evaluate.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as dom from '../../commons/dom';
2+
import { hasChildTextNodes } from '../../commons/dom/has-content-virtual';
23
import { getRole } from '../../commons/aria';
34
import * as standards from '../../commons/standards';
45
import matches from '../../commons/matches';
@@ -45,7 +46,10 @@ function getRegionlessNodes(options) {
4546
*/
4647
function findRegionlessElms(virtualNode, options) {
4748
const node = virtualNode.actualNode;
48-
// End recursion if the element is a landmark, skiplink, or hidden content
49+
// End recursion if the element is...
50+
// - a landmark
51+
// - a skiplink
52+
// - hidden content
4953
if (
5054
getRole(virtualNode) === 'button' ||
5155
isRegion(virtualNode, options) ||
@@ -73,7 +77,8 @@ function findRegionlessElms(virtualNode, options) {
7377
// @see https://github.com/dequelabs/axe-core/issues/2049
7478
} else if (
7579
node !== document.body &&
76-
dom.hasContent(node, /* noRecursion: */ true)
80+
dom.hasContent(node, /* noRecursion: */ true) &&
81+
!isShallowlyHidden(virtualNode)
7782
) {
7883
return [virtualNode];
7984

@@ -86,6 +91,14 @@ function findRegionlessElms(virtualNode, options) {
8691
}
8792
}
8893

94+
function isShallowlyHidden(virtualNode) {
95+
// The element itself is not visible to screen readers, but its descendants might be
96+
return (
97+
['none', 'presentation'].includes(getRole(virtualNode)) &&
98+
!hasChildTextNodes(virtualNode)
99+
);
100+
}
101+
89102
// Check if the current element is a landmark
90103
function isRegion(virtualNode, options) {
91104
const node = virtualNode.actualNode;

test/checks/navigation/region.js

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,87 @@ describe('region', function () {
4646
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
4747
});
4848

49-
it('should return false when img content is outside the region', function () {
50-
var checkArgs = checkSetup(
51-
'<img id="target" src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>'
52-
);
49+
it('should return false when img content is outside the region, no alt attribute at all', function () {
50+
const checkArgs = checkSetup(`
51+
<img id="target" src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7">
52+
<div role="main">Content</div>
53+
`);
54+
55+
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
56+
});
57+
58+
it('should return true when img content outside of the region is decorative, via an empty alt attr', function () {
59+
const checkArgs = checkSetup(`
60+
<img id="target" src="#" alt="" />
61+
<div role="main">Content</div>
62+
`);
63+
64+
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
65+
});
66+
67+
it('should return true when img content outside of the region is explicitly decorative, via a presentation role', function () {
68+
const checkArgs = checkSetup(`
69+
<img id="target" src="#" role="presentation" />
70+
<div role="main">Content</div>
71+
`);
72+
73+
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
74+
});
75+
76+
it('should return false when img content outside of the region is focusable (implicit role=img)', function () {
77+
const checkArgs = checkSetup(`
78+
<img id="target" src="#" tabindex="0" alt="" />
79+
<div role="main">Content</div>
80+
`);
81+
82+
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
83+
});
84+
85+
it('should return false when img content outside of the region has a global aria attribute (implicit role=img)', function () {
86+
const checkArgs = checkSetup(`
87+
<img id="target" src="#" aria-atomic="true" alt="" />
88+
<div role="main">Content</div>
89+
`);
90+
91+
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
92+
});
93+
94+
it('should return true when canvas role=none', function () {
95+
const checkArgs = checkSetup(`
96+
<canvas id="target" role="none" />
97+
<div role="main">Content</div>
98+
`);
99+
100+
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
101+
});
102+
103+
it('should return false when object has an aria-label', function () {
104+
const checkArgs = checkSetup(`
105+
<object id="target" aria-label="bar"></object>
106+
<div role="main">Content</div>
107+
`);
53108

54109
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
55110
});
56111

112+
it('should return false when a non-landmark has text content but a role=none', function () {
113+
const checkArgs = checkSetup(`
114+
<div id="target" role="none">apples</div>
115+
<div role="main">Content</div>
116+
`);
117+
118+
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
119+
});
120+
121+
it('should return true when a non-landmark does NOT have text content and a role=none', function () {
122+
const checkArgs = checkSetup(`
123+
<div id="target" role="none"></div>
124+
<div role="main">Content</div>
125+
`);
126+
127+
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
128+
});
129+
57130
it('should return true when textless text content is outside the region', function () {
58131
var checkArgs = checkSetup(
59132
'<p id="target"></p><div role="main"><h1 id="mainheader" tabindex="0">Introduction</h1></div>'
@@ -166,6 +239,15 @@ describe('region', function () {
166239
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
167240
});
168241

242+
it('ignores native landmark elements with an overwriting role with a nested child', function () {
243+
var checkArgs = checkSetup(`
244+
<main id="target" role="none"><p>Content</p></main>
245+
<div role="main">Content</div>
246+
`);
247+
248+
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
249+
});
250+
169251
it('returns false for content outside of form tags with accessible names', function () {
170252
var checkArgs = checkSetup(
171253
'<p id="target">Text</p><form aria-label="form"></form>'

test/integration/full/region/region-fail.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ <h1 id="mainheader" tabindex="0">This is a header.</h1>
4040
</section>
4141
</div>
4242

43+
<div id="img-no-alt">
44+
<img src="#" />
45+
</div>
46+
<div id="img-focusable">
47+
<img src="#" tabindex="0" alt="" />
48+
</div>
49+
<div id="img-aria-global">
50+
<img src="#" aria-atomic="true" alt="" />
51+
</div>
52+
<div id="labeled-object">
53+
<object aria-label="bar"></object>
54+
</div>
55+
<div id="none-role-div">
56+
<div id="target" role="none">apples</div>
57+
</div>
58+
4359
This should be ignored
4460

4561
<main id="mocha"></main>

test/integration/full/region/region-fail.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,40 @@ describe('region fail test', function () {
1515
});
1616

1717
describe('violations', function () {
18-
it('should find one', function () {
19-
assert.lengthOf(results.violations[0].nodes, 1);
18+
it('should find all violations', function () {
19+
assert.lengthOf(results.violations[0].nodes, 6);
2020
});
2121

2222
it('should find wrapper', function () {
2323
assert.deepEqual(results.violations[0].nodes[0].target, ['#wrapper']);
2424
});
25+
26+
it('should find image without an alt tag', function () {
27+
assert.deepEqual(results.violations[0].nodes[1].target, ['#img-no-alt']);
28+
});
29+
30+
it('should find focusable image', function () {
31+
assert.deepEqual(results.violations[0].nodes[2].target, [
32+
'#img-focusable'
33+
]);
34+
});
35+
36+
it('should find image with global aria attr', function () {
37+
assert.deepEqual(results.violations[0].nodes[3].target, [
38+
'#img-aria-global'
39+
]);
40+
});
41+
42+
it('should find object with a label', function () {
43+
assert.deepEqual(results.violations[0].nodes[4].target, [
44+
'#labeled-object'
45+
]);
46+
});
47+
48+
it('should find div with an role of none', function () {
49+
assert.deepEqual(results.violations[0].nodes[5].target, [
50+
'#none-role-div'
51+
]);
52+
});
2553
});
2654
});

test/integration/full/region/region-pass.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@
2020
</head>
2121
<body>
2222
<a href="#mainheader" id="skiplink">This is a skip link.</a>
23+
<!-- Decorative image -->
24+
<img src="#" alt="" />
25+
<!-- Decorative image -->
26+
<img src="#" role="presentation" />
27+
<!-- SVG's may be outside of a landmark -->
28+
<svg role="none" aria-label="foo"></svg>
29+
<!-- Div with a role of none may be outside a landmark -->
30+
<div role="none"></div>
31+
<!-- Canvas with a role of none may be outside a landmark -->
32+
<canvas role="none" />
2333
<div id="wrapper">
2434
<div role="main">
2535
<h1 id="mainheader" tabindex="0">This is a header.</h1>

0 commit comments

Comments
 (0)