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( '' @@ -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" > +