Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of these days I'm going to create a PR to just allow virtual nodes here... Not today though.


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