Skip to content

Commit c806e57

Browse files
authored
Merge pull request #4022 from dequelabs/release-2023-05-15
chore(release): v4.7.1
2 parents c7957d2 + 9842a86 commit c806e57

File tree

61 files changed

+1169
-832
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1169
-832
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
### [4.7.1](https://github.com/dequelabs/axe-core/compare/v4.7.0...v4.7.1) (2023-05-15)
6+
7+
### Bug Fixes
8+
9+
- **aria-allowed-attr:** no inconsistent aria-checked on HTML checkboxes ([#3895](https://github.com/dequelabs/axe-core/issues/3895)) ([704043e](https://github.com/dequelabs/axe-core/commit/704043e8a4b9359e871403c3b4fc294b9feee931))
10+
- **aria-allowed-attrs:** add aria-expanded to allowed attrs for menuitemcheckbox and menuitemradio ([#3994](https://github.com/dequelabs/axe-core/issues/3994)) ([0f405c6](https://github.com/dequelabs/axe-core/commit/0f405c6da55570db2d536e2a4a5464865d73e821))
11+
- **aria-required-children:** trigger reviewEmpty with hidden children ([#4012](https://github.com/dequelabs/axe-core/issues/4012)) ([a19b6cb](https://github.com/dequelabs/axe-core/commit/a19b6cb5252deb062f6170ab035d804742e7c1df))
12+
- **color-contrast:** support CSS 4 color spaces ([#4020](https://github.com/dequelabs/axe-core/issues/4020)) ([65621c3](https://github.com/dequelabs/axe-core/commit/65621c339fd42798cb3ce66bac62865e62926e8c))
13+
- **link-in-text-block:** set links with pseudo-content for review ([#4005](https://github.com/dequelabs/axe-core/issues/4005)) ([949f4f8](https://github.com/dequelabs/axe-core/commit/949f4f8dfccd018b88f929bd650dc8920ce4f6f0))
14+
515
## [4.7.0](https://github.com/dequelabs/axe-core/compare/v4.6.3...v4.7.0) (2023-04-17)
616

717
### Features

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ Axe is an accessibility testing engine for websites and other HTML-based user in
1414

1515
## The Accessibility Rules
1616

17-
Axe-core has different types of rules, for WCAG 2.0 and 2.1 on level A and AA, as well as a number of best practices that help you identify common accessibility practices like ensuring every page has an `h1` heading, and to help you avoid "gotchas" in ARIA like where an ARIA attribute you used will get ignored.
17+
Axe-core has different types of rules, for WCAG 2.0, 2.1, 2.2 on level A, AA and AAA as well as a number of best practices that help you identify common accessibility practices like ensuring every page has an `h1` heading, and to help you avoid "gotchas" in ARIA like where an ARIA attribute you used will get ignored. The complete list of rules, grouped WCAG level and best practice, can found in [doc/rule-descriptions.md](./doc/rule-descriptions.md).
1818

1919
With axe-core, you can find **on average 57% of WCAG issues automatically**. Additionally, axe-core will return elements as "incomplete" where axe-core could not be certain, and manual review is needed.
2020

2121
To catch bugs earlier in the development cycle we recommend using the [axe-linter vscode extension](https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter). To improve test coverage even further we recommend the [intelligent guided tests](https://www.youtube.com/watch?v=AtsX0dPCG_4&feature=youtu.be&ab_channel=DequeSystems) in the [axe Extension](https://www.deque.com/axe/browser-extensions/).
2222

23-
The complete list of rules, grouped WCAG level and best practice, can found in [doc/rule-descriptions.md](./doc/rule-descriptions.md).
24-
2523
## Getting started
2624

2725
First download the package:

bower.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "axe-core",
3-
"version": "4.7.0",
3+
"version": "4.7.1",
44
"deprecated": true,
55
"contributors": [
66
{

doc/check-options.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ All checks allow these global options:
207207

208208
### aria-allowed-attr
209209

210+
Previously supported properties `validTreeRowAttrs` is no longer available. `invalidTableRowAttrs` from [aria-conditional-attr](#aria-conditional-attr) instead.
211+
212+
### aria-conditional-attr
213+
210214
<table>
211215
<thead>
212216
<tr>
@@ -218,7 +222,7 @@ All checks allow these global options:
218222
<tbody>
219223
<tr>
220224
<td>
221-
<code>validTreeRowAttrs</code>
225+
<code>invalidTableRowAttrs</code>
222226
</td>
223227
<td align="left">
224228
<pre lang=js><code>[
Lines changed: 15 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { uniqueArray, closest, isHtmlElement } from '../../core/utils';
1+
import { uniqueArray, isHtmlElement } from '../../core/utils';
22
import { getRole, allowedAttr, validateAttr } from '../../commons/aria';
33
import { isFocusable } from '../../commons/dom';
4-
import cache from '../../core/base/cache';
54

65
/**
76
* Check if each ARIA attribute on an element is allowed for its semantic role.
@@ -30,62 +29,31 @@ import cache from '../../core/base/cache';
3029
export default function ariaAllowedAttrEvaluate(node, options, virtualNode) {
3130
const invalid = [];
3231
const role = getRole(virtualNode);
33-
const attrs = virtualNode.attrNames;
3432
let allowed = allowedAttr(role);
33+
3534
// @deprecated: allowed attr options to pass more attrs.
3635
// configure the standards spec instead
3736
if (Array.isArray(options[role])) {
3837
allowed = uniqueArray(options[role].concat(allowed));
3938
}
4039

41-
const tableMap = cache.get('aria-allowed-attr-table', () => new WeakMap());
42-
43-
function validateRowAttrs() {
44-
// check if the parent exists otherwise a TypeError will occur (virtual-nodes specifically)
45-
if (virtualNode.parent && role === 'row') {
46-
const table = closest(
47-
virtualNode,
48-
'table, [role="treegrid"], [role="table"], [role="grid"]'
49-
);
50-
51-
let tableRole = tableMap.get(table);
52-
if (table && !tableRole) {
53-
tableRole = getRole(table);
54-
tableMap.set(table, tableRole);
55-
}
56-
if (['table', 'grid'].includes(tableRole) && role === 'row') {
57-
return true;
58-
}
59-
}
60-
}
61-
// Allows options to be mapped to object e.g. {'aria-level' : validateRowAttrs}
62-
const ariaAttr = Array.isArray(options.validTreeRowAttrs)
63-
? options.validTreeRowAttrs
64-
: [];
65-
const preChecks = {};
66-
ariaAttr.forEach(attr => {
67-
preChecks[attr] = validateRowAttrs;
68-
});
69-
if (allowed) {
70-
for (let i = 0; i < attrs.length; i++) {
71-
const attrName = attrs[i];
72-
if (validateAttr(attrName) && preChecks[attrName]?.()) {
73-
invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
74-
} else if (validateAttr(attrName) && !allowed.includes(attrName)) {
75-
invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
76-
}
40+
// Unknown ARIA attributes are tested in aria-valid-attr
41+
for (const attrName of virtualNode.attrNames) {
42+
if (validateAttr(attrName) && !allowed.includes(attrName)) {
43+
invalid.push(attrName);
7744
}
7845
}
7946

80-
if (invalid.length) {
81-
this.data(invalid);
47+
if (!invalid.length) {
48+
return true;
49+
}
8250

83-
if (!isHtmlElement(virtualNode) && !role && !isFocusable(virtualNode)) {
84-
return undefined;
85-
}
51+
this.data(
52+
invalid.map(attrName => attrName + '="' + virtualNode.attr(attrName) + '"')
53+
);
8654

87-
return false;
55+
if (!role && !isHtmlElement(virtualNode) && !isFocusable(virtualNode)) {
56+
return undefined;
8857
}
89-
90-
return true;
58+
return false;
9159
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import getRole from '../../commons/aria/get-role';
2+
import ariaConditionalCheckboxAttr from './aria-conditional-checkbox-attr-evaluate';
3+
import ariaConditionalRowAttr from './aria-conditional-row-attr-evaluate';
4+
5+
const conditionalRoleMap = {
6+
row: ariaConditionalRowAttr,
7+
checkbox: ariaConditionalCheckboxAttr
8+
};
9+
10+
export default function ariaConditionalAttrEvaluate(
11+
node,
12+
options,
13+
virtualNode
14+
) {
15+
const role = getRole(virtualNode);
16+
if (!conditionalRoleMap[role]) {
17+
return true;
18+
}
19+
return conditionalRoleMap[role].call(this, node, options, virtualNode);
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"id": "aria-conditional-attr",
3+
"evaluate": "aria-conditional-attr-evaluate",
4+
"options": {
5+
"invalidTableRowAttrs": [
6+
"aria-posinset",
7+
"aria-setsize",
8+
"aria-expanded",
9+
"aria-level"
10+
]
11+
},
12+
"metadata": {
13+
"impact": "serious",
14+
"messages": {
15+
"pass": "ARIA attribute is allowed",
16+
"fail": {
17+
"checkbox": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real checkbox state",
18+
"rowSingular": "This attribute is supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}",
19+
"rowPlural": "These attributes are supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}"
20+
}
21+
}
22+
}
23+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export default function ariaConditionalCheckboxAttr(
2+
node,
3+
options,
4+
virtualNode
5+
) {
6+
const { nodeName, type } = virtualNode.props;
7+
const ariaChecked = normalizeAriaChecked(virtualNode.attr('aria-checked'));
8+
if (nodeName !== 'input' || type !== 'checkbox' || !ariaChecked) {
9+
return true;
10+
}
11+
12+
const checkState = getCheckState(virtualNode);
13+
if (ariaChecked === checkState) {
14+
return true;
15+
}
16+
this.data({
17+
messageKey: 'checkbox',
18+
checkState
19+
});
20+
return false;
21+
}
22+
23+
function getCheckState(vNode) {
24+
if (vNode.props.indeterminate) {
25+
return 'mixed';
26+
}
27+
return vNode.props.checked ? 'true' : 'false';
28+
}
29+
30+
function normalizeAriaChecked(ariaCheckedVal) {
31+
if (!ariaCheckedVal) {
32+
return '';
33+
}
34+
ariaCheckedVal = ariaCheckedVal.toLowerCase();
35+
if (['mixed', 'true'].includes(ariaCheckedVal)) {
36+
return ariaCheckedVal;
37+
}
38+
return 'false';
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import getRole from '../../commons/aria/get-role';
2+
import { closest } from '../../core/utils';
3+
4+
export default function ariaConditionalRowAttr(
5+
node,
6+
{ invalidTableRowAttrs } = {},
7+
virtualNode
8+
) {
9+
const invalidAttrs =
10+
invalidTableRowAttrs?.filter?.(invalidAttr => {
11+
return virtualNode.hasAttr(invalidAttr);
12+
}) ?? [];
13+
if (invalidAttrs.length === 0) {
14+
return true;
15+
}
16+
17+
const owner = getRowOwner(virtualNode);
18+
const ownerRole = owner && getRole(owner);
19+
if (!ownerRole || ownerRole === 'treegrid') {
20+
return true;
21+
}
22+
23+
const messageKey = `row${invalidAttrs.length > 1 ? 'Plural' : 'Singular'}`;
24+
this.data({ messageKey, invalidAttrs, ownerRole });
25+
return false;
26+
}
27+
28+
function getRowOwner(virtualNode) {
29+
// check if the parent exists otherwise a TypeError will occur (virtual-nodes specifically)
30+
if (!virtualNode.parent) {
31+
return;
32+
}
33+
const rowOwnerQuery =
34+
'table:not([role]), [role~="treegrid"], [role~="table"], [role~="grid"]';
35+
return closest(virtualNode, rowOwnerQuery);
36+
}

lib/checks/aria/aria-required-children-evaluate.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
import { getGlobalAriaAttrs } from '../../commons/standards';
88
import {
99
hasContentVirtual,
10-
idrefs,
1110
isFocusable,
1211
isVisibleToScreenReaders
1312
} from '../../commons/dom';
@@ -35,7 +34,7 @@ export default function ariaRequiredChildrenEvaluate(
3534
return true;
3635
}
3736

38-
const ownedRoles = getOwnedRoles(virtualNode, required);
37+
const { ownedRoles, ownedElements } = getOwnedRoles(virtualNode, required);
3938
const unallowed = ownedRoles.filter(({ role }) => !required.includes(role));
4039

4140
if (unallowed.length) {
@@ -65,12 +64,7 @@ export default function ariaRequiredChildrenEvaluate(
6564
this.data(missing);
6665

6766
// Only review empty nodes when a node is both empty and does not have an aria-owns relationship
68-
if (
69-
reviewEmpty.includes(role) &&
70-
!hasContentVirtual(virtualNode, false, true) &&
71-
!ownedRoles.length &&
72-
(!virtualNode.hasAttr('aria-owns') || !idrefs(node, 'aria-owns').length)
73-
) {
67+
if (reviewEmpty.includes(role) && !ownedElements.some(isContent)) {
7468
return undefined;
7569
}
7670

@@ -82,7 +76,10 @@ export default function ariaRequiredChildrenEvaluate(
8276
*/
8377
function getOwnedRoles(virtualNode, required) {
8478
const ownedRoles = [];
85-
const ownedElements = getOwnedVirtual(virtualNode);
79+
const ownedElements = getOwnedVirtual(virtualNode).filter(vNode => {
80+
return vNode.props.nodeType !== 1 || isVisibleToScreenReaders(vNode);
81+
});
82+
8683
for (let i = 0; i < ownedElements.length; i++) {
8784
const ownedElement = ownedElements[i];
8885
if (ownedElement.props.nodeType !== 1) {
@@ -100,7 +97,6 @@ function getOwnedRoles(virtualNode, required) {
10097
// this means intermediate roles between a required parent and
10198
// child will fail the check
10299
if (
103-
!isVisibleToScreenReaders(ownedElement) ||
104100
(!role && !hasGlobalAriaOrFocusable) ||
105101
(['group', 'rowgroup'].includes(role) &&
106102
required.some(requiredRole => requiredRole === role))
@@ -115,7 +111,7 @@ function getOwnedRoles(virtualNode, required) {
115111
}
116112
}
117113

118-
return ownedRoles;
114+
return { ownedRoles, ownedElements };
119115
}
120116

121117
/**
@@ -171,3 +167,15 @@ function getUnallowedSelector(vNode, attr) {
171167

172168
return nodeName;
173169
}
170+
171+
/**
172+
* Check if the node has content, or is itself content
173+
* @param {VirtualNode} vNode
174+
* @returns {Boolean}
175+
*/
176+
function isContent(vNode) {
177+
if (vNode.props.nodeType === 3) {
178+
return vNode.props.nodeValue.trim().length > 0;
179+
}
180+
return hasContentVirtual(vNode, false, true);
181+
}

0 commit comments

Comments
 (0)