Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(aria-allowed-attr): check for invalid aria-attributes for role="row" #3160

Merged
merged 16 commits into from
Oct 1, 2021
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
41 changes: 37 additions & 4 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));
}

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": {
Zidious marked this conversation as resolved.
Show resolved Hide resolved
"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 @@ -97,6 +97,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
14 changes: 14 additions & 0 deletions test/integration/rules/aria-allowed-attr/failures.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,17 @@
<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>
10 changes: 9 additions & 1 deletion test/integration/rules/aria-allowed-attr/failures.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
["#fail31"],
["#fail32"],
["#fail33"],
["#fail34"]
["#fail34"],
["#fail35"],
["#fail36"],
["#fail37"],
["#fail38"],
["#fail39"],
["#fail40"],
["#fail41"],
["#fail42"]
]
}