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 + + + + + + + + + + + + + + + + +
OptionDefaultDescription
+ 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 19775a9528..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)); } + 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 e63e64bbb9..8a6cc452a2 100644 --- a/test/checks/aria/allowed-attr.js +++ b/test/checks/aria/allowed-attr.js @@ -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( + '
' + + '
' + + '
' + ); + 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 985a64363f..578b339141 100644 --- a/test/integration/rules/aria-allowed-attr/failures.html +++ b/test/integration/rules/aria-allowed-attr/failures.html @@ -38,3 +38,17 @@
fail
fail
fail
+ +
+ +
+
+
+
+ +
+ +
+
+
+
diff --git a/test/integration/rules/aria-allowed-attr/failures.json b/test/integration/rules/aria-allowed-attr/failures.json index 5c5fb11ee7..ccedf0b10f 100644 --- a/test/integration/rules/aria-allowed-attr/failures.json +++ b/test/integration/rules/aria-allowed-attr/failures.json @@ -35,6 +35,14 @@ ["#fail31"], ["#fail32"], ["#fail33"], - ["#fail34"] + ["#fail34"], + ["#fail35"], + ["#fail36"], + ["#fail37"], + ["#fail38"], + ["#fail39"], + ["#fail40"], + ["#fail41"], + ["#fail42"] ] }