diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index 230fa9d568..ff2a28bc87 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -8,23 +8,6 @@ import flattenShadowColors from './flatten-shadow-colors'; import getTextShadowColors from './get-text-shadow-colors'; import visuallyContains from '../dom/visually-contains'; -/** - * Determine if element is partially overlapped, triggering a Can't Tell result - * @private - * @param {Element} elm - * @param {Element} bgElm - * @param {Object} bgColor - * @return {Boolean} - */ -function elmPartiallyObscured(elm, bgElm, bgColor) { - var obscured = - elm !== bgElm && !visuallyContains(elm, bgElm) && bgColor.alpha !== 0; - if (obscured) { - incompleteData.set('bgColor', 'elmPartiallyObscured'); - } - return obscured; -} - /** * Returns background color for element * Uses getBackgroundStack() to get all elements rendered underneath the current element, @@ -32,12 +15,16 @@ function elmPartiallyObscured(elm, bgElm, bgColor) { * * @method getBackgroundColor * @memberof axe.commons.color - * @param {Element} elm Element to determine background color - * @param {Array} [bgElms=[]] elements to inspect - * @param {Number} shadowOutlineEmMax Thickness of `text-shadow` at which it becomes a background color + * @param {Element} elm Element to determine background color + * @param {Array} [bgElms=[]] elements to inspect + * @param {Number} shadowOutlineEmMax Thickness of `text-shadow` at which it becomes a background color * @returns {Color} */ -function getBackgroundColor(elm, bgElms = [], shadowOutlineEmMax = 0.1) { +export default function getBackgroundColor( + elm, + bgElms = [], + shadowOutlineEmMax = 0.1 +) { let bgColors = getTextShadowColors(elm, { minRatio: shadowOutlineEmMax }); if (bgColors.length) { bgColors = [bgColors.reduce(flattenShadowColors)]; @@ -79,15 +66,89 @@ function getBackgroundColor(elm, bgElms = [], shadowOutlineEmMax = 0.1) { return null; } - // 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 + const pageBgs = getPageBackgroundColors( + elm, + elmStack.includes(document.body) + ); + bgColors.unshift(...pageBgs); + + // Mix the colors together. Colors must be mixed in bottom up + // order (background to foreground order) to produce the correct // result. // @see https://github.com/dequelabs/axe-core/issues/2924 - bgColors.unshift(new Color(255, 255, 255, 1)); var colors = bgColors.reduce((bgColor, fgColor) => { return flattenColors(fgColor, bgColor); }); return colors; } -export default getBackgroundColor; +/** + * Determine if element is partially overlapped, triggering a Can't Tell result + * @private + * @param {Element} elm + * @param {Element} bgElm + * @param {Object} bgColor + * @return {Boolean} + */ +function elmPartiallyObscured(elm, bgElm, bgColor) { + var obscured = + elm !== bgElm && !visuallyContains(elm, bgElm) && bgColor.alpha !== 0; + if (obscured) { + incompleteData.set('bgColor', 'elmPartiallyObscured'); + } + return obscured; +} + +/** + * Get the page background color. + * @private + * @param {Element} elm + * @param {Boolean} stackContainsBody + * @return {Colors[]} + */ +function getPageBackgroundColors(elm, stackContainsBody) { + const pageColors = []; + + // 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 need to determine if we should use the + // body background or the html background color + if (!stackContainsBody) { + // if the html element defines a bgColor and body defines a + // bgColor but body's 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 ( + (bodyBgColor.alpha !== 0 && htmlBgColor.alpha === 0) || + (bodyBgColorApplies && bodyBgColor.alpha !== 1) + ) { + pageColors.unshift(bodyBgColor); + } + + if ( + htmlBgColor.alpha !== 0 && + (!bodyBgColorApplies || (bodyBgColorApplies && bodyBgColor.alpha !== 1)) + ) { + pageColors.unshift(htmlBgColor); + } + } + + // default page background is white + pageColors.unshift(new Color(255, 255, 255, 1)); + + return pageColors; +} diff --git a/lib/commons/color/get-background-stack.js b/lib/commons/color/get-background-stack.js index cfe0721976..d06d071564 100644 --- a/lib/commons/color/get-background-stack.js +++ b/lib/commons/color/get-background-stack.js @@ -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 html background as the lowest element + bgNodes.push(document.documentElement); + } } return bgNodes; } diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js index f22ac33fd1..6770dac8b5 100644 --- a/test/commons/color/get-background-color.js +++ b/test/commons/color/get-background-color.js @@ -4,9 +4,18 @@ describe('color.getBackgroundColor', function() { var fixture = document.getElementById('fixture'); var shadowSupported = axe.testUtils.shadowSupport.v1; + var origBodyBg; + var origHtmlBg; + + before(function() { + origBodyBg = document.body.style.background; + origHtmlBg = document.documentElement.style.background; + }); afterEach(function() { - document.getElementById('fixture').innerHTML = ''; + document.body.style.background = origBodyBg; + document.documentElement.style.background = origHtmlBg; + axe.commons.color.incompleteData.clear(); axe._tree = undefined; }); @@ -635,7 +644,6 @@ describe('color.getBackgroundColor', function() { 'style="z-index:-1; position:absolute; width:100%; height:2em; background: #000">' + '