From fbc581d77e457fe092ecc2b95015e667292f1a08 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Wed, 13 Oct 2021 13:36:02 -0600 Subject: [PATCH] fix(color-contrast): correctly apply page background color (#3207) * fixes * finalize * sort * test * fix tests * Update lib/commons/color/get-background-stack.js Co-authored-by: Wilco Fiers * fixes * fix(color-contrast): correctly apply page background color Co-authored-by: Wilco Fiers --- lib/commons/color/get-background-color.js | 111 ++++++++++--- lib/commons/color/get-background-stack.js | 42 ++--- test/commons/color/get-background-color.js | 184 ++++++++++++++------- 3 files changed, 236 insertions(+), 101 deletions(-) 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">' + '
Some text
'; - var orig = document.body.style.background; document.body.style.background = '#FFF'; axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( @@ -649,65 +657,23 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.green, expected.green, 0.5); assert.closeTo(actual.blue, expected.blue, 0.5); assert.closeTo(actual.alpha, expected.alpha, 0.1); - - document.body.style.background = orig; - }); - - it('returns the body background', function() { - fixture.innerHTML = '
elm
'; - var orig = document.body.style.background; - document.body.style.background = '#F00'; - - axe.testUtils.flatTreeSetup(fixture); - var actual = axe.commons.color.getBackgroundColor( - document.getElementById('target'), - [] - ); - var expected = new axe.commons.color.Color(255, 0, 0, 1); - document.body.style.background = orig; - - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('returns the body background even when the body is MUCH larger than the screen', function() { - fixture.innerHTML = '
elm
'; - var orig = document.body.style.background; - document.body.style.background = '#F00'; - - axe.testUtils.flatTreeSetup(fixture); - var actual = axe.commons.color.getBackgroundColor( - document.getElementById('target'), - [] - ); - var expected = new axe.commons.color.Color(255, 0, 0, 1); - document.body.style.background = orig; - - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); - }); + it('should return null for negative z-index element when html and body have a background', function() { + fixture.innerHTML = + '
' + + '
'; - it('returns the html background', function() { - fixture.innerHTML = '
'; - var orig = document.documentElement.style.background; document.documentElement.style.background = '#0F0'; - + document.body.style.background = '#FFF'; axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] ); - var expected = new axe.commons.color.Color(0, 255, 0, 1); - document.documentElement.style.background = orig; - assert.closeTo(actual.red, expected.red, 0.5); - assert.closeTo(actual.green, expected.green, 0.5); - assert.closeTo(actual.blue, expected.blue, 0.5); - assert.closeTo(actual.alpha, expected.alpha, 0.1); + assert.isNull(actual); }); it('should return background color for inline elements that do not fit the viewport', function() { @@ -747,7 +713,7 @@ describe('color.getBackgroundColor', function() { // size body element so that target element is positioned outside of background var originalHeight = document.body.style.height; - var originalBg = document.body.style.background; + var originalMargin = document.body.style.margin; document.body.style.height = '1px'; document.body.style.background = '#000'; document.body.style.margin = 0; @@ -762,7 +728,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, 1, 0); document.body.style.height = originalHeight; - document.body.style.background = originalBg; + document.body.style.margin = originalMargin; }); it('should return the html canvas bgColor when element content does not overlap with body', function() { @@ -771,8 +737,6 @@ describe('color.getBackgroundColor', function() { // size body element so that target element is positioned outside of background var originalHeight = document.body.style.height; - var originalBg = document.body.style.background; - var originalRootBg = document.documentElement.style.background; document.body.style.height = '1px'; document.body.style.background = '#0f0'; document.documentElement.style.background = '#f00'; @@ -787,8 +751,6 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, 1, 0); document.body.style.height = originalHeight; - document.body.style.background = originalBg; - document.documentElement.style.background = originalRootBg; }); (shadowSupported ? it : xit)('finds colors in shadow boundaries', function() { @@ -1029,4 +991,112 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.blue, expected.blue, 0.5); assert.closeTo(actual.alpha, expected.alpha, 0.1); }); + + describe('body and document', function() { + it('returns the body background', function() { + fixture.innerHTML = '
elm
'; + document.body.style.background = '#F00'; + + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor( + document.getElementById('target'), + [] + ); + var expected = new axe.commons.color.Color(255, 0, 0, 1); + + assert.closeTo(actual.red, expected.red, 0.5); + assert.closeTo(actual.green, expected.green, 0.5); + assert.closeTo(actual.blue, expected.blue, 0.5); + assert.closeTo(actual.alpha, expected.alpha, 0.1); + }); + + it('returns the body background even when the body is MUCH larger than the screen', function() { + fixture.innerHTML = '
elm
'; + document.body.style.background = '#F00'; + + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor( + document.getElementById('target'), + [] + ); + var expected = new axe.commons.color.Color(255, 0, 0, 1); + + assert.closeTo(actual.red, expected.red, 0.5); + assert.closeTo(actual.green, expected.green, 0.5); + assert.closeTo(actual.blue, expected.blue, 0.5); + assert.closeTo(actual.alpha, expected.alpha, 0.1); + }); + + it('returns the html background', function() { + fixture.innerHTML = '
'; + document.documentElement.style.background = '#0F0'; + + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor( + document.getElementById('target'), + [] + ); + var expected = new axe.commons.color.Color(0, 255, 0, 1); + + assert.closeTo(actual.red, expected.red, 0.5); + assert.closeTo(actual.green, expected.green, 0.5); + assert.closeTo(actual.blue, expected.blue, 0.5); + assert.closeTo(actual.alpha, expected.alpha, 0.1); + }); + + it('returns the html background when body does not cover the element', function() { + fixture.innerHTML = + '
'; + document.documentElement.style.background = '#0F0'; + document.body.style.background = '#00F'; + + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor( + document.getElementById('target'), + [] + ); + var expected = new axe.commons.color.Color(0, 255, 0, 1); + + assert.closeTo(actual.red, expected.red, 0.5); + assert.closeTo(actual.green, expected.green, 0.5); + assert.closeTo(actual.blue, expected.blue, 0.5); + assert.closeTo(actual.alpha, expected.alpha, 0.1); + }); + + it('returns the body background when body does cover the element', function() { + fixture.innerHTML = '
'; + document.documentElement.style.background = '#0F0'; + document.body.style.background = '#00F'; + + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor( + document.getElementById('target'), + [] + ); + var expected = new axe.commons.color.Color(0, 0, 255, 1); + + assert.closeTo(actual.red, expected.red, 0.5); + assert.closeTo(actual.green, expected.green, 0.5); + assert.closeTo(actual.blue, expected.blue, 0.5); + assert.closeTo(actual.alpha, expected.alpha, 0.1); + }); + + it('returns both the html and body background if the body has alpha', function() { + fixture.innerHTML = '
'; + document.documentElement.style.background = '#0F0'; + document.body.style.background = 'rgba(0, 0, 255, 0.5)'; + + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor( + document.getElementById('target'), + [] + ); + var expected = new axe.commons.color.Color(0, 128, 128, 1); + + assert.closeTo(actual.red, expected.red, 0.5); + assert.closeTo(actual.green, expected.green, 0.5); + assert.closeTo(actual.blue, expected.blue, 0.5); + assert.closeTo(actual.alpha, expected.alpha, 0.1); + }); + }); });