diff --git a/lib/commons/dom/focus-disabled.js b/lib/commons/dom/focus-disabled.js
index 427c1aa0c0..8ba7be8855 100644
--- a/lib/commons/dom/focus-disabled.js
+++ b/lib/commons/dom/focus-disabled.js
@@ -1,6 +1,8 @@
import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node';
import { getNodeFromTree } from '../../core/utils';
import isHiddenForEveryone from './is-hidden-for-everyone';
+import isInert from './is-inert';
+
// Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled
const allowedDisabledNodeNames = [
'button',
@@ -27,8 +29,9 @@ function focusDisabled(el) {
const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el);
if (
- isDisabledAttrAllowed(vNode.props.nodeName) &&
- vNode.hasAttr('disabled')
+ (isDisabledAttrAllowed(vNode.props.nodeName) &&
+ vNode.hasAttr('disabled')) ||
+ isInert(vNode)
) {
return true;
}
diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js
index 20a972cb47..99389d55f8 100644
--- a/lib/commons/dom/index.js
+++ b/lib/commons/dom/index.js
@@ -31,6 +31,7 @@ export { default as isHiddenForEveryone } from './is-hidden-for-everyone';
export { default as isHTML5 } from './is-html5';
export { default as isInTabOrder } from './is-in-tab-order';
export { default as isInTextBlock } from './is-in-text-block';
+export { default as isInert } from './is-inert';
export { default as isModalOpen } from './is-modal-open';
export { default as isMultiline } from './is-multiline';
export { default as isNativelyFocusable } from './is-natively-focusable';
diff --git a/lib/commons/dom/is-inert.js b/lib/commons/dom/is-inert.js
new file mode 100644
index 0000000000..fc4c821da5
--- /dev/null
+++ b/lib/commons/dom/is-inert.js
@@ -0,0 +1,37 @@
+import memoize from '../../core/utils/memoize';
+
+/**
+ * Determines if an element is inside an inert subtree.
+ * @param {VirtualNode} vNode
+ * @param {Boolean} [options.skipAncestors] If the ancestor tree should not be used
+ * @return {Boolean} The element's inert state
+ */
+export default function isInert(vNode, { skipAncestors } = {}) {
+ if (skipAncestors) {
+ return isInertSelf(vNode);
+ }
+
+ return isInertAncestors(vNode);
+}
+
+/**
+ * Check the element for inert
+ */
+const isInertSelf = memoize(function isInertSelfMemoized(vNode) {
+ return vNode.hasAttr('inert');
+});
+
+/**
+ * Check the element and ancestors for inert
+ */
+const isInertAncestors = memoize(function isInertAncestorsMemoized(vNode) {
+ if (isInertSelf(vNode)) {
+ return true;
+ }
+
+ if (!vNode.parent) {
+ return false;
+ }
+
+ return isInertAncestors(vNode.parent);
+});
diff --git a/lib/commons/dom/is-visible-for-screenreader.js b/lib/commons/dom/is-visible-for-screenreader.js
index 712acbda37..79771e9129 100644
--- a/lib/commons/dom/is-visible-for-screenreader.js
+++ b/lib/commons/dom/is-visible-for-screenreader.js
@@ -3,6 +3,7 @@ import { getNodeFromTree } from '../../core/utils';
import memoize from '../../core/utils/memoize';
import isHiddenForEveryone from './is-hidden-for-everyone';
import { ariaHidden, areaHidden } from './visibility-methods';
+import isInert from './is-inert';
/**
* Determine if an element is visible to a screen reader
@@ -21,7 +22,7 @@ export default function isVisibleToScreenReaders(vNode) {
*/
const isVisibleToScreenReadersVirtual = memoize(
function isVisibleToScreenReadersMemoized(vNode, isAncestor) {
- if (ariaHidden(vNode)) {
+ if (ariaHidden(vNode) || isInert(vNode, { skipAncestors: true })) {
return false;
}
diff --git a/test/commons/dom/focus-disabled.js b/test/commons/dom/focus-disabled.js
index 2f8a242dc4..998dd60f9d 100644
--- a/test/commons/dom/focus-disabled.js
+++ b/test/commons/dom/focus-disabled.js
@@ -51,6 +51,29 @@ describe('dom.focus-disabled', () => {
assert.isTrue(focusDisabled(vNode));
});
+ it('returns true for element with inert', () => {
+ const vNode = queryFixture('');
+
+ assert.isTrue(focusDisabled(vNode));
+ });
+
+ it('returns true for ancestor with inert', () => {
+ const vNode = queryFixture(
+ '
'
+ );
+
+ assert.isTrue(focusDisabled(vNode));
+ });
+
+ it('returns true for ancestor with inert outside shadow tree', () => {
+ const vNode = queryShadowFixture(
+ '',
+ ''
+ );
+
+ assert.isTrue(focusDisabled(vNode));
+ });
+
describe('SerialVirtualNode', () => {
it('returns false if element is hidden for everyone', () => {
const vNode = new axe.SerialVirtualNode({
diff --git a/test/commons/dom/is-inert.js b/test/commons/dom/is-inert.js
new file mode 100644
index 0000000000..bd1d344c56
--- /dev/null
+++ b/test/commons/dom/is-inert.js
@@ -0,0 +1,40 @@
+describe('dom.isInert', () => {
+ const isInert = axe.commons.dom.isInert;
+ const { queryFixture } = axe.testUtils;
+
+ it('returns true for element with "inert=false`', () => {
+ const vNode = queryFixture('');
+
+ assert.isTrue(isInert(vNode));
+ });
+
+ it('returns true for element with "inert`', () => {
+ const vNode = queryFixture('');
+
+ assert.isTrue(isInert(vNode));
+ });
+
+ it('returns false for element without inert', () => {
+ const vNode = queryFixture('');
+
+ assert.isFalse(isInert(vNode));
+ });
+
+ it('returns true for ancestor with inert', () => {
+ const vNode = queryFixture(
+ ''
+ );
+
+ assert.isTrue(isInert(vNode));
+ });
+
+ describe('options.skipAncestors', () => {
+ it('returns false for ancestor with inert', () => {
+ const vNode = queryFixture(
+ ''
+ );
+
+ assert.isFalse(isInert(vNode, { skipAncestors: true }));
+ });
+ });
+});
diff --git a/test/commons/dom/is-visible-for-screenreader.js b/test/commons/dom/is-visible-for-screenreader.js
index 354b1becd8..3586366a2a 100644
--- a/test/commons/dom/is-visible-for-screenreader.js
+++ b/test/commons/dom/is-visible-for-screenreader.js
@@ -62,6 +62,13 @@ describe('dom.isVisibleToScreenReaders', function () {
assert.isFalse(isVisibleToScreenReaders(vNode));
});
+ it('should return false if `inert` is set', function () {
+ var vNode = queryFixture(
+ 'Hidden from screen readers
'
+ );
+ assert.isFalse(isVisibleToScreenReaders(vNode));
+ });
+
it('should return false if `display: none` is set', function () {
var vNode = queryFixture(
'Hidden from screen readers
'
@@ -230,5 +237,30 @@ describe('dom.isVisibleToScreenReaders', function () {
vNode.parent = parentVNode;
assert.isFalse(isVisibleToScreenReaders(vNode));
});
+
+ it('should return false if `inert` is set', function () {
+ var vNode = new axe.SerialVirtualNode({
+ nodeName: 'div',
+ attributes: {
+ inert: true
+ }
+ });
+ assert.isFalse(isVisibleToScreenReaders(vNode));
+ });
+
+ it('should return false if `inert` is set on parent', function () {
+ var vNode = new axe.SerialVirtualNode({
+ nodeName: 'div'
+ });
+ var parentVNode = new axe.SerialVirtualNode({
+ nodeName: 'div',
+ attributes: {
+ inert: true
+ }
+ });
+ parentVNode.children = [vNode];
+ vNode.parent = parentVNode;
+ assert.isFalse(isVisibleToScreenReaders(vNode));
+ });
});
});
diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html
index 59d9532886..b43814699a 100644
--- a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html
+++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html
@@ -27,6 +27,12 @@
+
+
diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json
index 1c6640ebd9..908a0e7178 100644
--- a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json
+++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json
@@ -16,7 +16,8 @@
["#pass3"],
["#pass4"],
["#pass5"],
- ["#pass6"]
+ ["#pass6"],
+ ["#pass7"]
],
"incomplete": [["#incomplete1"], ["#incomplete2"]]
}
diff --git a/test/integration/rules/frame-focusable-content/frame-focusable-content.html b/test/integration/rules/frame-focusable-content/frame-focusable-content.html
index f61fdba0be..8928fc6800 100644
--- a/test/integration/rules/frame-focusable-content/frame-focusable-content.html
+++ b/test/integration/rules/frame-focusable-content/frame-focusable-content.html
@@ -46,3 +46,8 @@
height="0"
id="inapplicable-3"
>
+