From 76aa5ec5ac8a311b90974f0dba9cf92594f92019 Mon Sep 17 00:00:00 2001
From: Gabe <41127686+Zidious@users.noreply.github.com>
Date: Fri, 1 Oct 2021 16:50:29 +0100
Subject: [PATCH] fix(aria-allowed-attr): check for invalid `aria-attributes`
for `role="row"` (#3160)
---
doc/check-options.md | 28 +++++
lib/checks/aria/aria-allowed-attr-evaluate.js | 43 ++++++-
lib/checks/aria/aria-allowed-attr.json | 8 ++
lib/checks/aria/aria-allowed-role-evaluate.js | 1 -
test/checks/aria/allowed-attr.js | 117 ++++++++++++++++++
.../rules/aria-allowed-attr/failures.html | 24 ++++
.../rules/aria-allowed-attr/failures.json | 15 ++-
7 files changed, 229 insertions(+), 7 deletions(-)
diff --git a/doc/check-options.md b/doc/check-options.md
index 17f5adac6c..4a95f84dd7 100644
--- a/doc/check-options.md
+++ b/doc/check-options.md
@@ -199,6 +199,34 @@ All checks allow these global options:
+### aria-allowed-attr
+
+
+
+
+ Option |
+ Default |
+ Description |
+
+
+
+
+
+ validTreeRowAttrs
+ |
+
+ [
+ 'aria-posinset',
+ 'aria-setsize',
+ 'aria-expanded',
+ 'aria-level',
+]
+ |
+ List of ARIA attributes that are not allowed on role=row when a descendant of a table or a grid |
+
+
+
+
### color-contrast
| Option | Default | Description |
diff --git a/lib/checks/aria/aria-allowed-attr-evaluate.js b/lib/checks/aria/aria-allowed-attr-evaluate.js
index 59da14c22a..add21f2b7b 100644
--- a/lib/checks/aria/aria-allowed-attr-evaluate.js
+++ b/lib/checks/aria/aria-allowed-attr-evaluate.js
@@ -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.
@@ -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) + '"');
}
}
diff --git a/lib/checks/aria/aria-allowed-attr.json b/lib/checks/aria/aria-allowed-attr.json
index 9d8386d570..4079b3ebcb 100644
--- a/lib/checks/aria/aria-allowed-attr.json
+++ b/lib/checks/aria/aria-allowed-attr.json
@@ -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": {
diff --git a/lib/checks/aria/aria-allowed-role-evaluate.js b/lib/checks/aria/aria-allowed-role-evaluate.js
index e681e812de..5ca99e9fc9 100644
--- a/lib/checks/aria/aria-allowed-role-evaluate.js
+++ b/lib/checks/aria/aria-allowed-role-evaluate.js
@@ -27,7 +27,6 @@ function ariaAllowedRoleEvaluate(node, options = {}, virtualNode) {
}
const unallowedRoles = getElementUnallowedRoles(virtualNode, allowImplicit);
-
if (unallowedRoles.length) {
this.data(unallowedRoles);
if (!isVisible(virtualNode, true)) {
diff --git a/test/checks/aria/allowed-attr.js b/test/checks/aria/allowed-attr.js
index cbba61b843..5a8956e0f9 100644
--- a/test/checks/aria/allowed-attr.js
+++ b/test/checks/aria/allowed-attr.js
@@ -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(
+ ' '
+ );
+ 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(
+ ' '
+ );
+ 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(
+ ' '
+ );
+
+ 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(
+ ' '
+ );
+
+ 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() {
diff --git a/test/integration/rules/aria-allowed-attr/failures.html b/test/integration/rules/aria-allowed-attr/failures.html
index f4839431e7..578b339141 100644
--- a/test/integration/rules/aria-allowed-attr/failures.html
+++ b/test/integration/rules/aria-allowed-attr/failures.html
@@ -28,3 +28,27 @@
+
+fail
+fail
+fail
+fail
+
+
+
+
diff --git a/test/integration/rules/aria-allowed-attr/failures.json b/test/integration/rules/aria-allowed-attr/failures.json
index 6b48f87d29..ccedf0b10f 100644
--- a/test/integration/rules/aria-allowed-attr/failures.json
+++ b/test/integration/rules/aria-allowed-attr/failures.json
@@ -30,6 +30,19 @@
["#fail26"],
["#fail27"],
["#fail28"],
- ["#fail29"]
+ ["#fail29"],
+ ["#fail30"],
+ ["#fail31"],
+ ["#fail32"],
+ ["#fail33"],
+ ["#fail34"],
+ ["#fail35"],
+ ["#fail36"],
+ ["#fail37"],
+ ["#fail38"],
+ ["#fail39"],
+ ["#fail40"],
+ ["#fail41"],
+ ["#fail42"]
]
}