Skip to content

Commit

Permalink
feat(aria-allowed-attr): add ARIA 1.2 prohibited attrs check (#2764)
Browse files Browse the repository at this point in the history
* chore: add prohibited attrs to aria-allowed-attr

* fix ie11
  • Loading branch information
straker authored Jan 19, 2021
1 parent fd639fb commit 4a77e88
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 13 deletions.
58 changes: 58 additions & 0 deletions lib/checks/aria/aria-prohibited-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getRole } from '../../commons/aria';
import standards from '../../standards';

/**
* Check that an element does not use any prohibited ARIA attributes.
*
* Prohibited attributes are taken from the `ariaAttrs` standards object from the attributes `prohibitedAttrs` property.
*
* ##### Data:
* <table class="props">
* <thead>
* <tr>
* <th>Type</th>
* <th>Description</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td><code>String[]</code></td>
* <td>List of all prohibited attributes</td>
* </tr>
* </tbody>
* </table>
*
* @memberof checks
* @return {Boolean} True if the element does not use any prohibited ARIA attributes. False otherwise.
*/
function ariaProhibitedAttrEvaluate(node, options, virtualNode) {
const prohibited = [];
const role = getRole(virtualNode);

if (!role) {
return false;
}

const attrs = virtualNode.attrNames;
const prohibitedAttrs = standards.ariaRoles[role].prohibitedAttrs;

if (!prohibitedAttrs) {
return false;
}

for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i];
if (prohibitedAttrs.includes(attrName)) {
prohibited.push(attrName);
}
}

if (prohibited.length) {
this.data(prohibited);
return true;
}

return false;
}

export default ariaProhibitedAttrEvaluate;
11 changes: 11 additions & 0 deletions lib/checks/aria/aria-prohibited-attr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "aria-prohibited-attr",
"evaluate": "aria-prohibited-attr-evaluate",
"metadata": {
"impact": "critical",
"messages": {
"pass": "ARIA attribute is allowed",
"fail": "ARIA attribute cannot be used: ${data.values}"
}
}
}
2 changes: 2 additions & 0 deletions lib/core/base/metadata-function-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ariaAllowedAttrEvaluate from '../../checks/aria/aria-allowed-attr-evaluat
import ariaAllowedRoledEvaluate from '../../checks/aria/aria-allowed-role-evaluate';
import ariaErrormessageEvaluate from '../../checks/aria/aria-errormessage-evaluate';
import ariaHiddenBodyEvaluate from '../../checks/aria/aria-hidden-body-evaluate';
import ariaProhibitedAttrEvaluate from '../../checks/aria/aria-prohibited-attr-evaluate';
import ariaRequiredAttrEvaluate from '../../checks/aria/aria-required-attr-evaluate';
import ariaRequiredChildrenEvaluate from '../../checks/aria/aria-required-children-evaluate';
import ariaRequiredParentEvaluate from '../../checks/aria/aria-required-parent-evaluate';
Expand Down Expand Up @@ -175,6 +176,7 @@ const metadataFunctionMap = {
'aria-allowed-role-evaluate': ariaAllowedRoledEvaluate,
'aria-errormessage-evaluate': ariaErrormessageEvaluate,
'aria-hidden-body-evaluate': ariaHiddenBodyEvaluate,
'aria-prohibited-attr-evaluate': ariaProhibitedAttrEvaluate,
'aria-required-attr-evaluate': ariaRequiredAttrEvaluate,
'aria-required-children-evaluate': ariaRequiredChildrenEvaluate,
'aria-required-parent-evaluate': ariaRequiredParentEvaluate,
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/aria-allowed-attr.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
},
"all": [],
"any": ["aria-allowed-attr"],
"none": ["aria-unsupported-attr"]
"none": ["aria-unsupported-attr", "aria-prohibited-attr"]
}
33 changes: 22 additions & 11 deletions lib/standards/aria-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ const ariaRoles = {
caption: {
type: 'structure',
requiredContext: ['figure', 'table', 'grid', 'treegrid'],
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
cell: {
type: 'structure',
Expand Down Expand Up @@ -92,7 +93,8 @@ const ariaRoles = {
},
code: {
type: 'structure',
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
columnheader: {
type: 'structure',
Expand Down Expand Up @@ -156,7 +158,8 @@ const ariaRoles = {
},
deletion: {
type: 'structure',
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
dialog: {
type: 'widget',
Expand All @@ -178,7 +181,8 @@ const ariaRoles = {
},
emphasis: {
type: 'structure',
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
feed: {
type: 'structure',
Expand Down Expand Up @@ -257,7 +261,8 @@ const ariaRoles = {
},
insertion: {
type: 'structure',
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
landmark: {
type: 'abstract',
Expand Down Expand Up @@ -397,7 +402,8 @@ const ariaRoles = {
},
none: {
type: 'structure',
superclassRole: ['structure']
superclassRole: ['structure'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
note: {
type: 'structure',
Expand All @@ -423,11 +429,13 @@ const ariaRoles = {
},
paragraph: {
type: 'structure',
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
presentation: {
type: 'structure',
superclassRole: ['structure']
superclassRole: ['structure'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
progressbar: {
type: 'widget',
Expand Down Expand Up @@ -642,19 +650,22 @@ const ariaRoles = {
},
strong: {
type: 'structure',
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
structure: {
type: 'abstract',
superclassRole: ['roletype']
},
subscript: {
type: 'structure',
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
superscript: {
type: 'structure',
superclassRole: ['section']
superclassRole: ['section'],
prohibitedAttrs: ['aria-label', 'aria-labelledby']
},
switch: {
type: 'widget',
Expand Down
46 changes: 46 additions & 0 deletions test/checks/aria/aria-prohibited-attr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
describe.only('aria-prohibited-attr', function() {
'use strict';

var checkContext = axe.testUtils.MockCheckContext();
var checkSetup = axe.testUtils.checkSetup;
var checkEvaluate = axe.testUtils.getCheckEvaluate('aria-prohibited-attr');

afterEach(function() {
checkContext.reset();
});

it('should return true for prohibited attributes', function() {
var params = checkSetup(
'<div id="target" role="code" aria-hidden="false" aria-label="foo">Contents</div>'
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['aria-label']);
});

it('should return true for multiple prohibited attributes', function() {
var params = checkSetup(
'<div id="target" role="code" aria-hidden="false" aria-label="foo" aria-labelledby="foo">Contents</div>'
);
assert.isTrue(checkEvaluate.apply(checkContext, params));

// attribute order not important
assert.sameDeepMembers(checkContext._data, [
'aria-label',
'aria-labelledby'
]);
});

it('should return false if all attributes are allowed', function() {
var params = checkSetup(
'<div id="target" role="button" aria-label="foo" aria-labelledby="foo">Contents</div>'
);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('should return false if element has no role', function() {
var params = checkSetup(
'<div id="target" aria-label="foo" aria-labelledby="foo">Contents</div>'
);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});
});
19 changes: 19 additions & 0 deletions test/integration/rules/aria-allowed-attr/failures.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,22 @@
<div role="link" aria-selected="true" id="fail2">fail</div>
<div role="row" aria-colcount="value" id="fail3">fail</div>
<div role="row" aria-rowcount="value" id="fail4">fail</div>
<div role="caption" aria-label="value" id="fail5">fail</div>
<div role="caption" aria-labelledby="value" id="fail6">fail</div>
<div role="code" aria-label="value" id="fail7">fail</div>
<div role="code" aria-labelledby="value" id="fail8">fail</div>
<div role="deletion" aria-label="value" id="fail9">fail</div>
<div role="deletion" aria-labelledby="value" id="fail10">fail</div>
<div role="emphasis" aria-label="value" id="fail11">fail</div>
<div role="emphasis" aria-labelledby="value" id="fail12">fail</div>
<div role="insertion" aria-label="value" id="fail13">fail</div>
<div role="insertion" aria-labelledby="value" id="fail14">fail</div>
<div role="paragraph" aria-label="value" id="fail15">fail</div>
<div role="paragraph" aria-labelledby="value" id="fail16">fail</div>
<div role="strong" aria-label="value" id="fail17">fail</div>
<div role="strong" aria-labelledby="value" id="fail18">fail</div>
<div role="subscript" aria-label="value" id="fail19">fail</div>
<div role="subscript" aria-labelledby="value" id="fail20">fail</div>
<div role="superscript" aria-label="value" id="fail21">fail</div>
<div role="superscript" aria-labelledby="value" id="fail22">fail</div>
<!-- technically presentation and none roles do not allow aria-label and aria-labelledby, but since those are global attributes the presentation role conflict will not resolve the roles to none or presentation -->
25 changes: 24 additions & 1 deletion test/integration/rules/aria-allowed-attr/failures.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
{
"description": "aria-allowed-attr failing tests",
"rule": "aria-allowed-attr",
"violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"]]
"violations": [
["#fail1"],
["#fail2"],
["#fail3"],
["#fail4"],
["#fail5"],
["#fail6"],
["#fail7"],
["#fail8"],
["#fail9"],
["#fail10"],
["#fail11"],
["#fail12"],
["#fail13"],
["#fail14"],
["#fail15"],
["#fail16"],
["#fail17"],
["#fail18"],
["#fail19"],
["#fail20"],
["#fail21"],
["#fail22"]
]
}

0 comments on commit 4a77e88

Please sign in to comment.