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 compute color-contrast of truncated children #3203

Merged
merged 12 commits into from
Oct 18, 2021
38 changes: 38 additions & 0 deletions lib/commons/color/get-background-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,44 @@ function getBackgroundColor(elm, bgElms = [], shadowOutlineEmMax = 0.1) {
return null;
}

// Body can sometimes appear out of order in the stack:
// 1) Body is not the first element due to negative z-index elements
// 2) Elements are positioned outside of body's rect coordinates
// (see https://github.com/dequelabs/axe-core/issues/1456)
// In those instances we want to reinsert body back into the element stack
// when not using the root document element as the html canvas for bgcolor
if (elmStack.indexOf(document.body) === -1) {
// if the html element defines a bgColor and body defines a
// bgColor but its height is not the full viewport, then the html
// bgColor fills the full viewport and body bgColor only fills to
// its size. however, if the html element does not define a
// bgColor, then the body bgColor fills the full viewport. so if
// the body wasn't added to the elmStack, we need to know which
// bgColor to get (html or body)
const html = document.documentElement;
const body = document.body;
const htmlStyle = window.getComputedStyle(html);
const bodyStyle = window.getComputedStyle(body);
const htmlBgColor = getOwnBackgroundColor(htmlStyle);
const bodyBgColor = getOwnBackgroundColor(bodyStyle);
const bodyBgColorApplies =
bodyBgColor.alpha !== 0 && visuallyContains(elm, body);

if (
(bgColors.alpha !== 0 && htmlBgColor.alpha === 0) ||
(bodyBgColorApplies && bodyBgColor.alpha !== 1)
) {
bgColors.unshift(bodyBgColor);
}

if (
htmlBgColor.alpha !== 0 &&
(!bodyBgColorApplies || (bodyBgColorApplies && bodyBgColor.alpha !== 1))
) {
bgColors.unshift(htmlBgColor);
}
}

// Mix the colors together, on top of a default white. Colors must be mixed
// in bottom up order (background to foreground order) to produce the correct
// result.
Expand Down
42 changes: 23 additions & 19 deletions lib/commons/color/get-background-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,32 +69,36 @@ function sortPageBackground(elmStack) {
const bodyIndex = elmStack.indexOf(document.body);
const bgNodes = elmStack;

// Body can sometimes appear out of order in the stack:
// 1) Body is not the first element due to negative z-index elements
// 2) Elements are positioned outside of body's rect coordinates
// (see https://github.com/dequelabs/axe-core/issues/1456)
// In those instances we want to reinsert body back into the element stack
// when not using the root document element as the html canvas for bgcolor
// prettier-ignore
const sortBodyElement =
bodyIndex > 1 || // negative z-index elements
bodyIndex === -1; // element does not intersect with body

// body can sometimes appear out of order in the stack when it
// is not the first element due to negative z-index elements.
// however, we only want to change order if the html element
// does not define a background color (ya, it's a strange edge
// case. it turns out that if html defines a background it treats
// body as a normal element, but if it doesn't then body is treated
// as the "html" element)
const htmlBgColor = getOwnBackgroundColor(
window.getComputedStyle(document.documentElement)
);
if (
sortBodyElement &&
!elementHasImage(document.documentElement) &&
getOwnBackgroundColor(window.getComputedStyle(document.documentElement))
.alpha === 0
bodyIndex > 1 &&
htmlBgColor.alpha === 0 &&
!elementHasImage(document.documentElement)
) {
// Only remove document.body if it was originally contained within the element stack
if (bodyIndex > 1) {
bgNodes.splice(bodyIndex, 1);

// Put the body background as the lowest element
bgNodes.push(document.body);
}
// Remove document element since body will be used for bgcolor
bgNodes.splice(elmStack.indexOf(document.documentElement), 1);

// Put the body background as the lowest element
bgNodes.push(document.body);
const htmlIndex = bgNodes.indexOf(document.documentElement);
if (htmlIndex > 0) {
bgNodes.splice(htmlIndex, 1);

// Put the body background as the lowest element
bgNodes.push(document.documentElement);
}
}
return bgNodes;
}
Expand Down
82 changes: 46 additions & 36 deletions lib/commons/dom/visually-contains.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,61 +19,71 @@ function getScrollAncestor(node) {
}

/**
* Checks whether a parent element visually contains its child, either directly or via scrolling.
* Checks whether a parent element fully contains its child, either directly or via scrolling.
* Assumes that |parent| is an ancestor of |node|.
* @param {Element} node
* @param {Element} parent
* @return {boolean} True if node is visually contained within parent
*/
function contains(node, parent) {
const rectBound = node.getBoundingClientRect();
const margin = 0.01;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you remove the margin?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do we need the margin?

Copy link
Contributor

Choose a reason for hiding this comment

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

Rounding errors

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If that's the case, I would rather use Math.ceil and Math.floor to handle rounding issues rather than use a magic number. It makes it way more clear what the intent is.

const rect = {
top: rectBound.top + margin,
bottom: rectBound.bottom - margin,
left: rectBound.left + margin,
right: rectBound.right - margin
};

const parentRect = parent.getBoundingClientRect();
const parentTop = parentRect.top;
const parentLeft = parentRect.left;
const parentScrollArea = {
top: parentTop - parent.scrollTop,
bottom: parentTop - parent.scrollTop + parent.scrollHeight,
left: parentLeft - parent.scrollLeft,
right: parentLeft - parent.scrollLeft + parent.scrollWidth
};

const style = window.getComputedStyle(parent);
const overflow = style.getPropertyValue('overflow');

// if parent element is inline, scrollArea will be too unpredictable
if (style.getPropertyValue('display') === 'inline') {
return true;
}

//In theory, we should just be able to look at the scroll area as a superset of the parentRect,
//but that's not true in Firefox
// use clientRects instead of boundingClientRect to account
// for truncation of text (one of the rects will be the size
// of the truncation)
// @see https://github.com/dequelabs/axe-core/issues/2669
const clientRects = Array.from(node.getClientRects());
// getBoundingClientRect prevents overrides of left/top
// (also can't destructure)
const boundingRect = parent.getBoundingClientRect();
const rect = {
left: boundingRect.left,
top: boundingRect.top,
width: boundingRect.width,
height: boundingRect.height
};

if (
(rect.left < parentScrollArea.left && rect.left < parentRect.left) ||
(rect.top < parentScrollArea.top && rect.top < parentRect.top) ||
(rect.right > parentScrollArea.right && rect.right > parentRect.right) ||
(rect.bottom > parentScrollArea.bottom && rect.bottom > parentRect.bottom)
['scroll', 'auto'].includes(overflow) ||
parent instanceof window.HTMLHtmlElement
) {
return false;
rect.width = parent.scrollWidth;
rect.height = parent.scrollHeight;
}

if (rect.right > parentRect.right || rect.bottom > parentRect.bottom) {
return (
style.overflow === 'scroll' ||
style.overflow === 'auto' ||
style.overflow === 'hidden' ||
parent instanceof window.HTMLBodyElement ||
parent instanceof window.HTMLHtmlElement
);
// in Chrome text truncation on the parent will cause the
// child to have multiple client rects (one for the bounding
// rect of the element and one more for the bounding rect of
// the truncation). however this doesn't happen for other
// browsers so we'll make it so that if we detect text
// truncation and there's only one client rect, we'll use
// the bounding rect of the parent as the client rect of
// the child
if (
clientRects.length === 1 &&
overflow === 'hidden' &&
style.getPropertyValue('white-space') === 'nowrap'
) {
clientRects[0] = rect;
}

return true;
// check if any client rect is fully inside the parent rect
// @see https://gist.github.com/Daniel-Hug/d7984d82b58d6d2679a087d896ca3d2b
return clientRects.some(
clientRect =>
!(
clientRect.left < rect.left ||
clientRect.top < rect.top ||
clientRect.left + clientRect.width > rect.left + rect.width ||
clientRect.top + clientRect.height > rect.top + rect.height
)
);
}

/**
Expand Down
Loading