From 07c5d91c658bda6bcd2743950bf70f25abd1f9ae Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 22 Jul 2024 08:18:20 -0600 Subject: [PATCH] fix(aria-prohibited-attr): allow aria-label/ledby on decendants of widget (#4541) Closes: https://github.com/dequelabs/axe-core/issues/2953 --------- Co-authored-by: Wilco Fiers --- .../aria/aria-prohibited-attr-evaluate.js | 27 ++- test/checks/aria/aria-prohibited-attr.js | 158 ++++++++++++++---- .../aria-prohibited-attr.html | 4 + .../aria-prohibited-attr.json | 14 +- 4 files changed, 167 insertions(+), 36 deletions(-) diff --git a/lib/checks/aria/aria-prohibited-attr-evaluate.js b/lib/checks/aria/aria-prohibited-attr-evaluate.js index e6e2f9da7d..cc9cefd5f8 100644 --- a/lib/checks/aria/aria-prohibited-attr-evaluate.js +++ b/lib/checks/aria/aria-prohibited-attr-evaluate.js @@ -1,6 +1,7 @@ -import { getRole } from '../../commons/aria'; +import { getRole, getRoleType } from '../../commons/aria'; import { sanitize, subtreeText } from '../../commons/text'; import standards from '../../standards'; +import memoize from '../../core/utils/memoize'; /** * Check that an element does not use any prohibited ARIA attributes. @@ -36,6 +37,7 @@ export default function ariaProhibitedAttrEvaluate( const role = getRole(virtualNode, { chromium: true }); const prohibitedList = listProhibitedAttrs( + virtualNode, role, nodeName, elementsAllowedAriaLabel @@ -64,13 +66,32 @@ export default function ariaProhibitedAttrEvaluate( return true; } -function listProhibitedAttrs(role, nodeName, elementsAllowedAriaLabel) { +function listProhibitedAttrs(vNode, role, nodeName, elementsAllowedAriaLabel) { const roleSpec = standards.ariaRoles[role]; if (roleSpec) { return roleSpec.prohibitedAttrs || []; } - if (!!role || elementsAllowedAriaLabel.includes(nodeName)) { + if ( + !!role || + elementsAllowedAriaLabel.includes(nodeName) || + getClosestAncestorRoleType(vNode) === 'widget' + ) { return []; } return ['aria-label', 'aria-labelledby']; } + +const getClosestAncestorRoleType = memoize( + function getClosestAncestorRoleTypeMemoized(vNode) { + if (!vNode) { + return; + } + + const role = getRole(vNode, { noPresentational: true, chromium: true }); + if (role) { + return getRoleType(role); + } + + return getClosestAncestorRoleType(vNode.parent); + } +); diff --git a/test/checks/aria/aria-prohibited-attr.js b/test/checks/aria/aria-prohibited-attr.js index 4ae70f6bb0..ad893a5d64 100644 --- a/test/checks/aria/aria-prohibited-attr.js +++ b/test/checks/aria/aria-prohibited-attr.js @@ -1,16 +1,16 @@ -describe('aria-prohibited-attr', function () { +describe('aria-prohibited-attr', () => { 'use strict'; - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var checkEvaluate = axe.testUtils.getCheckEvaluate('aria-prohibited-attr'); + const checkContext = axe.testUtils.MockCheckContext(); + const checkSetup = axe.testUtils.checkSetup; + const checkEvaluate = axe.testUtils.getCheckEvaluate('aria-prohibited-attr'); - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('should return true for prohibited attributes and no content', function () { - var params = checkSetup( + it('should return true for prohibited attributes and no content', () => { + const params = checkSetup( '
' ); assert.isTrue(checkEvaluate.apply(checkContext, params)); @@ -22,8 +22,8 @@ describe('aria-prohibited-attr', function () { }); }); - it('should return undefined for prohibited attributes and content', function () { - var params = checkSetup( + it('should return undefined for prohibited attributes and content', () => { + const params = checkSetup( '
Contents
' ); assert.isUndefined(checkEvaluate.apply(checkContext, params)); @@ -35,8 +35,8 @@ describe('aria-prohibited-attr', function () { }); }); - it('should return true for multiple prohibited attributes', function () { - var params = checkSetup( + it('should return true for multiple prohibited attributes', () => { + const params = checkSetup( '
' ); assert.isTrue(checkEvaluate.apply(checkContext, params)); @@ -49,8 +49,10 @@ describe('aria-prohibited-attr', function () { }); }); - it('should return undefined if element has no role and has text content (singular)', function () { - var params = checkSetup('
Contents
'); + it('should return undefined if element has no role and has text content (singular)', () => { + const params = checkSetup( + '
Contents
' + ); assert.isUndefined(checkEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { nodeName: 'div', @@ -60,8 +62,8 @@ describe('aria-prohibited-attr', function () { }); }); - it('should return undefined if element has no role and has text content (plural)', function () { - var params = checkSetup( + it('should return undefined if element has no role and has text content (plural)', () => { + const params = checkSetup( '
Contents
' ); assert.isUndefined(checkEvaluate.apply(checkContext, params)); @@ -73,8 +75,8 @@ describe('aria-prohibited-attr', function () { }); }); - it('should return true if element has no role and no text content (singular)', function () { - var params = checkSetup('
'); + it('should return true if element has no role and no text content (singular)', () => { + const params = checkSetup('
'); assert.isTrue(checkEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { nodeName: 'div', @@ -84,8 +86,8 @@ describe('aria-prohibited-attr', function () { }); }); - it('should return true if element has no role and no text content (plural)', function () { - var params = checkSetup( + it('should return true if element has no role and no text content (plural)', () => { + const params = checkSetup( '
' ); assert.isTrue(checkEvaluate.apply(checkContext, params)); @@ -97,45 +99,139 @@ describe('aria-prohibited-attr', function () { }); }); - it('should return false if all attributes are allowed', function () { - var params = checkSetup( + it('should return false if all attributes are allowed', () => { + const params = checkSetup( '
Contents
' ); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); - it('should return false if no prohibited attributes are used', function () { - var params = checkSetup( + it('should return false if no prohibited attributes are used', () => { + const params = checkSetup( '
Contents
' ); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); - it('should return false if prohibited attributes have no value', function () { - var params = checkSetup( + it('should return false if prohibited attributes have no value', () => { + const params = checkSetup( '
Contents
' ); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); - it('should allow `elementsAllowedAriaLabel` nodes to have aria-label', function () { - var params = checkSetup( + it('should allow `elementsAllowedAriaLabel` nodes to have aria-label', () => { + const params = checkSetup( '
', { elementsAllowedAriaLabel: ['div'] } ); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); - it('should not allow `elementsAllowedAriaLabel` nodes with a prohibited role', function () { - var params = checkSetup( + it('should not allow `elementsAllowedAriaLabel` nodes with a prohibited role', () => { + const params = checkSetup( '
', { elementsAllowedAriaLabel: ['div'] } ); assert.isTrue(checkEvaluate.apply(checkContext, params)); }); - it('should allow elements that have an implicit role in chromium', function () { - var params = checkSetup(''); + it('should allow elements that have an implicit role in chromium', () => { + const params = checkSetup( + '' + ); assert.isFalse(checkEvaluate.apply(checkContext, params)); }); + + describe('widget ancestor', () => { + it('should allow aria-label', () => { + const params = checkSetup(` + + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + }); + + it('should allow aria-labelledby', () => { + const params = checkSetup(` +
hello world
+ + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + }); + + it('should skip "role=none" roles in between ancestor', () => { + const params = checkSetup(` + + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + }); + + it('should skip "role=presentation" roles in between ancestor', () => { + const params = checkSetup(` + +

+ +

+
+ `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + }); + + it('should not allow aria-label on descendant of non-widget', () => { + const params = checkSetup(` +
+ + + +
+ `); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('should not allow aria-labelledby on descendant of non-widget', () => { + const params = checkSetup(` +
hello world
+
+ + + +
+ `); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('should use closet non-presentational ancestor', () => { + const params = checkSetup(` + + `); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('should use closet chromium role', () => { + const params = checkSetup(` + + `); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + }); }); diff --git a/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.html b/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.html index ec354260a3..e2f673a17b 100644 --- a/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.html +++ b/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.html @@ -3,6 +3,8 @@
Foo
+
+
@@ -35,6 +37,8 @@
+
+
Foo
Foo
diff --git a/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.json b/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.json index f6f9e7717b..6231640b4c 100644 --- a/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.json +++ b/test/integration/rules/aria-prohibited-attr/aria-prohibited-attr.json @@ -1,7 +1,15 @@ { "description": "aria-prohibited-attr tests", "rule": "aria-prohibited-attr", - "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"]], + "passes": [ + ["#pass1"], + ["#pass2"], + ["#pass3"], + ["#pass4"], + ["#pass5"], + ["#pass6"], + ["#pass7"] + ], "incomplete": [["#incomplete1"], ["#incomplete2"], ["#incomplete3"]], "violations": [ ["#fail1"], @@ -32,6 +40,8 @@ ["#fail26"], ["#fail27"], ["#fail28"], - ["#fail29"] + ["#fail29"], + ["#fail30"], + ["#fail31"] ] }