Skip to content

Commit 328ca2c

Browse files
authored
feat: new rule landmark-complementary-is-top-level (#1239)
* feat: new rule landmark-complementary-is-top-level Best practice requiring asides and complementary landmarks to be top level, in line with the ARIA Authoring Practices Guide. Closes #795 * fix: remove matches from top-level aside rule The first version of this rule included a body context check copied over from the top-level-banner-landmark rule, and it made very flaky iframe tests. It wasn't really necessary since aside doesn't have the same behavior as header/role=banner. * test: improve readability of landmark check
1 parent 59465dc commit 328ca2c

12 files changed

+344
-121
lines changed

doc/aria-supported.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ For a detailed description about how accessibility support is decided, see [How
1818
| -------------------- | ---------------- |
1919
| aria-describedat | No |
2020
| aria-details | No |
21-
| aria-roledescription | No |
21+
| aria-roledescription | No |

doc/rule-descriptions.md

Lines changed: 77 additions & 76 deletions
Large diffs are not rendered by default.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"id": "landmark-complementary-is-top-level",
3+
"selector": "aside:not([role]), [role=complementary]",
4+
"tags": [
5+
"cat.semantics",
6+
"best-practice"
7+
],
8+
"metadata": {
9+
"description": "Ensures the complementary landmark or aside is at top level",
10+
"help": "Aside must not be contained in another landmark"
11+
},
12+
"all": [],
13+
"any": [
14+
"landmark-is-top-level"
15+
],
16+
"none": []
17+
}

lib/rules/landmark-has-body-context.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const nativeScopeFilter = 'article, aside, main, nav, section';
22

33
// Filter elements that, within certain contexts, don't map their role.
4-
// e.g. a <footer> inside a <main> is not a banner, but in the <body> context it is
4+
// e.g. a <header> inside a <main> is not a banner, but in the <body> context it is
55
return (
66
node.hasAttribute('role') ||
77
!axe.commons.dom.findUpVirtual(virtualNode, nativeScopeFilter)

test/checks/keyboard/landmark-is-top-level.js

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,72 @@
11
describe('landmark-is-top-level', function() {
22
'use strict';
33

4-
var fixture = document.getElementById('fixture');
5-
64
var shadowSupported = axe.testUtils.shadowSupport.v1;
75
var checkSetup = axe.testUtils.checkSetup;
6+
var shadowCheckSetup = axe.testUtils.shadowCheckSetup;
87
var check = checks['landmark-is-top-level'];
98
var checkContext = new axe.testUtils.MockCheckContext();
109

1110
afterEach(function() {
12-
fixture.innerHTML = '';
1311
checkContext.reset();
1412
});
1513

16-
it('should return false if the landmark is in another landmark', function() {
17-
var mainLandmark = document.createElement('main');
18-
var bannerDiv = document.createElement('div');
19-
bannerDiv.setAttribute('role', 'banner');
20-
bannerDiv.appendChild(mainLandmark);
21-
fixture.appendChild(bannerDiv);
22-
assert.isFalse(check.evaluate.call(checkContext, mainLandmark));
14+
it('should return false if the main landmark is in another landmark', function() {
15+
var params = checkSetup(
16+
'<div role="banner"><main id="target"></main></div>'
17+
);
18+
assert.isFalse(check.evaluate.apply(checkContext, params));
2319
assert.deepEqual(checkContext._data, { role: 'main' });
2420
});
2521

22+
it('should return false if the complementary landmark is in another landmark', function() {
23+
var params = checkSetup(
24+
'<main><div role="complementary" id="target"></div></main>'
25+
);
26+
assert.isFalse(check.evaluate.apply(checkContext, params));
27+
assert.deepEqual(checkContext._data, { role: 'complementary' });
28+
});
29+
2630
it('should return false if div with role set to main is in another landmark', function() {
27-
var mainDiv = document.createElement('div');
28-
mainDiv.setAttribute('role', 'main');
29-
var navDiv = document.createElement('div');
30-
navDiv.setAttribute('role', 'navigation');
31-
navDiv.appendChild(mainDiv);
32-
fixture.appendChild(navDiv);
33-
assert.isFalse(check.evaluate.call(checkContext, mainDiv));
31+
var params = checkSetup(
32+
'<div role="navigation"><div role="main" id="target"></div></div>'
33+
);
34+
assert.isFalse(check.evaluate.apply(checkContext, params));
3435
assert.deepEqual(checkContext._data, { role: 'main' });
3536
});
3637

3738
it('should return true if the landmark is not in another landmark', function() {
38-
var footerLandmark = document.createElement('footer');
39-
var bannerDiv = document.createElement('div');
40-
bannerDiv.setAttribute('role', 'banner');
41-
fixture.appendChild(bannerDiv);
42-
fixture.appendChild(footerLandmark);
43-
assert.isTrue(check.evaluate.call(checkContext, footerLandmark));
39+
var params = checkSetup(
40+
'<div><footer id="target"></footer><div role="banner"></div></div>'
41+
);
42+
assert.isTrue(check.evaluate.apply(checkContext, params));
4443
assert.deepEqual(checkContext._data, { role: 'contentinfo' });
4544
});
4645

4746
it('should return true if div with role set to main is not in another landmark', function() {
48-
var mainDiv = document.createElement('div');
49-
mainDiv.setAttribute('role', 'main');
50-
var navDiv = document.createElement('div');
51-
navDiv.setAttribute('role', 'navigation');
52-
fixture.appendChild(navDiv);
53-
fixture.appendChild(mainDiv);
54-
assert.isTrue(check.evaluate.call(checkContext, mainDiv));
47+
var params = checkSetup(
48+
'<div><div role="main" id="target"></div><div role="navigation"></div></div>'
49+
);
50+
assert.isTrue(check.evaluate.apply(checkContext, params));
5551
assert.deepEqual(checkContext._data, { role: 'main' });
5652
});
5753

58-
it('should return true if the landmark is in form landmark', function() {
59-
var bannerDiv = document.createElement('div');
60-
bannerDiv.setAttribute('role', 'banner');
61-
var formDiv = document.createElement('div');
62-
formDiv.setAttribute('role', 'form');
63-
fixture.appendChild(formDiv);
64-
fixture.appendChild(bannerDiv);
65-
assert.isTrue(check.evaluate.call(checkContext, bannerDiv));
54+
it('should return true if the banner landmark is not in form landmark', function() {
55+
var params = checkSetup(
56+
'<div><div role="banner" id="target"></div><div role="form"></div></div>'
57+
);
58+
assert.isTrue(check.evaluate.apply(checkContext, params));
6659
assert.deepEqual(checkContext._data, { role: 'banner' });
6760
});
6861

6962
(shadowSupported ? it : xit)(
7063
'should test if the landmark in shadow DOM is top level',
7164
function() {
72-
var div = document.createElement('div');
73-
var shadow = div.attachShadow({ mode: 'open' });
74-
shadow.innerHTML = '<main>Main content</main>';
75-
var checkArgs = checkSetup(shadow.querySelector('main'));
76-
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
65+
var params = shadowCheckSetup(
66+
'<div></div>',
67+
'<main id="target">Main content</main>'
68+
);
69+
assert.isTrue(check.evaluate.apply(checkContext, params));
7770
assert.deepEqual(checkContext._data, { role: 'main' });
7871
}
7972
);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html id="violation2">
3+
<head>
4+
<meta charset="utf8">
5+
<script src="/axe.js"></script>
6+
</head>
7+
<body>
8+
<p>This iframe should fail, too</p>
9+
<main>
10+
<div role="complementary">
11+
<p>This complementary landmark is in a main landmark</p>
12+
</div>
13+
</main>
14+
</body>
15+
</html>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!doctype html>
2+
<html id="pass2">
3+
<head>
4+
<meta charset="utf8">
5+
<script src="/axe.js"></script>
6+
</head>
7+
<body>
8+
<p>This iframe should pass, too</p>
9+
10+
<div role="navigation">
11+
<p>This div has role navigation</p>
12+
</div>
13+
<header>
14+
<p>This banner content is not within another landmark</p>
15+
</header>
16+
<div role="complementary">
17+
<p>This div has role complementary</p>
18+
</div>
19+
<div role="search">
20+
<p>This div has role search</p>
21+
</div>
22+
<div role="form">
23+
<p>This div has role form<p>
24+
</div>
25+
<iframe id="frame2" src="level2.html"></iframe>
26+
</body>
27+
</html>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html id="pass3">
3+
<head>
4+
<meta charset="utf8">
5+
<script src="/axe.js"></script>
6+
</head>
7+
<body>
8+
<p>This iframe should pass<p>
9+
<aside>
10+
<p>This aside is top level and should be ignored</p>
11+
</aside>
12+
</body>
13+
</html>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!doctype html>
2+
<html lang="en" id="violation1">
3+
<head>
4+
<title>landmark-complementary-is-top-level test</title>
5+
<meta charset="utf8">
6+
<link rel="stylesheet" type="text/css" href="/node_modules/mocha/mocha.css" />
7+
<script src="/node_modules/mocha/mocha.js"></script>
8+
<script src="/node_modules/chai/chai.js"></script>
9+
<script src="/axe.js"></script>
10+
<script>
11+
mocha.setup({
12+
timeout: 10000,
13+
ui: 'bdd'
14+
});
15+
var assert = chai.assert;
16+
</script>
17+
</head>
18+
<body>
19+
<div role="navigation">
20+
<div role="complementary">
21+
<p>This is going to fail</p>
22+
</div>
23+
</div>
24+
<iframe id="frame1" src="frames/level1-fail.html"></iframe>
25+
<div id="mocha"></div>
26+
<script src="landmark-complementary-is-top-level-fail.js"></script>
27+
<script src="/test/integration/adapter.js"></script>
28+
</body>
29+
</html>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
describe('landmark-complementary-is-top-level test fail', function() {
2+
'use strict';
3+
var results;
4+
before(function(done) {
5+
window.addEventListener('load', function() {
6+
axe.run(
7+
{
8+
runOnly: {
9+
type: 'rule',
10+
values: ['landmark-complementary-is-top-level']
11+
}
12+
},
13+
function(err, r) {
14+
assert.isNull(err);
15+
results = r;
16+
done();
17+
}
18+
);
19+
});
20+
});
21+
22+
describe('violations', function() {
23+
it('should find 1', function() {
24+
assert.lengthOf(results.violations, 1);
25+
});
26+
27+
it('should find 2 nodes', function() {
28+
assert.lengthOf(results.violations[0].nodes, 2);
29+
});
30+
});
31+
32+
describe('passes', function() {
33+
it('should find none', function() {
34+
assert.lengthOf(results.passes, 0);
35+
});
36+
});
37+
38+
it('should find 0 inapplicable', function() {
39+
assert.lengthOf(results.inapplicable, 0);
40+
});
41+
42+
it('should find 0 incomplete', function() {
43+
assert.lengthOf(results.incomplete, 0);
44+
});
45+
});

0 commit comments

Comments
 (0)