From 46b6658af6ee21735b59c4c00455c13dddbce761 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 23 Sep 2022 08:15:05 -0600 Subject: [PATCH] fix(aria-hidden-focus): do not fail for focus trap bumper elements (#3667) * fix(aria-hidden-focus): do not fail for focus trap bumper elements * test * fix test * styles * fix test * refactor --- .../keyboard/focusable-disabled-evaluate.js | 37 +++++++----- .../focusable-not-tabbable-evaluate.js | 37 +++++++----- test/checks/keyboard/focusable-disabled.js | 59 +++++++++++-------- .../checks/keyboard/focusable-not-tabbable.js | 53 ++++++++++------- .../aria-hidden-focus/aria-hidden-focus.html | 30 ++++++++-- .../aria-hidden-focus/aria-hidden-focus.json | 3 +- 6 files changed, 136 insertions(+), 83 deletions(-) diff --git a/lib/checks/keyboard/focusable-disabled-evaluate.js b/lib/checks/keyboard/focusable-disabled-evaluate.js index 5ac5955f14..d588071584 100644 --- a/lib/checks/keyboard/focusable-disabled-evaluate.js +++ b/lib/checks/keyboard/focusable-disabled-evaluate.js @@ -2,11 +2,11 @@ import { isModalOpen } from '../../commons/dom'; function focusableDisabledEvaluate(node, options, virtualNode) { const elementsThatCanBeDisabled = [ - 'BUTTON', - 'FIELDSET', - 'INPUT', - 'SELECT', - 'TEXTAREA' + 'button', + 'fieldset', + 'input', + 'select', + 'textarea' ]; const tabbableElements = virtualNode.tabbableElements; @@ -15,21 +15,28 @@ function focusableDisabledEvaluate(node, options, virtualNode) { return true; } - const relatedNodes = tabbableElements.reduce((out, { actualNode: el }) => { - const nodeName = el.nodeName.toUpperCase(); - // populate nodes that can be disabled - if (elementsThatCanBeDisabled.includes(nodeName)) { - out.push(el); - } - return out; - }, []); + const relatedNodes = tabbableElements.filter(vNode => { + return elementsThatCanBeDisabled.includes(vNode.props.nodeName); + }); - this.relatedNodes(relatedNodes); + this.relatedNodes(relatedNodes.map(vNode => vNode.actualNode)); if (relatedNodes.length === 0 || isModalOpen()) { return true; } - return relatedNodes.every(related => related.onfocus) ? undefined : false + + return relatedNodes.every(vNode => { + const pointerEvents = vNode.getComputedStylePropertyValue('pointer-events'); + const width = parseInt(vNode.getComputedStylePropertyValue('width')); + const height = parseInt(vNode.getComputedStylePropertyValue('height')); + + return ( + vNode.actualNode.onfocus || + ((width === 0 || height === 0) && pointerEvents === 'none') + ); + }) + ? undefined + : false; } export default focusableDisabledEvaluate; diff --git a/lib/checks/keyboard/focusable-not-tabbable-evaluate.js b/lib/checks/keyboard/focusable-not-tabbable-evaluate.js index 8607d29bfe..b5a5d2965c 100644 --- a/lib/checks/keyboard/focusable-not-tabbable-evaluate.js +++ b/lib/checks/keyboard/focusable-not-tabbable-evaluate.js @@ -2,11 +2,11 @@ import { isModalOpen } from '../../commons/dom'; function focusableNotTabbableEvaluate(node, options, virtualNode) { const elementsThatCanBeDisabled = [ - 'BUTTON', - 'FIELDSET', - 'INPUT', - 'SELECT', - 'TEXTAREA' + 'button', + 'fieldset', + 'input', + 'select', + 'textarea' ]; const tabbableElements = virtualNode.tabbableElements; @@ -15,21 +15,28 @@ function focusableNotTabbableEvaluate(node, options, virtualNode) { return true; } - const relatedNodes = tabbableElements.reduce((out, { actualNode: el }) => { - const nodeName = el.nodeName.toUpperCase(); - // populate nodes that cannot be disabled - if (!elementsThatCanBeDisabled.includes(nodeName)) { - out.push(el); - } - return out; - }, []); + const relatedNodes = tabbableElements.filter(vNode => { + return !elementsThatCanBeDisabled.includes(vNode.props.nodeName); + }); - this.relatedNodes(relatedNodes); + this.relatedNodes(relatedNodes.map(vNode => vNode.actualNode)); if (relatedNodes.length === 0 || isModalOpen()) { return true; } - return relatedNodes.every(related => related.onfocus) ? undefined : false + + return relatedNodes.every(vNode => { + const pointerEvents = vNode.getComputedStylePropertyValue('pointer-events'); + const width = parseInt(vNode.getComputedStylePropertyValue('width')); + const height = parseInt(vNode.getComputedStylePropertyValue('height')); + + return ( + vNode.actualNode.onfocus || + ((width === 0 || height === 0) && pointerEvents === 'none') + ); + }) + ? undefined + : false; } export default focusableNotTabbableEvaluate; diff --git a/test/checks/keyboard/focusable-disabled.js b/test/checks/keyboard/focusable-disabled.js index 96f9537dee..ffe1536f94 100644 --- a/test/checks/keyboard/focusable-disabled.js +++ b/test/checks/keyboard/focusable-disabled.js @@ -1,4 +1,4 @@ -describe('focusable-disabled', function() { +describe('focusable-disabled', function () { 'use strict'; var check; @@ -8,24 +8,24 @@ describe('focusable-disabled', function() { var checkContext = axe.testUtils.MockCheckContext(); var checkSetup = axe.testUtils.checkSetup; - before(function() { + before(function () { check = checks['focusable-disabled']; }); - afterEach(function() { + afterEach(function () { fixture.innerHTML = ''; axe._tree = undefined; axe._selectorData = undefined; checkContext.reset(); }); - it('returns true when content not focusable by default (no tabbable elements)', function() { + it('returns true when content not focusable by default (no tabbable elements)', function () { var params = checkSetup(''); var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - it('returns true when content hidden through CSS (no tabbable elements)', function() { + it('returns true when content hidden through CSS (no tabbable elements)', function () { var params = checkSetup( '' ); @@ -33,7 +33,7 @@ describe('focusable-disabled', function() { assert.isTrue(actual); }); - it('returns true when content made unfocusable through disabled (no tabbable elements)', function() { + it('returns true when content made unfocusable through disabled (no tabbable elements)', function () { var params = checkSetup( '' ); @@ -41,7 +41,7 @@ describe('focusable-disabled', function() { assert.isTrue(actual); }); - it('returns true when content made unfocusable through disabled fieldset', function() { + it('returns true when content made unfocusable through disabled fieldset', function () { var params = checkSetup( '' ); @@ -51,7 +51,7 @@ describe('focusable-disabled', function() { (shadowSupported ? it : xit)( 'returns false when content is in a disabled fieldset but in another shadow tree', - function() { + function () { var fieldset = document.createElement('fieldset'); fieldset.setAttribute('disabled', 'true'); fieldset.setAttribute('aria-hidden', 'true'); @@ -70,7 +70,7 @@ describe('focusable-disabled', function() { } ); - it('returns false when content is in the legend of a disabled fieldset', function() { + it('returns false when content is in the legend of a disabled fieldset', function () { var params = checkSetup( '' ); @@ -78,7 +78,7 @@ describe('focusable-disabled', function() { assert.isFalse(actual); }); - it('returns false when content is in an aria-hidden but not disabled fieldset', function() { + it('returns false when content is in an aria-hidden but not disabled fieldset', function () { var params = checkSetup( '' ); @@ -86,7 +86,7 @@ describe('focusable-disabled', function() { assert.isFalse(actual); }); - it('returns true when focusable off screen link (cannot be disabled)', function() { + it('returns true when focusable off screen link (cannot be disabled)', function () { var params = checkSetup( '' ); @@ -95,7 +95,7 @@ describe('focusable-disabled', function() { assert.lengthOf(checkContext._relatedNodes, 0); }); - it('returns false when focusable form field only disabled through ARIA', function() { + it('returns false when focusable form field only disabled through ARIA', function () { var params = checkSetup( '' ); @@ -108,7 +108,7 @@ describe('focusable-disabled', function() { ); }); - it('returns false when focusable SELECT element that can be disabled', function() { + it('returns false when focusable SELECT element that can be disabled', function () { var params = checkSetup( '