diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 91f06e842c..21d0f49e02 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -72,34 +72,35 @@ Rules that do not necessarily conform to WCAG success criterion but are industry accepted practices that improve the user experience. -| Rule ID | Description | Impact | Tags | Issue Type | -| :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | :----------------- | :----------------------------------------- | :------------------------- | -| [accesskeys](https://dequeuniversity.com/rules/axe/4.0/accesskeys?application=RuleDescription) | Ensures every accesskey attribute value is unique | Serious | best-practice, cat.keyboard | failure | -| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.0/aria-allowed-role?application=RuleDescription) | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | -| [empty-heading](https://dequeuniversity.com/rules/axe/4.0/empty-heading?application=RuleDescription) | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | -| [frame-tested](https://dequeuniversity.com/rules/axe/4.0/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item, best-practice | failure, needs review | -| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.0/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, best-practice | failure | -| [heading-order](https://dequeuniversity.com/rules/axe/4.0/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure | -| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.0/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | wcag2aaa, wcag249, best-practice | needs review | -| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.0/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | -| [label-title-only](https://dequeuniversity.com/rules/axe/4.0/label-title-only?application=RuleDescription) | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | -| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.0/landmark-banner-is-top-level?application=RuleDescription) | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | -| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.0/landmark-complementary-is-top-level?application=RuleDescription) | Ensures the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice | failure | -| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.0/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | -| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.0/landmark-main-is-top-level?application=RuleDescription) | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | -| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.0/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | -| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.0/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | -| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.0/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | -| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.0/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | -| [landmark-unique](https://dequeuniversity.com/rules/axe/4.0/landmark-unique?application=RuleDescription) | Landmarks must have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | -| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.0/meta-viewport-large?application=RuleDescription) | Ensures <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | -| [meta-viewport](https://dequeuniversity.com/rules/axe/4.0/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, best-practice | failure | -| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.0/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | -| [region](https://dequeuniversity.com/rules/axe/4.0/region?application=RuleDescription) | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | failure | -| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.0/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | failure | -| [skip-link](https://dequeuniversity.com/rules/axe/4.0/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | -| [tabindex](https://dequeuniversity.com/rules/axe/4.0/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | -| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.0/table-duplicate-name?application=RuleDescription) | Ensure that tables do not have the same summary and caption | Minor | cat.tables, best-practice | failure | +| Rule ID | Description | Impact | Tags | Issue Type | +| :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :----------------- | :----------------------------------------- | :------------------------- | +| [accesskeys](https://dequeuniversity.com/rules/axe/4.0/accesskeys?application=RuleDescription) | Ensures every accesskey attribute value is unique | Serious | best-practice, cat.keyboard | failure | +| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.0/aria-allowed-role?application=RuleDescription) | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | +| [empty-heading](https://dequeuniversity.com/rules/axe/4.0/empty-heading?application=RuleDescription) | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | +| [frame-tested](https://dequeuniversity.com/rules/axe/4.0/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item, best-practice | failure, needs review | +| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.0/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, best-practice | failure | +| [heading-order](https://dequeuniversity.com/rules/axe/4.0/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure | +| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.0/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | wcag2aaa, wcag249, best-practice | needs review | +| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.0/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | +| [label-title-only](https://dequeuniversity.com/rules/axe/4.0/label-title-only?application=RuleDescription) | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | +| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.0/landmark-banner-is-top-level?application=RuleDescription) | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | +| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.0/landmark-complementary-is-top-level?application=RuleDescription) | Ensures the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice | failure | +| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.0/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | +| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.0/landmark-main-is-top-level?application=RuleDescription) | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | +| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.0/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | +| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.0/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | +| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.0/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | +| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.0/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | +| [landmark-unique](https://dequeuniversity.com/rules/axe/4.0/landmark-unique?application=RuleDescription) | Landmarks must have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | +| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.0/meta-viewport-large?application=RuleDescription) | Ensures <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | +| [meta-viewport](https://dequeuniversity.com/rules/axe/4.0/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, best-practice | failure | +| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.0/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | +| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.0/presentation-role-conflict?application=RuleDescription) | Flags elements whose role is none or presentation and which cause the role conflict resolution to trigger. | Minor | cat.aria, best-practice | failure | +| [region](https://dequeuniversity.com/rules/axe/4.0/region?application=RuleDescription) | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | failure | +| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.0/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | failure | +| [skip-link](https://dequeuniversity.com/rules/axe/4.0/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | +| [tabindex](https://dequeuniversity.com/rules/axe/4.0/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | +| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.0/table-duplicate-name?application=RuleDescription) | Ensure that tables do not have the same summary and caption | Minor | cat.tables, best-practice | failure | ## Experimental Rules diff --git a/lib/checks/aria/has-global-aria-attribute-evaluate.js b/lib/checks/aria/has-global-aria-attribute-evaluate.js new file mode 100644 index 0000000000..8be61ac04a --- /dev/null +++ b/lib/checks/aria/has-global-aria-attribute-evaluate.js @@ -0,0 +1,11 @@ +import getGlobalAriaAttrs from '../../commons/standards/get-global-aria-attrs'; + +function hasGlobalAriaAttributeEvaluate(node, options, virtualNode) { + const globalAttrs = getGlobalAriaAttrs().filter(attr => + virtualNode.hasAttr(attr) + ); + this.data(globalAttrs); + return globalAttrs.length > 0; +} + +export default hasGlobalAriaAttributeEvaluate; diff --git a/lib/checks/aria/has-global-aria-attribute.json b/lib/checks/aria/has-global-aria-attribute.json new file mode 100644 index 0000000000..6eb330dbed --- /dev/null +++ b/lib/checks/aria/has-global-aria-attribute.json @@ -0,0 +1,14 @@ +{ + "id": "has-global-aria-attribute", + "evaluate": "has-global-aria-attribute-evaluate", + "metadata": { + "impact": "minor", + "messages": { + "pass": { + "singular": "Element has global ARIA attribute: ${data.values}", + "plural": "Element has global ARIA attributes: ${data.values}" + }, + "fail": "Element does not have global ARIA attribute" + } + } +} diff --git a/lib/checks/aria/is-element-focusable-evaluate.js b/lib/checks/aria/is-element-focusable-evaluate.js new file mode 100755 index 0000000000..e1773adc79 --- /dev/null +++ b/lib/checks/aria/is-element-focusable-evaluate.js @@ -0,0 +1,7 @@ +import { isFocusable } from '../../commons/dom'; + +function isElementFocusableEvaluate(node, options, virtualNode) { + return isFocusable(virtualNode); +} + +export default isElementFocusableEvaluate; diff --git a/lib/checks/aria/is-element-focusable.json b/lib/checks/aria/is-element-focusable.json new file mode 100755 index 0000000000..871f99e35c --- /dev/null +++ b/lib/checks/aria/is-element-focusable.json @@ -0,0 +1,11 @@ +{ + "id": "is-element-focusable", + "evaluate": "is-element-focusable-evaluate", + "metadata": { + "impact": "minor", + "messages": { + "pass": "Element is focusable.", + "fail": "Element is not focusable." + } + } +} diff --git a/lib/core/base/metadata-function-map.js b/lib/core/base/metadata-function-map.js index ea04ab202c..f80a570d22 100644 --- a/lib/core/base/metadata-function-map.js +++ b/lib/core/base/metadata-function-map.js @@ -12,8 +12,10 @@ import ariaUnsupportedAttrEvaluate from '../../checks/aria/aria-unsupported-attr import ariaValidAttrEvaluate from '../../checks/aria/aria-valid-attr-evaluate'; import ariaValidAttrValueEvaluate from '../../checks/aria/aria-valid-attr-value-evaluate'; import fallbackroleEvaluate from '../../checks/aria/fallbackrole-evaluate'; +import hasGlobalAriaAttributeEvaluate from '../../checks/aria/has-global-aria-attribute-evaluate'; import hasWidgetRoleEvaluate from '../../checks/aria/has-widget-role-evaluate'; import invalidroleEvaluate from '../../checks/aria/invalidrole-evaluate'; +import isElementFocusableEvaluate from '../../checks/aria/is-element-focusable-evaluate'; import noImplicitExplicitLabelEvaluate from '../../checks/aria/no-implicit-explicit-label-evaluate'; import unsupportedroleEvaluate from '../../checks/aria/unsupportedrole-evaluate'; import validScrollableSemanticsEvaluate from '../../checks/aria/valid-scrollable-semantics-evaluate'; @@ -175,8 +177,10 @@ const metadataFunctionMap = { 'aria-valid-attr-evaluate': ariaValidAttrEvaluate, 'aria-valid-attr-value-evaluate': ariaValidAttrValueEvaluate, 'fallbackrole-evaluate': fallbackroleEvaluate, + 'has-global-aria-attribute-evaluate': hasGlobalAriaAttributeEvaluate, 'has-widget-role-evaluate': hasWidgetRoleEvaluate, 'invalidrole-evaluate': invalidroleEvaluate, + 'is-element-focusable-evaluate': isElementFocusableEvaluate, 'no-implicit-explicit-label-evaluate': noImplicitExplicitLabelEvaluate, 'unsupportedrole-evaluate': unsupportedroleEvaluate, 'valid-scrollable-semantics-evaluate': validScrollableSemanticsEvaluate, diff --git a/lib/rules/presentation-role-conflict.json b/lib/rules/presentation-role-conflict.json new file mode 100644 index 0000000000..b70cf74422 --- /dev/null +++ b/lib/rules/presentation-role-conflict.json @@ -0,0 +1,12 @@ +{ + "id": "presentation-role-conflict", + "selector": "[role=\"none\"], [role=\"presentation\"]", + "tags": ["cat.aria", "best-practice"], + "metadata": { + "description": "Flags elements whose role is none or presentation and which cause the role conflict resolution to trigger.", + "help": "Elements of role none or presentation should be flagged" + }, + "all": [], + "any": [], + "none": ["is-element-focusable", "has-global-aria-attribute"] +} diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 diff --git a/test/checks/aria/has-global-aria-attribute.js b/test/checks/aria/has-global-aria-attribute.js new file mode 100755 index 0000000000..9e2dc4f43b --- /dev/null +++ b/test/checks/aria/has-global-aria-attribute.js @@ -0,0 +1,31 @@ +describe('has-global-aria-attribute', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var checkSetup = axe.testUtils.checkSetup; + + var checkContext = axe.testUtils.MockCheckContext(); + var evaluate = axe.testUtils.getCheckEvaluate('has-global-aria-attribute'); + + afterEach(function() { + fixture.innerHTML = ''; + axe._tree = undefined; + checkContext.reset(); + }); + + it('should return true if any global ARIA attributes are found', function() { + var node = document.createElement('div'); + node.id = 'test'; + node.setAttribute('aria-label', 'hello'); + var params = checkSetup(node); + assert.isTrue(evaluate.apply(checkContext, params)); + }); + + it('should return false if no valid ARIA attributes are found', function() { + var node = document.createElement('div'); + node.id = 'test'; + node.setAttribute('aria-random', 'hello'); + var params = checkSetup(node); + assert.isFalse(evaluate.apply(checkContext, params)); + }); +}); diff --git a/test/checks/aria/is-element-focusable.js b/test/checks/aria/is-element-focusable.js new file mode 100755 index 0000000000..ff1b28a815 --- /dev/null +++ b/test/checks/aria/is-element-focusable.js @@ -0,0 +1,41 @@ +describe('is-element-focusable', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var checkContext = axe.testUtils.MockCheckContext(); + + afterEach(function() { + fixture.innerHTML = ''; + checkContext.reset(); + }); + + it('should return true for div with a tabindex', function() { + var node = document.createElement('div'); + node.id = 'target'; + node.tabIndex = 1; + fixture.appendChild(node); + axe._tree = axe.utils.getFlattenedTree(fixture); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + + assert.isTrue( + axe.testUtils + .getCheckEvaluate('is-element-focusable') + .call(checkContext, node, {}, virtualNode) + ); + }); + + it('should return false for natively unfocusable element', function() { + var node = document.createElement('span'); + node.id = 'target'; + node.role = 'link'; + node.href = '#'; + fixture.appendChild(node); + axe._tree = axe.utils.getFlattenedTree(fixture); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('is-element-focusable') + .call(checkContext, node, {}, virtualNode) + ); + }); +}); diff --git a/test/integration/rules/presentation-role-conflict/presentation-role-conflict.html b/test/integration/rules/presentation-role-conflict/presentation-role-conflict.html new file mode 100644 index 0000000000..9d95d87d89 --- /dev/null +++ b/test/integration/rules/presentation-role-conflict/presentation-role-conflict.html @@ -0,0 +1,4 @@ + + + +

Hello

diff --git a/test/integration/rules/presentation-role-conflict/presentation-role-conflict.json b/test/integration/rules/presentation-role-conflict/presentation-role-conflict.json new file mode 100644 index 0000000000..369ae80986 --- /dev/null +++ b/test/integration/rules/presentation-role-conflict/presentation-role-conflict.json @@ -0,0 +1,6 @@ +{ + "rule": "presentation-role-conflict", + "description": "presentation-role-conflict tests", + "violations": [["#violation1"], ["#violation2"]], + "passes": [["#pass1"], ["#pass2"]] +}