Skip to content

Commit 273c971

Browse files
authored
feat(dom.focusDisabled,dom.isVisibleForScreenreader): support the inert attribute (#3857)
* feat(dom.focus-disabled,dom.is-visible-for-screenreader): support the inert attribute * typos * integration tests
1 parent 3be2bad commit 273c971

10 files changed

Lines changed: 153 additions & 4 deletions

File tree

lib/commons/dom/focus-disabled.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node';
22
import { getNodeFromTree } from '../../core/utils';
33
import isHiddenForEveryone from './is-hidden-for-everyone';
4+
import isInert from './is-inert';
5+
46
// Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled
57
const allowedDisabledNodeNames = [
68
'button',
@@ -27,8 +29,9 @@ function focusDisabled(el) {
2729
const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el);
2830

2931
if (
30-
isDisabledAttrAllowed(vNode.props.nodeName) &&
31-
vNode.hasAttr('disabled')
32+
(isDisabledAttrAllowed(vNode.props.nodeName) &&
33+
vNode.hasAttr('disabled')) ||
34+
isInert(vNode)
3235
) {
3336
return true;
3437
}

lib/commons/dom/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export { default as isHiddenForEveryone } from './is-hidden-for-everyone';
3131
export { default as isHTML5 } from './is-html5';
3232
export { default as isInTabOrder } from './is-in-tab-order';
3333
export { default as isInTextBlock } from './is-in-text-block';
34+
export { default as isInert } from './is-inert';
3435
export { default as isModalOpen } from './is-modal-open';
3536
export { default as isMultiline } from './is-multiline';
3637
export { default as isNativelyFocusable } from './is-natively-focusable';

lib/commons/dom/is-inert.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import memoize from '../../core/utils/memoize';
2+
3+
/**
4+
* Determines if an element is inside an inert subtree.
5+
* @param {VirtualNode} vNode
6+
* @param {Boolean} [options.skipAncestors] If the ancestor tree should not be used
7+
* @return {Boolean} The element's inert state
8+
*/
9+
export default function isInert(vNode, { skipAncestors } = {}) {
10+
if (skipAncestors) {
11+
return isInertSelf(vNode);
12+
}
13+
14+
return isInertAncestors(vNode);
15+
}
16+
17+
/**
18+
* Check the element for inert
19+
*/
20+
const isInertSelf = memoize(function isInertSelfMemoized(vNode) {
21+
return vNode.hasAttr('inert');
22+
});
23+
24+
/**
25+
* Check the element and ancestors for inert
26+
*/
27+
const isInertAncestors = memoize(function isInertAncestorsMemoized(vNode) {
28+
if (isInertSelf(vNode)) {
29+
return true;
30+
}
31+
32+
if (!vNode.parent) {
33+
return false;
34+
}
35+
36+
return isInertAncestors(vNode.parent);
37+
});

lib/commons/dom/is-visible-for-screenreader.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getNodeFromTree } from '../../core/utils';
33
import memoize from '../../core/utils/memoize';
44
import isHiddenForEveryone from './is-hidden-for-everyone';
55
import { ariaHidden, areaHidden } from './visibility-methods';
6+
import isInert from './is-inert';
67

78
/**
89
* Determine if an element is visible to a screen reader
@@ -21,7 +22,7 @@ export default function isVisibleToScreenReaders(vNode) {
2122
*/
2223
const isVisibleToScreenReadersVirtual = memoize(
2324
function isVisibleToScreenReadersMemoized(vNode, isAncestor) {
24-
if (ariaHidden(vNode)) {
25+
if (ariaHidden(vNode) || isInert(vNode, { skipAncestors: true })) {
2526
return false;
2627
}
2728

test/commons/dom/focus-disabled.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,29 @@ describe('dom.focus-disabled', () => {
5151
assert.isTrue(focusDisabled(vNode));
5252
});
5353

54+
it('returns true for element with inert', () => {
55+
const vNode = queryFixture('<button id="target" inert></button>');
56+
57+
assert.isTrue(focusDisabled(vNode));
58+
});
59+
60+
it('returns true for ancestor with inert', () => {
61+
const vNode = queryFixture(
62+
'<div inert><div><button id="target"></button></div></div>'
63+
);
64+
65+
assert.isTrue(focusDisabled(vNode));
66+
});
67+
68+
it('returns true for ancestor with inert outside shadow tree', () => {
69+
const vNode = queryShadowFixture(
70+
'<div inert><div id="shadow"></div></div>',
71+
'<input id="target"/>'
72+
);
73+
74+
assert.isTrue(focusDisabled(vNode));
75+
});
76+
5477
describe('SerialVirtualNode', () => {
5578
it('returns false if element is hidden for everyone', () => {
5679
const vNode = new axe.SerialVirtualNode({

test/commons/dom/is-inert.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
describe('dom.isInert', () => {
2+
const isInert = axe.commons.dom.isInert;
3+
const { queryFixture } = axe.testUtils;
4+
5+
it('returns true for element with "inert=false`', () => {
6+
const vNode = queryFixture('<div id="target" inert="false"></div>');
7+
8+
assert.isTrue(isInert(vNode));
9+
});
10+
11+
it('returns true for element with "inert`', () => {
12+
const vNode = queryFixture('<div id="target" inert></div>');
13+
14+
assert.isTrue(isInert(vNode));
15+
});
16+
17+
it('returns false for element without inert', () => {
18+
const vNode = queryFixture('<div id="target"></div>');
19+
20+
assert.isFalse(isInert(vNode));
21+
});
22+
23+
it('returns true for ancestor with inert', () => {
24+
const vNode = queryFixture(
25+
'<div inert><div><div id="target"></div></div></div>'
26+
);
27+
28+
assert.isTrue(isInert(vNode));
29+
});
30+
31+
describe('options.skipAncestors', () => {
32+
it('returns false for ancestor with inert', () => {
33+
const vNode = queryFixture(
34+
'<div inert><div><div id="target"></div></div></div>'
35+
);
36+
37+
assert.isFalse(isInert(vNode, { skipAncestors: true }));
38+
});
39+
});
40+
});

test/commons/dom/is-visible-for-screenreader.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ describe('dom.isVisibleToScreenReaders', function () {
6262
assert.isFalse(isVisibleToScreenReaders(vNode));
6363
});
6464

65+
it('should return false if `inert` is set', function () {
66+
var vNode = queryFixture(
67+
'<div id="target" inert>Hidden from screen readers</div>'
68+
);
69+
assert.isFalse(isVisibleToScreenReaders(vNode));
70+
});
71+
6572
it('should return false if `display: none` is set', function () {
6673
var vNode = queryFixture(
6774
'<div id="target" style="display: none">Hidden from screen readers</div>'
@@ -230,5 +237,30 @@ describe('dom.isVisibleToScreenReaders', function () {
230237
vNode.parent = parentVNode;
231238
assert.isFalse(isVisibleToScreenReaders(vNode));
232239
});
240+
241+
it('should return false if `inert` is set', function () {
242+
var vNode = new axe.SerialVirtualNode({
243+
nodeName: 'div',
244+
attributes: {
245+
inert: true
246+
}
247+
});
248+
assert.isFalse(isVisibleToScreenReaders(vNode));
249+
});
250+
251+
it('should return false if `inert` is set on parent', function () {
252+
var vNode = new axe.SerialVirtualNode({
253+
nodeName: 'div'
254+
});
255+
var parentVNode = new axe.SerialVirtualNode({
256+
nodeName: 'div',
257+
attributes: {
258+
inert: true
259+
}
260+
});
261+
parentVNode.children = [vNode];
262+
vNode.parent = parentVNode;
263+
assert.isFalse(isVisibleToScreenReaders(vNode));
264+
});
233265
});
234266
});

test/integration/rules/aria-hidden-focus/aria-hidden-focus.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@
2727
</label>
2828
</div>
2929

30+
<div id="pass7" aria-hidden="true">
31+
<div inert>
32+
<button>hello</button>
33+
</div>
34+
</div>
35+
3036
<!-- ///////////////// -->
3137
<!-- Fail -->
3238
<!-- ///////////////// -->

test/integration/rules/aria-hidden-focus/aria-hidden-focus.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
["#pass3"],
1717
["#pass4"],
1818
["#pass5"],
19-
["#pass6"]
19+
["#pass6"],
20+
["#pass7"]
2021
],
2122
"incomplete": [["#incomplete1"], ["#incomplete2"]]
2223
}

test/integration/rules/frame-focusable-content/frame-focusable-content.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,8 @@
4646
height="0"
4747
id="inapplicable-3"
4848
></iframe>
49+
<iframe
50+
src="/integration/rules/frame-focusable-content/frames/focusable.html"
51+
inert
52+
id="inapplicable-4"
53+
></iframe>

0 commit comments

Comments
 (0)