Skip to content

Commit

Permalink
fix(color-contrast): correctly calculate background color of text nod…
Browse files Browse the repository at this point in the history
…es with different size than their container (#3703)

* fix(color-contrast): correctly calculate background color of text nodes with different size than their container

* undo file rename

* fix test

* remove export

* test name

* comment

* undeprecate for this pr

* tests

* fix test

* fix firefox test

* fix bug

* firefox...

* firefox ci is annoying

* fix test?

* new lint

* sigh...

* :P

* :P

* works now?

* Update lib/commons/color/get-background-color.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/commons/color/get-background-color.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/commons/math/get-intersection-rect.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/commons/math/get-intersection-rect.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/commons/math/get-intersection-rect.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* changes

* Update lib/commons/color/get-background-stack.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/commons/color/get-background-color.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/commons/dom/get-visible-text-rects.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* changes

* 🤖 Automated formatting fixes

* suggestions

* suggestions

* rename

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
Co-authored-by: straker <straker@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 8, 2022
1 parent 08e2d20 commit 123b83c
Show file tree
Hide file tree
Showing 16 changed files with 605 additions and 187 deletions.
83 changes: 54 additions & 29 deletions lib/commons/color/get-background-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Color from './color';
import flattenColors from './flatten-colors';
import flattenShadowColors from './flatten-shadow-colors';
import getTextShadowColors from './get-text-shadow-colors';
import visuallyContains from '../dom/visually-contains';
import getVisibleChildTextRects from '../dom/get-visible-child-text-rects';
import { getNodeFromTree } from '../../core/utils';

/**
Expand Down Expand Up @@ -53,40 +53,47 @@ function _getBackgroundColor(elm, bgElms, shadowOutlineEmMax) {
}

const elmStack = getBackgroundStack(elm);
const textRects = getVisibleChildTextRects(elm);

// Search the stack until we have an alpha === 1 background
(elmStack || []).some(bgElm => {
const bgElmStyle = window.getComputedStyle(bgElm);

if (elementHasImage(bgElm, bgElmStyle)) {
bgColors = null;
bgElms.push(bgElm);

return true;
}

// Get the background color
const bgColor = getOwnBackgroundColor(bgElmStyle);
if (bgColor.alpha === 0) {
return false;
}

// abort if a node is partially obscured and obscuring element has a background
if (
// abort if a node is partially obscured and obscuring element has a background
elmPartiallyObscured(elm, bgElm, bgColor) ||
// OR if the background elm is a graphic
elementHasImage(bgElm, bgElmStyle)
bgElmStyle.getPropertyValue('display') !== 'inline' &&
!fullyEncompasses(bgElm, textRects)
) {
bgColors = null;
bgElms.push(bgElm);
incompleteData.set('bgColor', 'elmPartiallyObscured');

return true;
}

if (bgColor.alpha !== 0) {
// store elements contributing to the br color.
bgElms.push(bgElm);
const blendMode = bgElmStyle.getPropertyValue('mix-blend-mode');
bgColors.unshift({
color: bgColor,
blendMode: normalizeBlendMode(blendMode)
});
// store elements contributing to the bg color.
bgElms.push(bgElm);
const blendMode = bgElmStyle.getPropertyValue('mix-blend-mode');
bgColors.unshift({
color: bgColor,
blendMode: normalizeBlendMode(blendMode)
});

// Exit if the background is opaque
return bgColor.alpha === 1;
} else {
return false;
}
// Exit if the background is opaque
return bgColor.alpha === 1;
});

if (bgColors === null || elmStack === null) {
Expand Down Expand Up @@ -124,20 +131,37 @@ function _getBackgroundColor(elm, bgElms, shadowOutlineEmMax) {
}

/**
* Determine if element is partially overlapped, triggering a Can't Tell result
* Checks whether a node fully encompasses a set of rects.
* @private
* @param {Element} elm
* @param {Element} bgElm
* @param {Object} bgColor
* @param {Element} node
* @param {NodeRect[]} rects
* @return {Boolean}
*/
function elmPartiallyObscured(elm, bgElm, bgColor) {
var obscured =
elm !== bgElm && !visuallyContains(elm, bgElm) && bgColor.alpha !== 0;
if (obscured) {
incompleteData.set('bgColor', 'elmPartiallyObscured');
function fullyEncompasses(node, rects) {
rects = Array.isArray(rects) ? rects : [rects];

const nodeRect = node.getBoundingClientRect();
const style = window.getComputedStyle(node);
const overflow = style.getPropertyValue('overflow');

if (
['scroll', 'auto'].includes(overflow) ||
node instanceof window.HTMLHtmlElement
) {
nodeRect.width = node.scrollWidth;
nodeRect.height = node.scrollHeight;
nodeRect.right = nodeRect.left + nodeRect.width;
nodeRect.bottom = nodeRect.top + nodeRect.height;
}
return obscured;

return rects.every(rect => {
return (
rect.top >= nodeRect.top &&
rect.bottom <= nodeRect.bottom &&
rect.left >= nodeRect.left &&
rect.right <= nodeRect.right
);
});
}

function normalizeBlendMode(blendmode) {
Expand Down Expand Up @@ -174,7 +198,8 @@ function getPageBackgroundColors(elm, stackContainsBody) {
const htmlBgColor = getOwnBackgroundColor(htmlStyle);
const bodyBgColor = getOwnBackgroundColor(bodyStyle);
const bodyBgColorApplies =
bodyBgColor.alpha !== 0 && visuallyContains(elm, body);
bodyBgColor.alpha !== 0 &&
fullyEncompasses(body, elm.getBoundingClientRect());
if (
(bodyBgColor.alpha !== 0 && htmlBgColor.alpha === 0) ||
(bodyBgColorApplies && bodyBgColor.alpha !== 1)
Expand Down
104 changes: 43 additions & 61 deletions lib/commons/color/get-background-stack.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,41 @@
import filteredRectStack from './filtered-rect-stack';
import getTextElementStack from '../dom/get-text-element-stack';
import elementHasImage from './element-has-image';
import getOwnBackgroundColor from './get-own-background-color';
import incompleteData from './incomplete-data';
import reduceToElementsBelowFloating from '../dom/reduce-to-elements-below-floating';

/**
* Determine if element B is an inline descendant of A
* @private
* Get all elements rendered underneath the current element,
* In the order they are displayed (front to back)
*
* @method getBackgroundStack
* @memberof axe.commons.color
* @param {Element} node
* @param {Element} descendant
* @return {Boolean}
* @return {Array}
*/
function isInlineDescendant(node, descendant) {
const CONTAINED_BY = window.Node.DOCUMENT_POSITION_CONTAINED_BY;
// eslint-disable-next-line no-bitwise
if (!(node.compareDocumentPosition(descendant) & CONTAINED_BY)) {
return false;
}
const style = window.getComputedStyle(descendant);
const display = style.getPropertyValue('display');
if (!display.includes('inline')) {
return false;
}
// IE needs this; It doesn't set display:block when position is set
const position = style.getPropertyValue('position');
return position === 'static';
}
export default function getBackgroundStack(node) {
const stacks = getTextElementStack(node).map(stack => {
stack = reduceToElementsBelowFloating(stack, node);
stack = sortPageBackground(stack);
return stack;
});

/**
* Determine if the element obscures / overlaps with the text
* @private
* @param {Number} elmIndex
* @param {Array} elmStack
* @param {Element} originalElm
* @return {Number|undefined}
*/
function calculateObscuringElement(elmIndex, elmStack, originalElm) {
// Reverse order, so that we can safely splice
for (let i = elmIndex - 1; i >= 0; i--) {
if (!isInlineDescendant(originalElm, elmStack[i])) {
return true;
for (let index = 0; index < stacks.length; index++) {
const stack = stacks[index];

if (stack[0] !== node) {
incompleteData.set('bgColor', 'bgOverlap');
return null;
}

// verify stacks are the same
if (index !== 0 && !shallowArraysEqual(stack, stacks[0])) {
incompleteData.set('bgColor', 'elmPartiallyObscuring');
return null;
}
// Ignore inline descendants, for example:
// <p>text <img></p>; We don't care about the <img> element,
// since it does not overlap the text inside of <p>
elmStack.splice(i, 1);
}
return false;

return stacks[0] || null;
}

/**
Expand Down Expand Up @@ -95,31 +84,24 @@ function sortPageBackground(elmStack) {
}

/**
* Get all elements rendered underneath the current element,
* In the order they are displayed (front to back)
*
* @method getBackgroundStack
* @memberof axe.commons.color
* @param {Element} elm
* @return {Array}
* Check to see if two arrays are equal
* @see https://stackoverflow.com/a/16436975/2124254
*/
function getBackgroundStack(elm) {
let elmStack = filteredRectStack(elm);

if (elmStack === null) {
return null;
function shallowArraysEqual(a, b) {
if (a === b) {
return true;
}
if (a === null || b === null) {
return false;
}
if (a.length !== b.length) {
return false;
}
elmStack = reduceToElementsBelowFloating(elmStack, elm);
elmStack = sortPageBackground(elmStack);

// Return all elements BELOW the current element, null if the element is undefined
const elmIndex = elmStack.indexOf(elm);
if (calculateObscuringElement(elmIndex, elmStack, elm)) {
// if the total of the elements above our element results in total obscuring, return null
incompleteData.set('bgColor', 'bgOverlap');
return null;
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) {
return false;
}
}
return elmIndex !== -1 ? elmStack : null;
return true;
}

export default getBackgroundStack;
28 changes: 28 additions & 0 deletions lib/commons/dom/get-overflow-hidden-ancestors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import memoize from '../../core/utils/memoize';

/**
* Get all ancestor nodes (including the passed in node) that have overflow:hidden
* @method getOverflowHiddenAncestors
* @memberof axe.commons.dom
* @param {VirtualNode} vNode
* @returns {VirtualNode[]}
*/
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));
}
);

export default getOverflowHiddenAncestors;
53 changes: 2 additions & 51 deletions lib/commons/dom/get-text-element-stack.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import getElementStack from './get-element-stack';
import { getRectStack } from './get-rect-stack';
import createGrid from './create-grid';
import sanitize from '../text/sanitize';
import { getNodeFromTree } from '../../core/utils';
import { isPointInRect, getRectCenter } from '../math';
import getVisibleChildTextRects from './get-visible-child-text-rects';

/**
* Return all elements that are at the center of each text client rect of the passed in node.
Expand All @@ -22,54 +20,7 @@ function getTextElementStack(node) {
return [];
}

// for code blocks that use syntax highlighting, you can get a ton of client
// rects (See https://github.com/dequelabs/axe-core/issues/1985). they use
// a mixture of text nodes and other nodes (which will contain their own text
// nodes), but all we care about is checking the direct text nodes as the
// other nodes will have their own client rects checked. doing this speeds up
// color contrast significantly for large syntax highlighted code blocks
const nodeRect = vNode.boundingClientRect;
const clientRects = [];
Array.from(node.childNodes).forEach(elm => {
if (elm.nodeType === 3 && sanitize(elm.textContent) !== '') {
const range = document.createRange();
range.selectNodeContents(elm);
const rects = Array.from(range.getClientRects());
/**
* if any text rect is larger than the bounds of the parent,
* or goes outside of the bounds of the parent, we need to use
* the parent rect so we stay within the bounds of the element.
*
* since we use the midpoint of the element when determining
* the rect stack we will also use the midpoint of the text rect
* to determine out of bounds.
*
* @see https://github.com/dequelabs/axe-core/issues/2178
* @see https://github.com/dequelabs/axe-core/issues/2483
* @see https://github.com/dequelabs/axe-core/issues/2681
*/
const outsideRectBounds = rects.some(rect => {
const centerPoint = getRectCenter(rect);
return !isPointInRect(centerPoint, nodeRect);
});
if (outsideRectBounds) {
return;
}

for (const rect of rects) {
// filter out 0 width and height rects (newline characters)
// ie11 has newline characters return 0.00998, so we'll say if the
// line is < 1 it shouldn't be counted
if (rect.width >= 1 && rect.height >= 1) {
clientRects.push(rect);
}
}
}
});

if (!clientRects.length) {
return [getElementStack(node)];
}
const clientRects = getVisibleChildTextRects(node);
return clientRects.map(rect => getRectStack(grid, rect));
}

Expand Down
Loading

0 comments on commit 123b83c

Please sign in to comment.