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(color-contrast): correctly calculate background color of text nodes with different size than their container #3703

Merged
merged 40 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d94e10e
fix(color-contrast): correctly calculate background color of text nod…
straker Oct 7, 2022
51f2d1a
undo file rename
straker Oct 7, 2022
2b4b5c5
fix test
straker Oct 7, 2022
5189270
remove export
straker Oct 7, 2022
ee9dc00
test name
straker Oct 7, 2022
d6fbe0d
comment
straker Oct 7, 2022
5af92cf
merge develop
straker Oct 20, 2022
e5e49a6
undeprecate for this pr
straker Oct 20, 2022
b473b51
tests
straker Oct 21, 2022
2d07ab0
Merge branch 'develop' into color-contrast-text-rects
straker Oct 21, 2022
8b4a242
fix test
straker Oct 21, 2022
9c79ddb
fix firefox test
straker Oct 24, 2022
647447f
fix bug
straker Oct 24, 2022
717d5f7
firefox...
straker Oct 24, 2022
3c75348
firefox ci is annoying
straker Oct 24, 2022
46e5073
Merge branch 'develop' into color-contrast-text-rects
straker Nov 2, 2022
90b88b2
fix test?
straker Nov 2, 2022
e6db4ec
new lint
straker Nov 2, 2022
8814049
sigh...
straker Nov 2, 2022
1f3eefd
:P
straker Nov 2, 2022
bd623a2
:P
straker Nov 2, 2022
90effe0
works now?
straker Nov 2, 2022
4347550
Update lib/commons/color/get-background-color.js
straker Nov 8, 2022
f95a15d
Update lib/commons/color/get-background-color.js
straker Nov 8, 2022
0e18a8f
Update lib/commons/math/get-intersection-rect.js
straker Nov 8, 2022
6ead726
Update lib/commons/math/get-intersection-rect.js
straker Nov 8, 2022
d928c67
Update lib/commons/math/get-intersection-rect.js
straker Nov 8, 2022
a44d62f
Merge branch 'develop' into color-contrast-text-rects
straker Nov 21, 2022
c7d2720
changes
straker Nov 21, 2022
c20d0dc
Update lib/commons/color/get-background-stack.js
straker Nov 29, 2022
2b00d57
Update lib/commons/color/get-background-color.js
straker Nov 29, 2022
0ba9fdd
Update lib/commons/dom/get-visible-text-rects.js
straker Nov 29, 2022
82308bd
Merge branch 'develop' into color-contrast-text-rects
straker Dec 6, 2022
17bf2d0
Merge branch 'color-contrast-text-rects' of https://github.com/dequel…
straker Dec 6, 2022
871a7c9
changes
straker Dec 6, 2022
652866a
:robot: Automated formatting fixes
straker Dec 6, 2022
a58b620
Merge branch 'develop' into color-contrast-text-rects
straker Dec 6, 2022
a49adbb
suggestions
straker Dec 7, 2022
dc8656b
suggestions
straker Dec 8, 2022
de4c3ef
rename
straker Dec 8, 2022
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
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);
straker marked this conversation as resolved.
Show resolved Hide resolved

// 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');
straker marked this conversation as resolved.
Show resolved Hide resolved

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);
WilcoFiers marked this conversation as resolved.
Show resolved Hide resolved

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') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should have a ticket for overflow-x: hidden overflow-y: visible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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