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(is-visible-on-screen): ignore elements hidden by overflow:hidden #3676

Merged
merged 7 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 3 additions & 2 deletions lib/checks/shared/svg-non-empty-title-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import visibleVirtual from '../../commons/text/visible-virtual';
import { subtreeText } from '../../commons/text';

function svgNonEmptyTitleEvaluate(node, options, virtualNode) {
if (!virtualNode.children) {
Expand All @@ -17,7 +17,8 @@ function svgNonEmptyTitleEvaluate(node, options, virtualNode) {
}

try {
if (visibleVirtual(titleNode) === '') {
const titleText = subtreeText(titleNode, { includeHidden: true }).trim();
if (titleText === '') {
this.data({
messageKey: 'emptyTitle'
});
Expand Down
59 changes: 49 additions & 10 deletions lib/commons/dom/visibility-methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
closest,
getRootNode,
querySelectorAll,
escapeSelector
escapeSelector,
memoize
} from '../../core/utils';
import rectsTouch from '../math/rects-touch';

const clipRegex =
/rect\s*\(([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px\s*\)/;
Expand Down Expand Up @@ -88,21 +90,58 @@ export function scrollHidden(vNode) {
}

/**
* Determine if an element is hidden by using overflow: hidden and dimensions
* Determine if an element is hidden by using overflow: hidden.
* @param {VirtualNode} vNode
* @return {Boolean}
*/
export function overflowHidden(vNode) {
const elHeight = parseInt(vNode.getComputedStylePropertyValue('height'));
const elWidth = parseInt(vNode.getComputedStylePropertyValue('width'));
export function overflowHidden(vNode, { isAncestor } = {}) {
// an ancestor that is hidden outside an overflow
// does not mean that a descendant is also hidden
// since the descendant can reposition itself to be
// in view of the overflow:hidden ancestor
if (isAncestor) {
return false;
}

return (
vNode.getComputedStylePropertyValue('position') === 'absolute' &&
(elHeight < 2 || elWidth < 2) &&
vNode.getComputedStylePropertyValue('overflow') === 'hidden'
);
const rect = vNode.boundingClientRect;
const nodes = getOverflowHiddenAncestors(vNode);

if (!nodes.length) {
return false;
}

return nodes.some(node => {
const nodeRect = node.boundingClientRect;

if (nodeRect.width < 2 || nodeRect.height < 2) {
return true;
}

return !rectsTouch(rect, nodeRect);
});
}

/**
* Get all ancestor nodes (including the passed in node) that have overflow:hidden
*/
const getOverflowHiddenAncestors = memoize(
function getOverflowHiddenAncestorsMemoized(vNode) {
const ancestors = [];

if (!vNode) {
return ancestors;
}

const overflow = vNode.getComputedStylePropertyValue('overflow');

if (overflow === 'hidden') {
ancestors.push(vNode);
}

return ancestors.concat(getOverflowHiddenAncestors(vNode.parent));
}
);

/**
* Determines if an element is hidden with a clip or clip-path technique
* @param {VirtualNode} vNode
Expand Down
1 change: 1 addition & 0 deletions lib/commons/math/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as getOffset } from './get-offset';
export { default as hasVisualOverlap } from './has-visual-overlap';
export { default as rectsTouch } from './rects-touch';
export { default as splitRects } from './split-rects';
23 changes: 23 additions & 0 deletions lib/commons/math/rects-touch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Determine if two rectangles touch.
* @method rectsTouch
* @memberof axe.commons.math
* @param {Rect} rect1
* @param {Rect} rect2
* @returns {Boolean}
*/
export default function rectsTouch(rect1, rect2) {
// perform an AABB (axis-aligned bounding box) check.
// account for differences in how browsers handle floating
// point precision of bounding rects
// @see https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection

/* eslint-disable no-bitwise */
return (
(rect1.left | 0) < (rect2.right | 0) &&
(rect1.right | 0) > (rect2.left | 0) &&
(rect1.top | 0) < (rect2.bottom | 0) &&
(rect1.bottom | 0) > (rect2.top | 0)
);
/* eslint-enable no-bitwise */
}
26 changes: 26 additions & 0 deletions test/checks/color/color-contrast.js
Original file line number Diff line number Diff line change
Expand Up @@ -895,5 +895,31 @@ describe('color-contrast', function () {
);
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});

it('passes for element outside overflow:hidden', function () {
var params = checkSetup(`
<style>
.container {
width: 200px;
height: 200px;
overflow: hidden;
}
.foo * {
width: 200px;
height: 200px;
}
#target {
color: #eee;
}
</style>
<div class="container">
<div class="foo" id="foo">
<div id="one">hello</div>
<div id="target">goodbye</div>
</div>
</div>
`);
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});
});
});
33 changes: 20 additions & 13 deletions test/checks/shared/svg-non-empty-title.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,75 @@
describe('svg-non-empty-title tests', function() {
describe('svg-non-empty-title tests', function () {
'use strict';

var fixture = document.getElementById('fixture');
var checkContext = axe.testUtils.MockCheckContext();
var checkSetup = axe.testUtils.checkSetup;
var checkEvaluate = axe.testUtils.getCheckEvaluate('svg-non-empty-title');

afterEach(function() {
afterEach(function () {
fixture.innerHTML = '';
checkContext.reset();
});

it('returns true if the element has a `title` child', function() {
it('returns true if the element has a `title` child', function () {
var checkArgs = checkSetup(
'<svg id="target"><title>Time II: Party</title></svg>'
);
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});

it('returns true if the `title` child has text nested in another element', function() {
it('returns true if the `title` child has text nested in another element', function () {
var checkArgs = checkSetup(
'<svg id="target"><title><g>Time II: Party</g></title></svg>'
);
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});

it('returns false if the element has no `title` child', function() {
it('returns true if the element has a `title` child with `display:none`', function () {
var checkArgs = checkSetup(
'<svg id="target"><title style="display: none;">Time II: Party</title></svg>'
);
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});

it('returns false if the element has no `title` child', function () {
var checkArgs = checkSetup('<svg id="target"></svg>');
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._data.messageKey, 'noTitle');
});

it('returns false if the `title` child is empty', function() {
it('returns false if the `title` child is empty', function () {
var checkArgs = checkSetup('<svg id="target"><title></title></svg>');
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._data.messageKey, 'emptyTitle');
});

it('returns false if the `title` is a grandchild', function() {
it('returns false if the `title` is a grandchild', function () {
var checkArgs = checkSetup(
'<svg id="target"><circle><title>Time II: Party</title></circle></svg>'
);
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._data.messageKey, 'noTitle');
});

it('returns false if the `title` child has only whitespace', function() {
it('returns false if the `title` child has only whitespace', function () {
var checkArgs = checkSetup(
'<svg id="target"><title> \t\r\n </title></svg>'
);
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._data.messageKey, 'emptyTitle');
});

it('returns false if there are multiple titles, and the first is empty', function() {
it('returns false if there are multiple titles, and the first is empty', function () {
var checkArgs = checkSetup(
'<svg id="target"><title></title><title>Time II: Party</title></svg>'
);
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
assert.equal(checkContext._data.messageKey, 'emptyTitle');
});

describe('Serial Virtual Node', function() {
it('returns true if the element has a `title` child', function() {
describe('Serial Virtual Node', function () {
it('returns true if the element has a `title` child', function () {
var serialNode = new axe.SerialVirtualNode({
nodeName: 'svg'
});
Expand All @@ -82,7 +89,7 @@ describe('svg-non-empty-title tests', function() {
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});

it('returns false if the element has no `title` child', function() {
it('returns false if the element has no `title` child', function () {
var serialNode = new axe.SerialVirtualNode({
nodeName: 'svg'
});
Expand All @@ -93,7 +100,7 @@ describe('svg-non-empty-title tests', function() {
assert.equal(checkContext._data.messageKey, 'noTitle');
});

it('returns undefined if the element has empty children', function() {
it('returns undefined if the element has empty children', function () {
var serialNode = new axe.SerialVirtualNode({
nodeName: 'svg'
});
Expand Down
13 changes: 11 additions & 2 deletions test/commons/dom/is-visible-on-screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,18 @@ describe('dom.isVisibleOnScreen', function () {
assert.isFalse(isVisibleOnScreen(el.actualNode));
}
);
it('should return false if element is visually hidden using position absolute, overflow hidden, and a very small height', function () {

it('should return false for screen reader only technique', function () {
var vNode = queryFixture(
'<div id="target" style="position: absolute; width: 1px; height: 1x; margin: -1px; padding: 0; border: 0; overflow: hidden;">Visually Hidden</div>'
);

assert.isFalse(isVisibleOnScreen(vNode));
});

it('should return false for element outside "overflow:hidden"', function () {
var vNode = queryFixture(
'<div id="target" style="position:absolute; height: 1px; overflow: hidden;">StickySticky</div>'
'<div style="overflow: hidden; height: 100px;"><div id="target" style="margin-top: 200px;">Visually Hidden</div></div>'
);

assert.isFalse(isVisibleOnScreen(vNode));
Expand Down
Loading