From 353755d46229e00fd5d418c2fa6c5786bb74d054 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 16 Jan 2023 14:23:42 -0700 Subject: [PATCH 1/3] feat(dom.focus-disabled,dom.is-visible-for-screenreader): support the inert attribute --- lib/commons/dom/focus-disabled.js | 7 +++- lib/commons/dom/index.js | 1 + lib/commons/dom/is-inert.js | 36 +++++++++++++++++ .../dom/is-visible-for-screenreader.js | 3 +- test/commons/dom/focus-disabled.js | 23 +++++++++++ test/commons/dom/is-inert.js | 40 +++++++++++++++++++ .../dom/is-visible-for-screenreader.js | 32 +++++++++++++++ 7 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 lib/commons/dom/is-inert.js create mode 100644 test/commons/dom/is-inert.js 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..778fd1363e --- /dev/null +++ b/lib/commons/dom/is-inert.js @@ -0,0 +1,36 @@ +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 + */ +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..d825fbe3cd --- /dev/null +++ b/test/commons/dom/is-inert.js @@ -0,0 +1,40 @@ +describe('dom.is-inert', () => { + const isInert = axe.commons.dom.isInert; + const { queryFixture } = axe.testUtils; + + it('should return true for element with "inert=false`', () => { + const vNode = queryFixture('
'); + + assert.isTrue(isInert(vNode)); + }); + + it('should return true for element with "inert`', () => { + const vNode = queryFixture('
'); + + assert.isTrue(isInert(vNode)); + }); + + it('should return false for element without inert', () => { + const vNode = queryFixture('
'); + + assert.isFalse(isInert(vNode)); + }); + + it('should return true for ancestor with inert', () => { + const vNode = queryFixture( + '
' + ); + + assert.isTrue(isInert(vNode)); + }); + + describe('options.skipAncestors', () => { + it('should return 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)); + }); }); }); From df5c9302ed10698329f958c74ef33992c085b647 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 16 Jan 2023 14:26:46 -0700 Subject: [PATCH 2/3] typos --- lib/commons/dom/is-inert.js | 1 + test/commons/dom/is-inert.js | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/commons/dom/is-inert.js b/lib/commons/dom/is-inert.js index 778fd1363e..fc4c821da5 100644 --- a/lib/commons/dom/is-inert.js +++ b/lib/commons/dom/is-inert.js @@ -4,6 +4,7 @@ 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) { diff --git a/test/commons/dom/is-inert.js b/test/commons/dom/is-inert.js index d825fbe3cd..bd1d344c56 100644 --- a/test/commons/dom/is-inert.js +++ b/test/commons/dom/is-inert.js @@ -1,26 +1,26 @@ -describe('dom.is-inert', () => { +describe('dom.isInert', () => { const isInert = axe.commons.dom.isInert; const { queryFixture } = axe.testUtils; - it('should return true for element with "inert=false`', () => { + it('returns true for element with "inert=false`', () => { const vNode = queryFixture('
'); assert.isTrue(isInert(vNode)); }); - it('should return true for element with "inert`', () => { + it('returns true for element with "inert`', () => { const vNode = queryFixture('
'); assert.isTrue(isInert(vNode)); }); - it('should return false for element without inert', () => { + it('returns false for element without inert', () => { const vNode = queryFixture('
'); assert.isFalse(isInert(vNode)); }); - it('should return true for ancestor with inert', () => { + it('returns true for ancestor with inert', () => { const vNode = queryFixture( '
' ); @@ -29,7 +29,7 @@ describe('dom.is-inert', () => { }); describe('options.skipAncestors', () => { - it('should return false for ancestor with inert', () => { + it('returns false for ancestor with inert', () => { const vNode = queryFixture( '
' ); From 41f469aa53b652d7e6f60e6c295c38eb1c9cf3da Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 17 Jan 2023 09:03:16 -0700 Subject: [PATCH 3/3] integration tests --- .../rules/aria-hidden-focus/aria-hidden-focus.html | 6 ++++++ .../rules/aria-hidden-focus/aria-hidden-focus.json | 3 ++- .../frame-focusable-content/frame-focusable-content.html | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) 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" > +