diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 2f83018a1d..f70b77b21a 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -35,6 +35,7 @@ | label-title-only | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | cat.forms, best-practice | true | | label | Ensures every form element has a label | cat.forms, wcag2a, wcag332, wcag131, section508, section508.22.n | true | | landmark-banner-is-top-level | A banner landmark identifies site-oriented content at the beginning of each page within a website | best-practice | true | +| landmark-contentinfo-is-top-level | A contentinfo landmark is a way to identify common information at the bottom of each page within a website | best-practice | true | | landmark-main-is-top-level | The main landmark should not be contained in another landmark | best-practice | true | | landmark-no-duplicate-banner | Ensures the document has no more than one banner landmark | best-practice | true | | landmark-no-duplicate-contentinfo | Ensures the document has no more than one contentinfo landmark | best-practice | true | diff --git a/lib/checks/keyboard/contentinfo-is-top-level.js b/lib/checks/keyboard/contentinfo-is-top-level.js new file mode 100644 index 0000000000..1d0b5b66a3 --- /dev/null +++ b/lib/checks/keyboard/contentinfo-is-top-level.js @@ -0,0 +1,19 @@ +const landmarks = axe.commons.aria.getRolesByType('landmark'); +const sectioning = ['article', 'aside', 'main', 'navigation', 'section']; +const nodeIsHeader = node.tagName.toLowerCase() === 'footer' && node.getAttribute('role') !== 'contentinfo'; +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 && nodeIsHeader && sectioning.includes(role)){ + return true; + } + if (role && landmarks.includes(role)){ + return false; + } + parent = axe.commons.dom.getComposedParent(parent); +} +return true; diff --git a/lib/checks/keyboard/contentinfo-is-top-level.json b/lib/checks/keyboard/contentinfo-is-top-level.json new file mode 100644 index 0000000000..aaea4b457c --- /dev/null +++ b/lib/checks/keyboard/contentinfo-is-top-level.json @@ -0,0 +1,11 @@ +{ + "id": "contentinfo-is-top-level", + "evaluate": "contentinfo-is-top-level.js", + "metadata": { + "impact": "moderate", + "messages": { + "pass": "Contentinfo landmark is top level or footer element is not contentinfo", + "fail": "Contentinfo landmark is not top level" + } + } +} diff --git a/lib/rules/landmark-contentinfo-is-top-level.json b/lib/rules/landmark-contentinfo-is-top-level.json new file mode 100644 index 0000000000..6cd6586d9e --- /dev/null +++ b/lib/rules/landmark-contentinfo-is-top-level.json @@ -0,0 +1,16 @@ +{ + "id": "landmark-contentinfo-is-top-level", + "selector": "[role=contentinfo], footer", + "tags": [ + "best-practice" + ], + "metadata": { + "description": "A contentinfo landmark is a way to identify common information at the bottom of each page within a website", + "help": "Contentinfo landmark must be at top level" + }, + "all": [], + "any": [ + "contentinfo-is-top-level" + ], + "none": [] +} diff --git a/test/checks/keyboard/contentinfo-is-top-level.js b/test/checks/keyboard/contentinfo-is-top-level.js new file mode 100644 index 0000000000..89c12cc0ee --- /dev/null +++ b/test/checks/keyboard/contentinfo-is-top-level.js @@ -0,0 +1,89 @@ +describe('contentinfo-is-top-level', function () { + 'use strict'; + + var fixture = document.getElementById('fixture'); + + var checkSetup = axe.testUtils.checkSetup; + var shadowSupported = axe.testUtils.shadowSupport.v1; + + afterEach(function () { + fixture.innerHTML = ''; + }); + + it('should return false if contentinfo landmark is in main element', function() { + var main = document.createElement('main'); + var contentinfo = document.createElement('div'); + contentinfo.setAttribute('role','contentinfo'); + main.appendChild(contentinfo); + fixture.appendChild(main); + assert.isFalse(checks['contentinfo-is-top-level'].evaluate(contentinfo)); + }); + + it('should return false if contentinfo landmark is in main element', function () { + var main = document.createElement('main'); + var contentinfo = document.createElement('div'); + contentinfo.setAttribute('role','contentinfo'); + main.appendChild(contentinfo); + fixture.appendChild(main); + assert.isFalse(checks['contentinfo-is-top-level'].evaluate(contentinfo)); + }); + + it('should return false if contentinfo landmark is in div with role main', function () { + var main = document.createElement('div'); + main.setAttribute('role','main'); + var contentinfo = document.createElement('div'); + contentinfo.setAttribute('role','contentinfo'); + main.appendChild(contentinfo); + fixture.appendChild(main); + assert.isFalse(checks['contentinfo-is-top-level'].evaluate(contentinfo)); + }); + + it('should return false if footer is not sectioning element and in div with role search', function () { + var search = document.createElement('div'); + search.setAttribute('role','search'); + var contentinfo = document.createElement('footer'); + search.appendChild(contentinfo); + fixture.appendChild(search); + assert.isFalse(checks['contentinfo-is-top-level'].evaluate(contentinfo)); + }); + + + it('should return true if contentinfo landmark is not contained in another landmark', function () { + var contentinfo = document.createElement('div'); + contentinfo.setAttribute('role','contentinfo'); + fixture.appendChild(contentinfo); + assert.isTrue(checks['contentinfo-is-top-level'].evaluate(contentinfo)); + }); + + it('should return true if footer element is not sectioning element and not contained in landmark', function () { + var footer = document.createElement('footer'); + fixture.appendChild(footer); + assert.isTrue(checks['contentinfo-is-top-level'].evaluate(footer)); + }); + + it('should return true if footer element is in sectioning element', function () { + var footer = document.createElement('footer'); + var article = document.createElement('div'); + article.setAttribute('role', 'main'); + article.appendChild(footer); + fixture.appendChild(article); + assert.isTrue(checks['contentinfo-is-top-level'].evaluate(footer)); + }); + + (shadowSupported ? it : xit)('should test if contentinfo in shadow DOM is top level', function () { + var div = document.createElement('div'); + var shadow = div.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
contentinfo landmark
'; + var checkArgs = checkSetup(shadow.querySelector('[role=contentinfo], footer')); + assert.isTrue(checks['contentinfo-is-top-level'].evaluate.apply(null, checkArgs)); + }); + + (shadowSupported ? it : xit)('should test if footer in shadow DOM is top level', function () { + var div = document.createElement('div'); + var shadow = div.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; + var checkArgs = checkSetup(shadow.querySelector('[role=contentinfo], footer')); + assert.isTrue(checks['contentinfo-is-top-level'].evaluate.apply(null, checkArgs)); + }); + +}); diff --git a/test/integration/full/landmark-contentinfo-is-top-level/frames/level1-fail.html b/test/integration/full/landmark-contentinfo-is-top-level/frames/level1-fail.html new file mode 100644 index 0000000000..f8b61c14ca --- /dev/null +++ b/test/integration/full/landmark-contentinfo-is-top-level/frames/level1-fail.html @@ -0,0 +1,15 @@ + + + + + + + +

This iframe should fail, too

+
+ +
+ + diff --git a/test/integration/full/landmark-contentinfo-is-top-level/frames/level1.html b/test/integration/full/landmark-contentinfo-is-top-level/frames/level1.html new file mode 100644 index 0000000000..95f49f2cad --- /dev/null +++ b/test/integration/full/landmark-contentinfo-is-top-level/frames/level1.html @@ -0,0 +1,27 @@ + + + + + + + +

This iframe should pass, too

+ +
+

This div has role navigation

+
+ +
+

This div has role complementary

+
+
+

This div has role search

+
+
+

This div has role form

+

+ + + diff --git a/test/integration/full/landmark-contentinfo-is-top-level/frames/level2.html b/test/integration/full/landmark-contentinfo-is-top-level/frames/level2.html new file mode 100644 index 0000000000..3c7dae0847 --- /dev/null +++ b/test/integration/full/landmark-contentinfo-is-top-level/frames/level2.html @@ -0,0 +1,15 @@ + + + + + + + +

This iframe should pass

+

+ +
+ + diff --git a/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-fail.html b/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-fail.html new file mode 100644 index 0000000000..220f029c82 --- /dev/null +++ b/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-fail.html @@ -0,0 +1,29 @@ + + + + landmark-contentinfo-is-top-level test + + + + + + + + +
+
+

This is going to fail

+
+
+ +
+ + + + diff --git a/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-fail.js b/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-fail.js new file mode 100644 index 0000000000..5ec0cdf914 --- /dev/null +++ b/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-fail.js @@ -0,0 +1,40 @@ +describe('landmark-contentinfo-is-top-level test fail', function () { + 'use strict'; + var results; + before(function (done) { + window.addEventListener('load', function () { + axe.run({ runOnly: { type: 'rule', values: ['landmark-contentinfo-is-top-level'] } }, function (err, r) { + assert.isNull(err); + results = r; + done(); + }); + }); + }); + + describe('violations', function () { + it('should find 1', function () { + assert.lengthOf(results.violations, 1); + }); + + it('should find 2 nodes', function () { + assert.lengthOf(results.violations[0].nodes, 2); + }); + }); + + describe('passes', function () { + it('should find none', function () { + assert.lengthOf(results.passes, 0); + }); + + }); + + + it('should find 0 inapplicable', function () { + assert.lengthOf(results.inapplicable, 0); + }); + + it('should find 0 incomplete', function () { + assert.lengthOf(results.incomplete, 0); + }); + +}); diff --git a/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-pass.html b/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-pass.html new file mode 100644 index 0000000000..249555395d --- /dev/null +++ b/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-pass.html @@ -0,0 +1,39 @@ + + + + landmark-contentinfo-is-top-level test + + + + + + + + +
+

This div has role navigation

+
+
+

This contentinfo is not within another landmark

+
+
+

This div has role complementary

+
+
+

This div has role search

+
+
+

This div has role form

+

+ +
+ + + + diff --git a/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-pass.js b/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-pass.js new file mode 100644 index 0000000000..ce674404c5 --- /dev/null +++ b/test/integration/full/landmark-contentinfo-is-top-level/landmark-contentinfo-is-top-level-pass.js @@ -0,0 +1,34 @@ +describe('landmark-contentinfo-is-top-level test pass', function () { + 'use strict'; + var results; + before(function (done) { + window.addEventListener('load', function () { + axe.run({ runOnly: { type: 'rule', values: ['landmark-contentinfo-is-top-level'] } }, function (err, r) { + assert.isNull(err); + results = r; + done(); + }); + }); + }); + + describe('violations', function () { + it('should find 0', function () { + assert.lengthOf(results.violations, 0); + }); + }); + + describe('passes', function () { + it('should find 3', function () { + assert.lengthOf(results.passes[0].nodes, 3); + }); + }); + + it('should find 0 inapplicable', function () { + assert.lengthOf(results.inapplicable, 0); + }); + + it('should find 0 incomplete', function () { + assert.lengthOf(results.incomplete, 0); + }); + +});