Skip to content

Commit

Permalink
fix(aria-prohibited-attr): allow aria-label/ledby on decendants of wi…
Browse files Browse the repository at this point in the history
…dget (dequelabs#4541)

Closes: dequelabs#2953

---------

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
  • Loading branch information
straker and WilcoFiers authored Jul 22, 2024
1 parent f019068 commit 07c5d91
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 36 deletions.
27 changes: 24 additions & 3 deletions lib/checks/aria/aria-prohibited-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -36,6 +37,7 @@ export default function ariaProhibitedAttrEvaluate(
const role = getRole(virtualNode, { chromium: true });

const prohibitedList = listProhibitedAttrs(
virtualNode,
role,
nodeName,
elementsAllowedAriaLabel
Expand Down Expand Up @@ -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);
}
);
158 changes: 127 additions & 31 deletions test/checks/aria/aria-prohibited-attr.js
Original file line number Diff line number Diff line change
@@ -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(
'<div id="target" role="code" aria-hidden="false" aria-label="foo"></div>'
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
Expand All @@ -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(
'<div id="target" role="code" aria-hidden="false" aria-label="foo">Contents</div>'
);
assert.isUndefined(checkEvaluate.apply(checkContext, params));
Expand All @@ -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(
'<div id="target" role="code" aria-hidden="false" aria-label="foo" aria-labelledby="foo"></div>'
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
Expand All @@ -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('<div id="target" aria-label="foo">Contents</div>');
it('should return undefined if element has no role and has text content (singular)', () => {
const params = checkSetup(
'<div id="target" aria-label="foo">Contents</div>'
);
assert.isUndefined(checkEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, {
nodeName: 'div',
Expand All @@ -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(
'<div id="target" aria-label="foo" aria-labelledby="foo">Contents</div>'
);
assert.isUndefined(checkEvaluate.apply(checkContext, params));
Expand All @@ -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('<div id="target" aria-label="foo"></div>');
it('should return true if element has no role and no text content (singular)', () => {
const params = checkSetup('<div id="target" aria-label="foo"></div>');
assert.isTrue(checkEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, {
nodeName: 'div',
Expand All @@ -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(
'<div id="target" aria-label="foo" aria-labelledby="foo"></div>'
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
Expand All @@ -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(
'<div id="target" role="button" aria-label="foo" aria-labelledby="foo">Contents</div>'
);
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(
'<div id="target" role="code" aria-selected="true">Contents</div>'
);
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(
'<div id="target" role="code" aria-label=" " aria-labelledby=" ">Contents</div>'
);
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(
'<div id="target" aria-label="hello world"></div>',
{ 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(
'<div id="target" role="code" aria-label="hello world"></div>',
{ elementsAllowedAriaLabel: ['div'] }
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
});

it('should allow elements that have an implicit role in chromium', function () {
var params = checkSetup('<svg id="target" aria-label="hello world"></svg>');
it('should allow elements that have an implicit role in chromium', () => {
const params = checkSetup(
'<svg id="target" aria-label="hello world"></svg>'
);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

describe('widget ancestor', () => {
it('should allow aria-label', () => {
const params = checkSetup(`
<button>
<span>
<span id="target" aria-label="hello world"></span>
</span>
</button>
`);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('should allow aria-labelledby', () => {
const params = checkSetup(`
<div id="foo">hello world</div>
<button>
<span>
<span id="target" aria-labelledby="foo"></span>
</span>
</button>
`);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('should skip "role=none" roles in between ancestor', () => {
const params = checkSetup(`
<button>
<h1 role="none">
<span id="target" aria-label="hello world"></span>
</h1>
</button>
`);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('should skip "role=presentation" roles in between ancestor', () => {
const params = checkSetup(`
<a href="#">
<h1 role="presentation">
<span id="target" aria-label="hello world"></span>
</h1>
</a>
`);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('should not allow aria-label on descendant of non-widget', () => {
const params = checkSetup(`
<div role="grid">
<span>
<span id="target" aria-label="foo"></span>
</span>
</div>
`);
assert.isTrue(checkEvaluate.apply(checkContext, params));
});

it('should not allow aria-labelledby on descendant of non-widget', () => {
const params = checkSetup(`
<div id="foo">hello world</div>
<div role="grid">
<span>
<span id="target" aria-labelledby="foo"></span>
</span>
</div>
`);
assert.isTrue(checkEvaluate.apply(checkContext, params));
});

it('should use closet non-presentational ancestor', () => {
const params = checkSetup(`
<button>
<span role="grid">
<span id="target" aria-label="foo"></span>
</span>
</button>
`);
assert.isTrue(checkEvaluate.apply(checkContext, params));
});

it('should use closet chromium role', () => {
const params = checkSetup(`
<button>
<label>
<span id="target" aria-label="foo"></span>
</label>
</button>
`);
assert.isTrue(checkEvaluate.apply(checkContext, params));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<div id="pass3" aria-labelledby=" ">Foo</div>
<div role="alert" aria-selected="true" id="pass4"></div>
<div role="row" aria-colcount="value" id="pass5"></div>
<div role="button"><span id="pass6" aria-label="value"></span></div>
<div role="button"><span id="pass7" aria-labelledby="value"></span></div>

<div role="caption" aria-label="value" id="fail1"></div>
<div role="caption" aria-labelledby="value" id="fail2"></div>
Expand Down Expand Up @@ -35,6 +37,8 @@
<div role="mark" aria-labelledby="value" id="fail27"></div>
<div role="suggestion" aria-label="value" id="fail28"></div>
<div role="suggestion" aria-labelledby="value" id="fail29"></div>
<div role="grid"><span id="fail30" aria-label="value"></span></div>
<div role="grid"><span id="fail31" aria-labelledby="value"></span></div>

<div id="incomplete1" aria-label="foo">Foo</div>
<div id="incomplete2" aria-labelledby="missing">Foo</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down Expand Up @@ -32,6 +40,8 @@
["#fail26"],
["#fail27"],
["#fail28"],
["#fail29"]
["#fail29"],
["#fail30"],
["#fail31"]
]
}

0 comments on commit 07c5d91

Please sign in to comment.