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 = '
This iframe should fail, too
+This iframe should pass, too
+ + + +This div has role complementary
+This div has role search
+This div has role form
+
This iframe should pass
+
This contentinfo is not within another landmark
+This div has role complementary
+This div has role search
+This div has role form
+