Skip to content

Commit

Permalink
fix(aria-allowed-attr): check for invalid aria-attributes for `role…
Browse files Browse the repository at this point in the history
…="row"` (#3160)
  • Loading branch information
Zidious authored and straker committed Oct 18, 2021
1 parent cf58aea commit 76aa5ec
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 7 deletions.
28 changes: 28 additions & 0 deletions doc/check-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,34 @@ All checks allow these global options:
</tbody>
</table>

### aria-allowed-attr

<table>
<thead>
<tr>
<th>Option</th>
<th align="left">Default</th>
<th align="left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>validTreeRowAttrs</code>
</td>
<td align="left">
<pre lang=js><code>[
'aria-posinset',
'aria-setsize',
'aria-expanded',
'aria-level',
]</code></pre>
</td>
<td align="left">List of ARIA attributes that are not allowed on <code>role=row</code> when a descendant of a table or a grid</td>
</tr>
</tbody>
</table>

### color-contrast

| Option | Default | Description |
Expand Down
43 changes: 38 additions & 5 deletions lib/checks/aria/aria-allowed-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { uniqueArray } from '../../core/utils';
import { uniqueArray, closest } from '../../core/utils';
import { getRole, allowedAttr, validateAttr } from '../../commons/aria';
import cache from '../../core/base/cache';

/**
* Check if each ARIA attribute on an element is allowed for its semantic role.
Expand Down Expand Up @@ -27,21 +28,53 @@ import { getRole, allowedAttr, validateAttr } from '../../commons/aria';
*/
function ariaAllowedAttrEvaluate(node, options, virtualNode) {
const invalid = [];

const role = getRole(virtualNode);
const attrs = virtualNode.attrNames;
let allowed = allowedAttr(role);

// @deprecated: allowed attr options to pass more attrs.
// configure the standards spec instead
if (Array.isArray(options[role])) {
allowed = uniqueArray(options[role].concat(allowed));
}

if (role && allowed) {
let tableMap = cache.get('aria-allowed-attr-table');
if (!tableMap) {
tableMap = new WeakMap();
cache.set('aria-allowed-attr-table', tableMap);
}

function validateRowAttrs() {
// check if the parent exists otherwise a TypeError will occur (virtual-nodes specifically)
if (virtualNode.parent && role === 'row') {
const table = closest(
virtualNode,
'table, [role="treegrid"], [role="table"], [role="grid"]'
);

let tableRole = tableMap.get(table);
if (table && !tableRole) {
tableRole = getRole(table);
tableMap.set(table, tableRole);
}
if (['table', 'grid'].includes(tableRole) && role === 'row') {
return true;
}
}
}
// Allows options to be mapped to object e.g. {'aria-level' : validateRowAttrs}
const ariaAttr = Array.isArray(options.validTreeRowAttrs)
? options.validTreeRowAttrs
: [];
const preChecks = {};
ariaAttr.forEach(attr => {
preChecks[attr] = validateRowAttrs;
});
if (allowed) {
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i];
if (validateAttr(attrName) && !allowed.includes(attrName)) {
if (validateAttr(attrName) && preChecks[attrName]?.()) {
invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
} else if (validateAttr(attrName) && !allowed.includes(attrName)) {
invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
}
}
Expand Down
8 changes: 8 additions & 0 deletions lib/checks/aria/aria-allowed-attr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{
"id": "aria-allowed-attr",
"evaluate": "aria-allowed-attr-evaluate",
"options": {
"validTreeRowAttrs": [
"aria-posinset",
"aria-setsize",
"aria-expanded",
"aria-level"
]
},
"metadata": {
"impact": "critical",
"messages": {
Expand Down
1 change: 0 additions & 1 deletion lib/checks/aria/aria-allowed-role-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ function ariaAllowedRoleEvaluate(node, options = {}, virtualNode) {
}

const unallowedRoles = getElementUnallowedRoles(virtualNode, allowImplicit);

if (unallowedRoles.length) {
this.data(unallowedRoles);
if (!isVisible(virtualNode, true)) {
Expand Down
117 changes: 117 additions & 0 deletions test/checks/aria/allowed-attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,123 @@ describe('aria-allowed-attr', function() {
);
assert.isNull(checkContext._data);
});
describe('invalid aria-attributes when used on role=row as a descendant of a table or a grid', function() {
[
'aria-posinset="1"',
'aria-setsize="1"',
'aria-expanded="true"',
'aria-level="1"'
].forEach(function(attrName) {
it(
'should return false when ' +
attrName +
' is used on role=row thats parent is a table',
function() {
var vNode = queryFixture(
' <div role="table">' +
'<div id="target" role="row" ' +
attrName +
'></div>' +
'</div>'
);
assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, null, vNode)
);
assert.isNotNull(checkContext._data);
}
);
});

[
'aria-posinset="1"',
'aria-setsize="1"',
'aria-expanded="true"',
'aria-level="1"'
].forEach(function(attrName) {
it(
'should return false when ' +
attrName +
' is used on role=row thats parent is a grid',
function() {
var vNode = queryFixture(
' <div role="grid">' +
'<div id="target" role="row" ' +
attrName +
'></div>' +
'</div>'
);
assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, null, vNode)
);
assert.isNotNull(checkContext._data);
}
);
});
});

describe('options.invalidRowAttrs on role=row when a descendant of a table or a grid', function() {
it('should return false when provided a single aria-attribute is provided for a table', function() {
axe.configure({
checks: [
{
id: 'aria-allowed-attr',
options: {
validTreeRowAttrs: ['aria-posinset']
}
}
]
});

var options = {
validTreeRowAttrs: ['aria-posinset']
};
var vNode = queryFixture(
' <div role="table">' +
'<div id="target" role="row" aria-posinset="2"><div role="cell"></div></div>' +
'</div>'
);

assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, options, vNode)
);
assert.isNotNull(checkContext._data);
});

it('should return false when provided a single aria-attribute is provided for a grid', function() {
axe.configure({
checks: [
{
id: 'aria-allowed-attr',
options: {
validTreeRowAttrs: ['aria-level']
}
}
]
});

var options = {
validTreeRowAttrs: ['aria-level']
};
var vNode = queryFixture(
' <div role="grid">' +
'<div id="target" role="row" aria-level="2"><div role="gridcell"></div></div>' +
'</div>'
);

assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, options, vNode)
);
assert.isNotNull(checkContext._data);
});
});

describe('options', function() {
it('should allow provided attribute names for a role', function() {
Expand Down
24 changes: 24 additions & 0 deletions test/integration/rules/aria-allowed-attr/failures.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,27 @@
<kbd aria-label="value" id="fail27"></kbd>
<abbr aria-label="value" id="fail28"></abbr>
<custom-elm aria-label="value" id="fail29"></custom-elm>
<audio
src="/test/assets/moon-speech.mp3"
controls
aria-orientation="horizontal"
id="fail30"
></audio>
<div role="mark" aria-label="value" id="fail31">fail</div>
<div role="mark" aria-labelledby="value" id="fail32">fail</div>
<div role="suggestion" aria-label="value" id="fail33">fail</div>
<div role="suggestion" aria-labelledby="value" id="fail34">fail</div>

<div role="table">
<div role="row" aria-expanded="false" id="fail35"></div>
<div role="row" aria-posinset="1" id="fail36"></div>
<div role="row" aria-setsize="10" id="fail37"></div>
<div role="row" aria-level="1" id="fail38"></div>
</div>

<div role="grid">
<div role="row" aria-expanded="false" id="fail39"></div>
<div role="row" aria-posinset="1" id="fail40"></div>
<div role="row" aria-setsize="10" id="fail41"></div>
<div role="row" aria-level="1" id="fail42"></div>
</div>
15 changes: 14 additions & 1 deletion test/integration/rules/aria-allowed-attr/failures.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
["#fail26"],
["#fail27"],
["#fail28"],
["#fail29"]
["#fail29"],
["#fail30"],
["#fail31"],
["#fail32"],
["#fail33"],
["#fail34"],
["#fail35"],
["#fail36"],
["#fail37"],
["#fail38"],
["#fail39"],
["#fail40"],
["#fail41"],
["#fail42"]
]
}

0 comments on commit 76aa5ec

Please sign in to comment.