Skip to content

Commit c71e3dd

Browse files
authored
chore(release): v4.11.3 (#5070)
### Bug Fixes - **aria-allowed-attr:** restrict br and wbr elements to aria-hidden only ([#4974](#4974)) ([1d80163](1d80163)) - **target-size:** ignore position: fixed elements that are offscreen when page is scrolled ([#5066](#5066)) ([5906273](5906273)), closes [#5065](#5065)
2 parents 41093da + 3ab66ba commit c71e3dd

32 files changed

Lines changed: 914 additions & 366 deletions

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
run: |
6565
npm run prepare
6666
npm run build
67-
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
67+
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
6868
with:
6969
name: axe-core
7070
path: axe.js
@@ -136,7 +136,7 @@ jobs:
136136
- *install-deps
137137
- &restore-axe-build
138138
name: Restore axe build
139-
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
139+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
140140
with:
141141
name: axe-core
142142
- name: Run ACT Tests

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
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.11.3](https://github.com/dequelabs/axe-core/compare/v4.11.2...v4.11.3) (2026-04-13)
6+
7+
### Bug Fixes
8+
9+
- **aria-allowed-attr:** restrict br and wbr elements to aria-hidden only ([#4974](https://github.com/dequelabs/axe-core/issues/4974)) ([1d80163](https://github.com/dequelabs/axe-core/commit/1d801636f058f2abd885c488baff954872b13846))
10+
- **target-size:** ignore position: fixed elements that are offscreen when page is scrolled ([#5066](https://github.com/dequelabs/axe-core/issues/5066)) ([5906273](https://github.com/dequelabs/axe-core/commit/5906273841cbd7ac9e08af730dffc244cf42b39b)), closes [#5065](https://github.com/dequelabs/axe-core/issues/5065)
11+
512
### [4.11.2](https://github.com/dequelabs/axe-core/compare/v4.11.1...v4.11.2) (2026-03-30)
613

714
### Bug Fixes

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.11.2",
3+
"version": "4.11.3",
44
"deprecated": true,
55
"contributors": [
66
{

doc/standards-object.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ The [`htmlElms`](../lib/standards/html-elms.js) object defines valid HTML elemen
9595

9696
### Used by Rules
9797

98-
- `aria-allowed-attr` - Checks if the attribute can be used on the element from the `noAriaAttrs` property.
98+
- `aria-allowed-attr` - Checks if the attribute can be used on the element from the `noAriaAttrs` and `allowedAriaAttrs` properties.
9999
- `aria-allowed-role` - Checks if the role can be used on the HTML element from the `allowedRoles` property.
100100
- `aria-required-attrs` - Checks if any required attrs are defined implicitly on the element from the `implicitAttrs` property.
101101

@@ -110,6 +110,7 @@ The [`htmlElms`](../lib/standards/html-elms.js) object defines valid HTML elemen
110110
- `interactive`
111111
- `allowedRoles` - boolean or array(required). If element is allowed to use ARIA roles, a value of `true` means any role while a list of roles means only those are allowed. A value of `false` means no roles are allowed.
112112
- `noAriaAttrs` - boolean(optional. Defaults `true`). If the element is allowed to use global ARIA attributes and any allowed for the elements role.
113+
- `allowedAriaAttrs` - array(optional). If specified, restricts which ARIA attributes may be used with this element (when no explicit role is set). Used by the `aria-allowed-attr` rule.
113114
- `shadowRoot` - boolean(optional. Default `false`). If the element is allowed to have a shadow root.
114115
- `implicitAttrs` - object(optional. Default `{}`). Any implicit ARIA attributes for the element and their default value.
115116
- `namingMethods` - array(optional. Default `[]`). The [native text method](../lib/commons/text/native-text-methods.js) used to calculate the accessible name of the element.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getExplicitRole } from '../../commons/aria';
2+
import { getElementSpec, getGlobalAriaAttrs } from '../../commons/standards';
3+
4+
export default function ariaAllowedAttrElmEvaluate(node, options, virtualNode) {
5+
const elmSpec = getElementSpec(virtualNode);
6+
7+
// If no allowedAriaAttrs restriction, this check doesn't apply
8+
if (!elmSpec.allowedAriaAttrs) {
9+
return true;
10+
}
11+
12+
// If element has an explicit role, defer to the role-based check
13+
const explicitRole = getExplicitRole(virtualNode);
14+
if (explicitRole) {
15+
return true;
16+
}
17+
18+
const { allowedAriaAttrs } = elmSpec;
19+
const globalAriaAttrs = getGlobalAriaAttrs();
20+
const invalid = [];
21+
22+
for (const attrName of virtualNode.attrNames) {
23+
if (
24+
globalAriaAttrs.includes(attrName) &&
25+
!allowedAriaAttrs.includes(attrName)
26+
) {
27+
invalid.push(attrName);
28+
}
29+
}
30+
31+
if (!invalid.length) {
32+
return true;
33+
}
34+
35+
const messageKey = invalid.length > 1 ? 'plural' : 'singular';
36+
this.data({
37+
messageKey,
38+
nodeName: virtualNode.props.nodeName,
39+
values: invalid
40+
.map(attrName => attrName + '="' + virtualNode.attr(attrName) + '"')
41+
.join(', ')
42+
});
43+
44+
return false;
45+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"id": "aria-allowed-attr-elm",
3+
"evaluate": "aria-allowed-attr-elm-evaluate",
4+
"metadata": {
5+
"messages": {
6+
"pass": "ARIA attributes are allowed for this element",
7+
"fail": {
8+
"singular": "ARIA attribute is not allowed on ${data.nodeName} elements: ${data.values}",
9+
"plural": "ARIA attributes are not allowed on ${data.nodeName} elements: ${data.values}"
10+
}
11+
}
12+
}
13+
}
Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import getNodeGrid from './get-node-grid';
2-
import { memoize } from '../../core/utils';
2+
import isFixedPosition from './is-fixed-position';
33

44
export default function findNearbyElms(vNode, margin = 0) {
55
const grid = getNodeGrid(vNode);
66
if (!grid?.cells?.length) {
77
return []; // Elements not in the grid don't have ._grid
88
}
99
const rect = vNode.boundingClientRect;
10-
const selfIsFixed = hasFixedPosition(vNode);
10+
const selfIsFixed = isFixedPosition(vNode);
1111
const gridPosition = grid.getGridPositionOfRect(rect, margin);
1212

1313
const neighbors = [];
@@ -17,7 +17,7 @@ export default function findNearbyElms(vNode, margin = 0) {
1717
vNeighbor &&
1818
vNeighbor !== vNode &&
1919
!neighbors.includes(vNeighbor) &&
20-
selfIsFixed === hasFixedPosition(vNeighbor)
20+
selfIsFixed === isFixedPosition(vNeighbor)
2121
) {
2222
neighbors.push(vNeighbor);
2323
}
@@ -26,13 +26,3 @@ export default function findNearbyElms(vNode, margin = 0) {
2626

2727
return neighbors;
2828
}
29-
30-
const hasFixedPosition = memoize(vNode => {
31-
if (!vNode) {
32-
return false;
33-
}
34-
if (vNode.getComputedStylePropertyValue('position') === 'fixed') {
35-
return true;
36-
}
37-
return hasFixedPosition(vNode.parent);
38-
});

lib/commons/dom/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export { default as hasLangText } from './has-lang-text';
2828
export { default as idrefs } from './idrefs';
2929
export { default as insertedIntoFocusOrder } from './inserted-into-focus-order';
3030
export { default as isCurrentPageLink } from './is-current-page-link';
31+
export { default as isFixedPosition } from './is-fixed-position';
3132
export { default as isFocusable } from './is-focusable';
3233
export { default as isHiddenWithCSS } from './is-hidden-with-css';
3334
export { default as isHiddenForEveryone } from './is-hidden-for-everyone';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import memoize from '../../core/utils/memoize';
2+
import { nodeLookup } from '../../core/utils';
3+
4+
/**
5+
* Determines if an element is inside a position:fixed subtree, even if the element itself is positioned differently.
6+
* @param {VirtualNode|Element} node
7+
* @param {Boolean} [options.skipAncestors] If the ancestor tree should not be used
8+
* @return {Boolean} The element's position state
9+
*/
10+
export default function isFixedPosition(node, { skipAncestors } = {}) {
11+
const { vNode } = nodeLookup(node);
12+
13+
// detached element
14+
if (!vNode) {
15+
return false;
16+
}
17+
18+
if (skipAncestors) {
19+
return isFixedSelf(vNode);
20+
}
21+
22+
return isFixedAncestors(vNode);
23+
}
24+
25+
/**
26+
* Check the element for position:fixed
27+
*/
28+
const isFixedSelf = memoize(function isFixedSelfMemoized(vNode) {
29+
return vNode.getComputedStylePropertyValue('position') === 'fixed';
30+
});
31+
32+
/**
33+
* Check the element and ancestors for position:fixed
34+
*/
35+
const isFixedAncestors = memoize(function isFixedAncestorsMemoized(vNode) {
36+
if (isFixedSelf(vNode)) {
37+
return true;
38+
}
39+
40+
if (!vNode.parent) {
41+
return false;
42+
}
43+
44+
return isFixedAncestors(vNode.parent);
45+
});

lib/commons/dom/is-offscreen.js

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import getComposedParent from './get-composed-parent';
22
import getElementCoordinates from './get-element-coordinates';
33
import getViewportSize from './get-viewport-size';
44
import { nodeLookup } from '../../core/utils';
5+
import isFixedPosition from './is-fixed-position';
56

67
function noParentScrolled(element, offset) {
78
element = getComposedParent(element);
@@ -37,39 +38,43 @@ function isOffscreen(element, { isAncestor } = {}) {
3738
return undefined;
3839
}
3940

40-
let leftBoundary;
4141
const docElement = document.documentElement;
4242
const styl = window.getComputedStyle(domNode);
4343
const dir = window
4444
.getComputedStyle(document.body || docElement)
4545
.getPropertyValue('direction');
46-
const coords = getElementCoordinates(domNode);
46+
const isFixed = isFixedPosition(domNode);
47+
const coords = isFixed
48+
? domNode.getBoundingClientRect()
49+
: getElementCoordinates(domNode);
50+
51+
// Consider 0 height/ width elements at origin visible
52+
if (coords.top === 0 && coords.bottom === 0) {
53+
return false;
54+
}
55+
if (coords.left === 0 && coords.right === 0) {
56+
return false;
57+
}
4758

48-
// bottom edge beyond
4959
if (
50-
coords.bottom < 0 &&
60+
coords.bottom <= 0 &&
5161
(noParentScrolled(domNode, coords.bottom) || styl.position === 'absolute')
5262
) {
5363
return true;
5464
}
5565

56-
if (coords.left === 0 && coords.right === 0) {
57-
//This is an edge case, an empty (zero-width) element that isn't positioned 'off screen'.
58-
return false;
66+
const viewportSize = getViewportSize(window);
67+
if (isFixed && coords.top >= viewportSize.height) {
68+
return true; // Positioned below the viewport
5969
}
6070

61-
if (dir === 'ltr') {
62-
if (coords.right <= 0) {
63-
return true;
64-
}
65-
} else {
66-
leftBoundary = Math.max(
67-
docElement.scrollWidth,
68-
getViewportSize(window).width
69-
);
70-
if (coords.left >= leftBoundary) {
71-
return true;
72-
}
71+
const rightEdge = Math.max(docElement.scrollWidth, viewportSize.width);
72+
if ((isFixed || dir === 'rtl') && coords.left >= rightEdge) {
73+
return true; // Positioned right of the viewport, preventing right scrolling
74+
}
75+
76+
if ((isFixed || dir === 'ltr') && coords.right <= 0) {
77+
return true; // Positioned left of the viewport, preventing left scrolling
7378
}
7479

7580
return false;

0 commit comments

Comments
 (0)