Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/check-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ All checks allow these global options:
</td>
<td align="left">List of element names that without a role, are allowed an `aria-label` and `aria-labelledby` attribute</td>
</tr>
<tr>
<td>
<code>elementsProhibitedAriaLabel</code>
</td>
<td align="left">
<pre lang=js><code>[
"body",
"label"
]</code></pre>
</td>
<td align="left">List of element names that are prohibited from having an `aria-label` or `aria-labelledby` attribute, regardless of the role they expose</td>
</tr>
</tbody>
</table>

Expand Down
30 changes: 22 additions & 8 deletions lib/checks/aria/aria-prohibited-attr-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,30 @@ export default function ariaProhibitedAttrEvaluate(
virtualNode
) {
const elementsAllowedAriaLabel = options?.elementsAllowedAriaLabel || [];
const elementsProhibitedAriaLabel =
options?.elementsProhibitedAriaLabel || [];
const { nodeName } = virtualNode.props;
const role = getRole(virtualNode, {
chromium: true,
// this check allows fallback roles. For example, `<div role="foo img" aria-label="...">` is legal.
fallback: true
});

const prohibitedList = listProhibitedAttrs(
virtualNode,
role,
nodeName,
elementsAllowedAriaLabel
);
// Some HTML elements prohibit an author-provided accessible name regardless
// of the (implicit) role browsers expose for them, e.g. `body` and `label`.
// See ARIA in HTML (https://www.w3.org/TR/html-aria/), "naming prohibited".
const isNameProhibitedElement =
elementsProhibitedAriaLabel.includes(nodeName) &&
!elementsAllowedAriaLabel.includes(nodeName);

const prohibitedList = isNameProhibitedElement
? ['aria-label', 'aria-labelledby']
: listProhibitedAttrs(
virtualNode,
role,
nodeName,
elementsAllowedAriaLabel
);
const prohibited = prohibitedList.filter(attrName => {
if (!virtualNode.attrNames.includes(attrName)) {
return false;
Expand All @@ -57,9 +68,12 @@ export default function ariaProhibitedAttrEvaluate(
return false;
}

let messageKey = role !== null ? 'hasRole' : 'noRole';
// Element-level prohibitions report as "no role"; the prohibition is tied to
// the element, not the implicit role browsers expose (e.g. body -> document).
const effectiveRole = isNameProhibitedElement ? null : role;
let messageKey = effectiveRole !== null ? 'hasRole' : 'noRole';
messageKey += prohibited.length > 1 ? 'Plural' : 'Singular';
this.data({ role, nodeName, messageKey, prohibited });
this.data({ role: effectiveRole, nodeName, messageKey, prohibited });

// `subtreeDescendant` to override namedFromContents
const textContent = subtreeText(virtualNode, { subtreeDescendant: true });
Expand Down
3 changes: 2 additions & 1 deletion lib/checks/aria/aria-prohibited-attr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"id": "aria-prohibited-attr",
"evaluate": "aria-prohibited-attr-evaluate",
"options": {
"elementsAllowedAriaLabel": ["applet", "input", "section", "aside"]
"elementsAllowedAriaLabel": ["applet", "input", "section", "aside"],
"elementsProhibitedAriaLabel": ["body", "label"]
},
"metadata": {
"impact": "serious",
Expand Down
74 changes: 74 additions & 0 deletions test/checks/aria/aria-prohibited-attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,78 @@ describe('aria-prohibited-attr', () => {
assert.isFalse(checkEvaluate.apply(checkContext, params));
});
});

describe('elementsProhibitedAriaLabel (body and label)', () => {
it('should return true for aria-label on a label', () => {
const params = checkSetup(
'<label id="target" aria-label="foo"></label>',
{
elementsProhibitedAriaLabel: ['body', 'label']
}
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, {
nodeName: 'label',
role: null,
messageKey: 'noRoleSingular',
prohibited: ['aria-label']
});
});

it('should return true for aria-labelledby on a label', () => {
const params = checkSetup(
'<label id="target" aria-labelledby="foo"></label>',
{ elementsProhibitedAriaLabel: ['body', 'label'] }
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, {
nodeName: 'label',
role: null,
messageKey: 'noRoleSingular',
prohibited: ['aria-labelledby']
});
});

it('should return true for multiple prohibited attributes on a label', () => {
const params = checkSetup(
'<label id="target" aria-label="foo" aria-labelledby="foo"></label>',
{ elementsProhibitedAriaLabel: ['body', 'label'] }
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, {
nodeName: 'label',
role: null,
messageKey: 'noRolePlural',
prohibited: ['aria-label', 'aria-labelledby']
});
});

it('should return undefined for a label with a prohibited attribute and text content', () => {
const params = checkSetup(
'<label id="target" aria-label="foo">Text</label>',
{ elementsProhibitedAriaLabel: ['body', 'label'] }
);
assert.isUndefined(checkEvaluate.apply(checkContext, params));
});

it('should allow a label opted out via elementsAllowedAriaLabel', () => {
const params = checkSetup(
'<label id="target" aria-label="foo"></label>',
{
elementsProhibitedAriaLabel: ['body', 'label'],
elementsAllowedAriaLabel: ['label']
}
);
assert.isFalse(checkEvaluate.apply(checkContext, params));
});

it('should return true for aria-label on a label in shadow DOM', () => {
const params = axe.testUtils.shadowCheckSetup(
'<div id="shadow"></div>',
'<label id="target" aria-label="foo"></label>',
{ elementsProhibitedAriaLabel: ['body', 'label'] }
);
assert.isTrue(checkEvaluate.apply(checkContext, params));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@
></testutils-element>
<div role="code" aria-actions="value" id="fail35"></div>
<div role="mark" aria-actions="value" id="fail36"></div>
<label id="fail37" aria-label="value"></label>
<label id="fail38" aria-labelledby="value"></label>

<div id="incomplete1" aria-label="foo">Foo</div>
<div id="incomplete2" aria-labelledby="missing">Foo</div>
<div id="incomplete3" aria-label="foo" role="code">Foo</div>
<label id="incomplete4" aria-label="foo">Foo</label>
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
["#pass12"],
["#pass13"]
],
"incomplete": [["#incomplete1"], ["#incomplete2"], ["#incomplete3"]],
"incomplete": [
["#incomplete1"],
["#incomplete2"],
["#incomplete3"],
["#incomplete4"]
],
"violations": [
["#fail1"],
["#fail2"],
Expand Down Expand Up @@ -53,6 +58,8 @@
["#fail33"],
["#fail34"],
["#fail35"],
["#fail36"]
["#fail36"],
["#fail37"],
["#fail38"]
]
}
48 changes: 48 additions & 0 deletions test/integration/virtual-rules/aria-prohibited-attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,52 @@ describe('aria-prohibited-attr virtual-rule', () => {
assert.lengthOf(results.violations, 1);
assert.lengthOf(results.incomplete, 0);
});

it('should fail for aria-label on a body element', () => {
const vNode = new axe.SerialVirtualNode({
nodeName: 'body',
attributes: {
'aria-label': 'foo'
}
});
vNode.children = [];

const results = axe.runVirtualRule('aria-prohibited-attr', vNode);

assert.lengthOf(results.passes, 0);
assert.lengthOf(results.violations, 1);
assert.lengthOf(results.incomplete, 0);
});

it('should fail for aria-label on a label element', () => {
const vNode = new axe.SerialVirtualNode({
nodeName: 'label',
attributes: {
'aria-label': 'foo'
}
});
vNode.children = [];

const results = axe.runVirtualRule('aria-prohibited-attr', vNode);

assert.lengthOf(results.passes, 0);
assert.lengthOf(results.violations, 1);
assert.lengthOf(results.incomplete, 0);
});

it('should fail for aria-labelledby on a label element', () => {
const vNode = new axe.SerialVirtualNode({
nodeName: 'label',
attributes: {
'aria-labelledby': 'foo'
}
});
vNode.children = [];

const results = axe.runVirtualRule('aria-prohibited-attr', vNode);

assert.lengthOf(results.passes, 0);
assert.lengthOf(results.violations, 1);
assert.lengthOf(results.incomplete, 0);
});
});
Loading