Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(release): v4.7.1 #4022

Merged
merged 10 commits into from
May 15, 2023
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

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.

### [4.7.1](https://github.com/dequelabs/axe-core/compare/v4.7.0...v4.7.1) (2023-05-15)

### Bug Fixes

- **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))
- **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))
- **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))
- **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))
- **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))

## [4.7.0](https://github.com/dequelabs/axe-core/compare/v4.6.3...v4.7.0) (2023-04-17)

### Features
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ Axe is an accessibility testing engine for websites and other HTML-based user in

## The Accessibility Rules

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.
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).

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.

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/).

The complete list of rules, grouped WCAG level and best practice, can found in [doc/rule-descriptions.md](./doc/rule-descriptions.md).

## Getting started

First download the package:
Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "axe-core",
"version": "4.7.0",
"version": "4.7.1",
"deprecated": true,
"contributors": [
{
Expand Down
6 changes: 5 additions & 1 deletion doc/check-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ All checks allow these global options:

### aria-allowed-attr

Previously supported properties `validTreeRowAttrs` is no longer available. `invalidTableRowAttrs` from [aria-conditional-attr](#aria-conditional-attr) instead.

### aria-conditional-attr

<table>
<thead>
<tr>
Expand All @@ -218,7 +222,7 @@ All checks allow these global options:
<tbody>
<tr>
<td>
<code>validTreeRowAttrs</code>
<code>invalidTableRowAttrs</code>
</td>
<td align="left">
<pre lang=js><code>[
Expand Down
62 changes: 15 additions & 47 deletions lib/checks/aria/aria-allowed-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { uniqueArray, closest, isHtmlElement } from '../../core/utils';
import { uniqueArray, isHtmlElement } from '../../core/utils';
import { getRole, allowedAttr, validateAttr } from '../../commons/aria';
import { isFocusable } from '../../commons/dom';
import cache from '../../core/base/cache';

/**
* Check if each ARIA attribute on an element is allowed for its semantic role.
Expand Down Expand Up @@ -30,62 +29,31 @@ import cache from '../../core/base/cache';
export default 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));
}

const tableMap = cache.get('aria-allowed-attr-table', () => new WeakMap());

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) && preChecks[attrName]?.()) {
invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
} else if (validateAttr(attrName) && !allowed.includes(attrName)) {
invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
}
// Unknown ARIA attributes are tested in aria-valid-attr
for (const attrName of virtualNode.attrNames) {
if (validateAttr(attrName) && !allowed.includes(attrName)) {
invalid.push(attrName);
}
}

if (invalid.length) {
this.data(invalid);
if (!invalid.length) {
return true;
}

if (!isHtmlElement(virtualNode) && !role && !isFocusable(virtualNode)) {
return undefined;
}
this.data(
invalid.map(attrName => attrName + '="' + virtualNode.attr(attrName) + '"')
);

return false;
if (!role && !isHtmlElement(virtualNode) && !isFocusable(virtualNode)) {
return undefined;
}

return true;
return false;
}
20 changes: 20 additions & 0 deletions lib/checks/aria/aria-conditional-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import getRole from '../../commons/aria/get-role';
import ariaConditionalCheckboxAttr from './aria-conditional-checkbox-attr-evaluate';
import ariaConditionalRowAttr from './aria-conditional-row-attr-evaluate';

const conditionalRoleMap = {
row: ariaConditionalRowAttr,
checkbox: ariaConditionalCheckboxAttr
};

export default function ariaConditionalAttrEvaluate(
node,
options,
virtualNode
) {
const role = getRole(virtualNode);
if (!conditionalRoleMap[role]) {
return true;
}
return conditionalRoleMap[role].call(this, node, options, virtualNode);
}
23 changes: 23 additions & 0 deletions lib/checks/aria/aria-conditional-attr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"id": "aria-conditional-attr",
"evaluate": "aria-conditional-attr-evaluate",
"options": {
"invalidTableRowAttrs": [
"aria-posinset",
"aria-setsize",
"aria-expanded",
"aria-level"
]
},
"metadata": {
"impact": "serious",
"messages": {
"pass": "ARIA attribute is allowed",
"fail": {
"checkbox": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real checkbox state",
"rowSingular": "This attribute is supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}",
"rowPlural": "These attributes are supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}"
}
}
}
}
39 changes: 39 additions & 0 deletions lib/checks/aria/aria-conditional-checkbox-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export default function ariaConditionalCheckboxAttr(
node,
options,
virtualNode
) {
const { nodeName, type } = virtualNode.props;
const ariaChecked = normalizeAriaChecked(virtualNode.attr('aria-checked'));
if (nodeName !== 'input' || type !== 'checkbox' || !ariaChecked) {
return true;
}

const checkState = getCheckState(virtualNode);
if (ariaChecked === checkState) {
return true;
}
this.data({
messageKey: 'checkbox',
checkState
});
return false;
}

function getCheckState(vNode) {
if (vNode.props.indeterminate) {
return 'mixed';
}
return vNode.props.checked ? 'true' : 'false';
}

function normalizeAriaChecked(ariaCheckedVal) {
if (!ariaCheckedVal) {
return '';
}
ariaCheckedVal = ariaCheckedVal.toLowerCase();
if (['mixed', 'true'].includes(ariaCheckedVal)) {
return ariaCheckedVal;
}
return 'false';
}
36 changes: 36 additions & 0 deletions lib/checks/aria/aria-conditional-row-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import getRole from '../../commons/aria/get-role';
import { closest } from '../../core/utils';

export default function ariaConditionalRowAttr(
node,
{ invalidTableRowAttrs } = {},
virtualNode
) {
const invalidAttrs =
invalidTableRowAttrs?.filter?.(invalidAttr => {
return virtualNode.hasAttr(invalidAttr);
}) ?? [];
if (invalidAttrs.length === 0) {
return true;
}

const owner = getRowOwner(virtualNode);
const ownerRole = owner && getRole(owner);
if (!ownerRole || ownerRole === 'treegrid') {
return true;
}

const messageKey = `row${invalidAttrs.length > 1 ? 'Plural' : 'Singular'}`;
this.data({ messageKey, invalidAttrs, ownerRole });
return false;
}

function getRowOwner(virtualNode) {
// check if the parent exists otherwise a TypeError will occur (virtual-nodes specifically)
if (!virtualNode.parent) {
return;
}
const rowOwnerQuery =
'table:not([role]), [role~="treegrid"], [role~="table"], [role~="grid"]';
return closest(virtualNode, rowOwnerQuery);
}
30 changes: 19 additions & 11 deletions lib/checks/aria/aria-required-children-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import { getGlobalAriaAttrs } from '../../commons/standards';
import {
hasContentVirtual,
idrefs,
isFocusable,
isVisibleToScreenReaders
} from '../../commons/dom';
Expand Down Expand Up @@ -35,7 +34,7 @@ export default function ariaRequiredChildrenEvaluate(
return true;
}

const ownedRoles = getOwnedRoles(virtualNode, required);
const { ownedRoles, ownedElements } = getOwnedRoles(virtualNode, required);
const unallowed = ownedRoles.filter(({ role }) => !required.includes(role));

if (unallowed.length) {
Expand Down Expand Up @@ -65,12 +64,7 @@ export default function ariaRequiredChildrenEvaluate(
this.data(missing);

// Only review empty nodes when a node is both empty and does not have an aria-owns relationship
if (
reviewEmpty.includes(role) &&
!hasContentVirtual(virtualNode, false, true) &&
!ownedRoles.length &&
(!virtualNode.hasAttr('aria-owns') || !idrefs(node, 'aria-owns').length)
) {
if (reviewEmpty.includes(role) && !ownedElements.some(isContent)) {
return undefined;
}

Expand All @@ -82,7 +76,10 @@ export default function ariaRequiredChildrenEvaluate(
*/
function getOwnedRoles(virtualNode, required) {
const ownedRoles = [];
const ownedElements = getOwnedVirtual(virtualNode);
const ownedElements = getOwnedVirtual(virtualNode).filter(vNode => {
return vNode.props.nodeType !== 1 || isVisibleToScreenReaders(vNode);
});

for (let i = 0; i < ownedElements.length; i++) {
const ownedElement = ownedElements[i];
if (ownedElement.props.nodeType !== 1) {
Expand All @@ -100,7 +97,6 @@ function getOwnedRoles(virtualNode, required) {
// this means intermediate roles between a required parent and
// child will fail the check
if (
!isVisibleToScreenReaders(ownedElement) ||
(!role && !hasGlobalAriaOrFocusable) ||
(['group', 'rowgroup'].includes(role) &&
required.some(requiredRole => requiredRole === role))
Expand All @@ -115,7 +111,7 @@ function getOwnedRoles(virtualNode, required) {
}
}

return ownedRoles;
return { ownedRoles, ownedElements };
}

/**
Expand Down Expand Up @@ -171,3 +167,15 @@ function getUnallowedSelector(vNode, attr) {

return nodeName;
}

/**
* Check if the node has content, or is itself content
* @param {VirtualNode} vNode
* @returns {Boolean}
*/
function isContent(vNode) {
if (vNode.props.nodeType === 3) {
return vNode.props.nodeValue.trim().length > 0;
}
return hasContentVirtual(vNode, false, true);
}
31 changes: 24 additions & 7 deletions lib/checks/color/link-in-text-block-style-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@ const blockLike = [
'grid',
'inline-block'
];
function isBlock(elm) {
var display = window.getComputedStyle(elm).getPropertyValue('display');
return blockLike.indexOf(display) !== -1 || display.substr(0, 6) === 'table-';
}

function linkInTextBlockStyleEvaluate(node) {
export default function linkInTextBlockStyleEvaluate(node) {
if (isBlock(node)) {
return false;
}
Expand All @@ -30,7 +26,28 @@ function linkInTextBlockStyleEvaluate(node) {

this.relatedNodes([parentBlock]);

return elementIsDistinct(node, parentBlock);
if (elementIsDistinct(node, parentBlock)) {
return true;
}
if (hasPseudoContent(node)) {
this.data({ messageKey: 'pseudoContent' });
return undefined;
}
return false;
}

function isBlock(elm) {
var display = window.getComputedStyle(elm).getPropertyValue('display');
return blockLike.indexOf(display) !== -1 || display.substr(0, 6) === 'table-';
}

export default linkInTextBlockStyleEvaluate;
function hasPseudoContent(node) {
for (const pseudo of ['before', 'after']) {
const style = window.getComputedStyle(node, `:${pseudo}`);
const content = style.getPropertyValue('content');
if (content !== 'none') {
return true;
}
}
return false;
}
Loading