Skip to content

Commit

Permalink
fix(aria-hidden-focus): do not fail for focus trap bumper elements (#…
Browse files Browse the repository at this point in the history
…3667)

* fix(aria-hidden-focus): do not fail for focus trap bumper elements

* test

* fix test

* styles

* fix test

* refactor
  • Loading branch information
straker authored Sep 23, 2022
1 parent 691f1b6 commit 46b6658
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 83 deletions.
37 changes: 22 additions & 15 deletions lib/checks/keyboard/focusable-disabled-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
37 changes: 22 additions & 15 deletions lib/checks/keyboard/focusable-not-tabbable-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
59 changes: 34 additions & 25 deletions test/checks/keyboard/focusable-disabled.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
describe('focusable-disabled', function() {
describe('focusable-disabled', function () {
'use strict';

var check;
Expand All @@ -8,40 +8,40 @@ 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('<p id="target" aria-hidden="true">Some text</p>');
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(
'<div id="target" aria-hidden="true"><a href="/" style="display:none">Link</a></div>'
);
var actual = check.evaluate.apply(checkContext, params);
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(
'<input id="target" disabled aria-hidden="true" />'
);
var actual = check.evaluate.apply(checkContext, params);
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(
'<fieldset id="target" disabled aria-hidden="true"><input /></fieldset>'
);
Expand All @@ -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');
Expand All @@ -70,23 +70,23 @@ 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(
'<fieldset id="target" disabled aria-hidden="true"><legend><input /></legend></fieldset>'
);
var actual = check.evaluate.apply(checkContext, params);
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(
'<fieldset id="target" aria-hidden="true"><input /></fieldset>'
);
var actual = check.evaluate.apply(checkContext, params);
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(
'<div id="target" aria-hidden="true"><a href="/" style="position:absolute; top:-999em">Link</a></div>'
);
Expand All @@ -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(
'<div id="target" aria-hidden="true"><input type="text" aria-disabled="true"/></div>'
);
Expand All @@ -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(
'<div id="target" aria-hidden="true">' +
'<label>Choose:' +
Expand All @@ -128,7 +128,7 @@ describe('focusable-disabled', function() {
);
});

it('returns true when focusable AREA element (cannot be disabled)', function() {
it('returns true when focusable AREA element (cannot be disabled)', function () {
var params = checkSetup(
'<main id="target" aria-hidden="true">' +
'<map name="infographic">' +
Expand All @@ -143,7 +143,7 @@ describe('focusable-disabled', function() {

(shadowSupported ? it : xit)(
'returns false when focusable content inside shadowDOM, that can be disabled',
function() {
function () {
// Note:
// `testUtils.checkSetup` does not work for shadowDOM
// as `axe._tree` and `axe._selectorData` needs to be updated after shadowDOM construction
Expand All @@ -159,23 +159,23 @@ describe('focusable-disabled', function() {
}
);

it('returns true when focusable target that cannot be disabled', function() {
it('returns true when focusable target that cannot be disabled', function () {
var params = checkSetup(
'<div aria-hidden="true"><a id="target" href="">foo</a><button>bar</button></div>'
);
var actual = check.evaluate.apply(checkContext, params);
assert.isTrue(actual);
});

it('returns false when focusable target that can be disabled', function() {
it('returns false when focusable target that can be disabled', function () {
var params = checkSetup(
'<div aria-hidden="true"><a href="">foo</a><button id="target">bar</button></div>'
);
var actual = check.evaluate.apply(checkContext, params);
assert.isFalse(actual);
});

it('returns true if there is a focusable element and modal is open', function() {
it('returns true if there is a focusable element and modal is open', function () {
var params = checkSetup(
'<div id="target" aria-hidden="true">' +
'<button>Some button</button>' +
Expand All @@ -194,19 +194,28 @@ describe('focusable-disabled', function() {
});

it('returns undefined when all focusable controls have onfocus events', function () {
var params = checkSetup('<div aria-hidden="true" id="target">' +
' <button onfocus="redirectFocus()">button</button>' +
'</div>'
var params = checkSetup(
'<div aria-hidden="true" id="target">' +
' <button onfocus="redirectFocus()">button</button>' +
'</div>'
);
assert.isUndefined(check.evaluate.apply(checkContext, params));
});

it('returns false when some, but not all focusable controls have onfocus events', function () {
var params = checkSetup('<div aria-hidden="true" id="target">' +
' <button onfocus="redirectFocus()">button</button>' +
' <button>button</button>' +
'</div>'
var params = checkSetup(
'<div aria-hidden="true" id="target">' +
' <button onfocus="redirectFocus()">button</button>' +
' <button>button</button>' +
'</div>'
);
assert.isFalse(check.evaluate.apply(checkContext, params));
});

it('returns undefined when control has 0 width and height and pointer events: none (focus trap bumper)', () => {
var params = checkSetup(
'<button id="target" aria-hidden="true" style="pointer-events: none; width: 0; height: 0; margin: 0; padding: 0; border: 0"></button>'
);
assert.isUndefined(check.evaluate.apply(checkContext, params));
});
});
Loading

0 comments on commit 46b6658

Please sign in to comment.