From 87197005d046cc8c845764ff9107683938864c65 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 18 Nov 2019 10:11:34 -0700 Subject: [PATCH] fix(color-contrast): take into account parent opacity for foreground color (#1902) * fix(color-contrast): take into account parent opacity for foreground color * fix rounding errors * proper closeTo params * update * test recursive --- lib/checks/color/color-contrast.js | 2 +- lib/commons/color/get-foreground-color.js | 41 +++++++++++++++++--- test/commons/color/get-foreground-color.js | 44 ++++++++++++++++++++++ test/runner.tmpl | 1 + 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/lib/checks/color/color-contrast.js b/lib/checks/color/color-contrast.js index 2bc0f81f4d..24b37db82d 100644 --- a/lib/checks/color/color-contrast.js +++ b/lib/checks/color/color-contrast.js @@ -7,7 +7,7 @@ if (!dom.isVisible(node, false)) { const noScroll = !!(options || {}).noScroll; const bgNodes = []; const bgColor = color.getBackgroundColor(node, bgNodes, noScroll); -const fgColor = color.getForegroundColor(node, noScroll); +const fgColor = color.getForegroundColor(node, noScroll, bgColor); const nodeStyle = window.getComputedStyle(node); const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size')); diff --git a/lib/commons/color/get-foreground-color.js b/lib/commons/color/get-foreground-color.js index 12dd524206..353c544477 100644 --- a/lib/commons/color/get-foreground-color.js +++ b/lib/commons/color/get-foreground-color.js @@ -1,5 +1,30 @@ /* global axe, color */ +function getOpacity(node) { + if (!node) { + return 1; + } + + const vNode = axe.utils.getNodeFromTree(node); + + if (vNode && vNode._opacity !== undefined && vNode._opacity !== null) { + return vNode._opacity; + } + + const nodeStyle = window.getComputedStyle(node); + const opacity = nodeStyle.getPropertyValue('opacity'); + const finalOpacity = opacity * getOpacity(node.parentElement); + + // cache the results of the getOpacity check on the parent tree + // so we don't have to look at the parent tree again for all its + // descendants + if (vNode) { + vNode._opacity = finalOpacity; + } + + return finalOpacity; +} + /** * Returns the flattened foreground color of an element, or null if it can't be determined because * of transparency @@ -8,22 +33,26 @@ * @instance * @param {Element} node * @param {Boolean} noScroll (default false) + * @param {Color} bgColor * @return {Color|null} */ -color.getForegroundColor = function(node, noScroll) { - var nodeStyle = window.getComputedStyle(node); +color.getForegroundColor = function(node, noScroll, bgColor) { + const nodeStyle = window.getComputedStyle(node); - var fgColor = new color.Color(); + const fgColor = new color.Color(); fgColor.parseRgbString(nodeStyle.getPropertyValue('color')); - var opacity = nodeStyle.getPropertyValue('opacity'); + const opacity = getOpacity(node); fgColor.alpha = fgColor.alpha * opacity; if (fgColor.alpha === 1) { return fgColor; } - var bgColor = color.getBackgroundColor(node, [], noScroll); + if (!bgColor) { + bgColor = color.getBackgroundColor(node, [], noScroll); + } + if (bgColor === null) { - var reason = axe.commons.color.incompleteData.get('bgColor'); + const reason = axe.commons.color.incompleteData.get('bgColor'); axe.commons.color.incompleteData.set('fgColor', reason); return null; } diff --git a/test/commons/color/get-foreground-color.js b/test/commons/color/get-foreground-color.js index 6390ea4eca..d96c580388 100644 --- a/test/commons/color/get-foreground-color.js +++ b/test/commons/color/get-foreground-color.js @@ -1,3 +1,5 @@ +/* global sinon */ + describe('color.getForegroundColor', function() { 'use strict'; @@ -42,6 +44,39 @@ describe('color.getForegroundColor', function() { assert.equal(actual.alpha, expected.alpha); }); + it('should take into account parent opacity tree', function() { + fixture.innerHTML = + '
' + + '
' + + '
' + + 'This is my text' + + '
'; + var target = fixture.querySelector('#target'); + var actual = axe.commons.color.getForegroundColor(target); + var expected = new axe.commons.color.Color(119.5, 119.5, 119.5, 1); + assert.closeTo(actual.red, expected.red, 0.8); + assert.closeTo(actual.green, expected.green, 0.8); + assert.closeTo(actual.blue, expected.blue, 0.8); + assert.closeTo(actual.alpha, expected.alpha, 0.1); + }); + + it('should take into account entire parent opacity tree', function() { + fixture.innerHTML = + '
' + + '
' + + '
' + + '
' + + 'This is my text' + + '
'; + var target = fixture.querySelector('#target'); + var actual = axe.commons.color.getForegroundColor(target); + var expected = new axe.commons.color.Color(119.5, 119.5, 119.5, 1); + assert.closeTo(actual.red, expected.red, 0.8); + assert.closeTo(actual.green, expected.green, 0.8); + assert.closeTo(actual.blue, expected.blue, 0.8); + assert.closeTo(actual.alpha, expected.alpha, 0.1); + }); + it('should return null if containing parent has a background image and is non-opaque', function() { fixture.innerHTML = '
+