Skip to content

Commit

Permalink
Merge pull request #4022 from dequelabs/release-2023-05-15
Browse files Browse the repository at this point in the history
chore(release): v4.7.1
  • Loading branch information
WilcoFiers committed May 15, 2023
2 parents c7957d2 + 9842a86 commit c806e57
Show file tree
Hide file tree
Showing 61 changed files with 1,169 additions and 832 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
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
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
@@ -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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
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;
}

0 comments on commit c806e57

Please sign in to comment.