diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md
index 044af174d7..9525e12313 100644
--- a/doc/rule-descriptions.md
+++ b/doc/rule-descriptions.md
@@ -33,6 +33,7 @@
| input-image-alt | Ensures <input type="image"> elements have alternate text | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true |
| label-title-only | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | cat.forms, best-practice | false |
| label | Ensures every form element has a label | cat.forms, wcag2a, wcag332, wcag131, section508, section508.22.n | true |
+| landmark-main-is-top-level | The main landmark should not be contained in another landmark | best-practice | true |
| layout-table | Ensures presentational <table> elements do not use <th>, <caption> elements or the summary attribute | cat.semantics, wcag2a, wcag131 | true |
| link-in-text-block | Links can be distinguished without relying on color | cat.color, experimental, wcag2a, wcag141 | true |
| link-name | Ensures links have discernible text | cat.name-role-value, wcag2a, wcag111, wcag412, wcag244, section508, section508.22.a | true |
diff --git a/lib/checks/keyboard/main-is-top-level.js b/lib/checks/keyboard/main-is-top-level.js
new file mode 100644
index 0000000000..562c2d360f
--- /dev/null
+++ b/lib/checks/keyboard/main-is-top-level.js
@@ -0,0 +1,13 @@
+var landmarks = axe.commons.aria.getRolesByType('landmark');
+var parent = axe.commons.dom.getComposedParent(node);
+while (parent){
+ var role = parent.getAttribute('role');
+ if (!role && (parent.tagName.toLowerCase() !== 'form')){
+ role = axe.commons.aria.implicitRole(parent);
+ }
+ if (role && landmarks.includes(role)){
+ return false;
+ }
+ parent = axe.commons.dom.getComposedParent(parent);
+}
+return true;
diff --git a/lib/checks/keyboard/main-is-top-level.json b/lib/checks/keyboard/main-is-top-level.json
new file mode 100644
index 0000000000..1f0038d336
--- /dev/null
+++ b/lib/checks/keyboard/main-is-top-level.json
@@ -0,0 +1,11 @@
+{
+ "id": "main-is-top-level",
+ "evaluate": "main-is-top-level.js",
+ "metadata": {
+ "impact": "moderate",
+ "messages": {
+ "pass": "The main landmark is at the top level.",
+ "fail": "The main landmark is contained in another landmark."
+ }
+ }
+}
diff --git a/lib/rules/landmark-main-is-top-level.json b/lib/rules/landmark-main-is-top-level.json
new file mode 100644
index 0000000000..b4255d4200
--- /dev/null
+++ b/lib/rules/landmark-main-is-top-level.json
@@ -0,0 +1,16 @@
+{
+ "id": "landmark-main-is-top-level",
+ "selector": "main,[role=main]",
+ "tags": [
+ "best-practice"
+ ],
+ "metadata": {
+ "description": "The main landmark should not be contained in another landmark",
+ "help": "Main landmark is not at top level"
+ },
+ "all": [],
+ "any": [
+ "main-is-top-level"
+ ],
+ "none": []
+}
diff --git a/test/checks/keyboard/main-is-top-level.js b/test/checks/keyboard/main-is-top-level.js
new file mode 100644
index 0000000000..27316deaa0
--- /dev/null
+++ b/test/checks/keyboard/main-is-top-level.js
@@ -0,0 +1,70 @@
+describe('main-is-top-level', function () {
+ 'use strict';
+
+ var fixture = document.getElementById('fixture');
+
+ var shadowSupported = axe.testUtils.shadowSupport.v1;
+ var checkSetup = axe.testUtils.checkSetup;
+
+ afterEach(function () {
+ fixture.innerHTML = '';
+ });
+
+ it('should return false if main landmark is in another landmark', function () {
+ var mainLandmark = document.createElement('main');
+ var bannerDiv = document.createElement('div');
+ bannerDiv.setAttribute('role','banner');
+ bannerDiv.appendChild(mainLandmark);
+ fixture.appendChild(bannerDiv);
+ assert.isFalse(checks['main-is-top-level'].evaluate(mainLandmark));
+ });
+
+ it('should return false if div with role set to main is in another landmark', function () {
+ var mainDiv = document.createElement('div');
+ mainDiv.setAttribute('role','main');
+ var navDiv = document.createElement('div');
+ navDiv.setAttribute('role','navigation');
+ navDiv.appendChild(mainDiv);
+ fixture.appendChild(navDiv);
+ assert.isFalse(checks['main-is-top-level'].evaluate(mainDiv));
+ });
+
+ it('should return true if main landmark is not in another landmark', function () {
+ var mainLandmark = document.createElement('main');
+ var bannerDiv = document.createElement('div');
+ bannerDiv.setAttribute('role','banner');
+ fixture.appendChild(bannerDiv);
+ fixture.appendChild(mainLandmark);
+ assert.isTrue(checks['main-is-top-level'].evaluate(mainLandmark));
+ });
+
+ it('should return true if div with role set to main is not in another landmark', function () {
+ var mainDiv = document.createElement('div');
+ mainDiv.setAttribute('role','main');
+ var navDiv = document.createElement('div');
+ navDiv.setAttribute('role','navigation');
+ fixture.appendChild(navDiv);
+ fixture.appendChild(mainDiv);
+ assert.isTrue(checks['main-is-top-level'].evaluate(mainDiv));
+ });
+
+ it('should return true if main is in form landmark', function () {
+ var mainDiv = document.createElement('div');
+ mainDiv.setAttribute('role','main');
+ var formDiv = document.createElement('div');
+ formDiv.setAttribute('role','form');
+ fixture.appendChild(formDiv);
+ fixture.appendChild(mainDiv);
+ assert.isTrue(checks['main-is-top-level'].evaluate(mainDiv));
+ });
+
+
+ (shadowSupported ? it : xit)('should test if main in shadow DOM is top level', function () {
+ var div = document.createElement('div');
+ var shadow = div.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '
This iframe should fail, too
+This main landmark is in a complementary landmark
+This iframe should pass, too
+ +This div has role banner
+This main content is not within another landmark
+This div has role complementary
+This div has role search
+This div has role form
+
This iframe is also a violation
+ + + diff --git a/test/integration/full/landmark-main-is-top-level/frames/level2.html b/test/integration/full/landmark-main-is-top-level/frames/level2.html new file mode 100644 index 0000000000..a0c30fd536 --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/frames/level2.html @@ -0,0 +1,15 @@ + + + + + + + +This iframe is another violation
+
This main landmark is in a search landmark
+This div has role banner
+This main content is not within another landmark
+This div has role complementary
+This div has role search
+This div has role form
+