Skip to content

Commit

Permalink
fix(is-visible-on-screen): ignore elements hidden by overflow:hidden (#…
Browse files Browse the repository at this point in the history
…3676)

* fix(is-visible-on-screen): ignore elements hidden by overflow:hidden

* finalize

* suggestions

* suggestions

* revert files from prettier not in scope

* move test
  • Loading branch information
straker committed Oct 10, 2022
1 parent 4e83437 commit 2935950
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 105 deletions.
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 rectsOverlap from '../math/rects-overlap';

const clipRegex =
/rect\s*\(([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px\s*\)/;
Expand Down Expand Up @@ -102,21 +104,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 !rectsOverlap(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 rectsOverlap } from './rects-overlap';
export { default as splitRects } from './split-rects';
23 changes: 23 additions & 0 deletions lib/commons/math/rects-overlap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Determine if two rectangles touch.
* @method rectsOverlap
* @memberof axe.commons.math
* @param {Rect} rect1
* @param {Rect} rect2
* @returns {Boolean}
*/
export default function rectsOverlap(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 @@ -399,6 +399,32 @@ describe('color-contrast', function () {
});
});

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));
});

describe('with pseudo elements', function () {
it('should return undefined if :before pseudo element has a background color', function () {
var params = checkSetup(
Expand Down
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

0 comments on commit 2935950

Please sign in to comment.