diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index a4a9593712..ab93cc0124 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -57,7 +57,7 @@ function contentOverlapping(targetElement, bgNode) { // get content box of target element // check to see if the current bgNode is overlapping var targetRect = targetElement.getClientRects()[0]; - var obscuringElements = document.elementsFromPoint(targetRect.left, targetRect.top); + var obscuringElements = dom.shadowElementsFromPoint(targetRect.left, targetRect.top); if (obscuringElements) { for(var i = 0; i < obscuringElements.length; i++) { if (obscuringElements[i] !== targetElement && obscuringElements[i] === bgNode) { @@ -204,7 +204,7 @@ color.getCoords = function(rect) { return {x, y}; }; /** - * Get elements from point for block and inline elements, excluding line breaks + * Get relevant stacks of block and inline elements, excluding line breaks * @method getRectStack * @memberof axe.commons.color * @instance @@ -214,9 +214,9 @@ color.getCoords = function(rect) { color.getRectStack = function(elm) { let boundingCoords = color.getCoords(elm.getBoundingClientRect()); if (boundingCoords) { + let boundingStack = dom.shadowElementsFromPoint(boundingCoords.x, boundingCoords.y); // allows inline elements spanning multiple lines to be evaluated let rects = Array.from(elm.getClientRects()); - let boundingStack = Array.from(document.elementsFromPoint(boundingCoords.x, boundingCoords.y)); if (rects && rects.length > 1) { let filteredArr = rects.filter((rect) => { // exclude manual line breaks in Chrome/Safari @@ -225,7 +225,7 @@ color.getRectStack = function(elm) { .map((rect) => { let coords = color.getCoords(rect); if (coords) { - return Array.from(document.elementsFromPoint(coords.x, coords.y)); + return dom.shadowElementsFromPoint(coords.x, coords.y); } }); // add bounding client rect stack for comparison later diff --git a/lib/commons/dom/shadow-elements-from-point.js b/lib/commons/dom/shadow-elements-from-point.js new file mode 100644 index 0000000000..343c1108a5 --- /dev/null +++ b/lib/commons/dom/shadow-elements-from-point.js @@ -0,0 +1,28 @@ +/* global axe, dom */ +/** + * Get elements from point across shadow dom boundaries + * @method shadowElementsFromPoint + * @memberof axe.commons.dom + * @instance + * @param {Number} nodeX X coordinates of point + * @param {Number} nodeY Y coordinates of point + * @param {Object} [root] Shadow root or document root + * @return {Array} + */ +dom.shadowElementsFromPoint = function(nodeX, nodeY, root = document) { + return root.elementsFromPoint(nodeX, nodeY) + .reduce((stack, elm) => { + if (axe.utils.isShadowRoot(elm)) { + const shadowStack = dom.shadowElementsFromPoint(nodeX, nodeY, elm.shadowRoot); + stack = stack.concat(shadowStack); + // filter host nodes which get included regardless of overlap + // TODO: refactor multiline overlap checking inside shadow dom + if (stack.length && axe.commons.dom.visuallyContains(stack[0], elm)) { + stack.push(elm); + } + } else { + stack.push(elm); + } + return stack; + }, []); +}; diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js index ec618bbcb1..60d302daf3 100644 --- a/lib/core/utils/flattened-tree.js +++ b/lib/core/utils/flattened-tree.js @@ -51,20 +51,6 @@ function getSlotChildren(node) { return retVal; } -const possibleShadowRoots = ['article', 'aside', 'blockquote', - 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'header', 'main', 'nav', 'p', 'section', 'span']; -axe.utils.isShadowRoot = function isShadowRoot (node) { - const nodeName = node.nodeName.toLowerCase(); - if (node.shadowRoot) { - if (/^[a-z][a-z0-9_.-]*-[a-z0-9_.-]*$/.test(nodeName) || - possibleShadowRoots.includes(nodeName)) { - return true; - } - } - return false; -}; - /** * Recursvely returns an array of the virtual DOM nodes at this level * excluding comment nodes and the shadow DOM nodes and diff --git a/lib/core/utils/is-shadow-root.js b/lib/core/utils/is-shadow-root.js new file mode 100644 index 0000000000..7699147fda --- /dev/null +++ b/lib/core/utils/is-shadow-root.js @@ -0,0 +1,21 @@ +/* global axe */ + +const possibleShadowRoots = ['article', 'aside', 'blockquote', + 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'header', 'main', 'nav', 'p', 'section', 'span']; +/** + * Test a node to see if it has a spec-conforming shadow root + * + * @param {Node} node The HTML DOM node + * @return {Boolean} + */ +axe.utils.isShadowRoot = function isShadowRoot (node) { + const nodeName = node.nodeName.toLowerCase(); + if (node.shadowRoot) { + if (/^[a-z][a-z0-9_.-]*-[a-z0-9_.-]*$/.test(nodeName) || + possibleShadowRoots.includes(nodeName)) { + return true; + } + } + return false; +}; diff --git a/test/checks/color/color-contrast.js b/test/checks/color/color-contrast.js index 625842a616..eab3f2fd11 100644 --- a/test/checks/color/color-contrast.js +++ b/test/checks/color/color-contrast.js @@ -3,6 +3,8 @@ describe('color-contrast', function () { var fixture = document.getElementById('fixture'); var fixtureSetup = axe.testUtils.fixtureSetup; + var shadowSupported = axe.testUtils.shadowSupport.v1; + var shadowCheckSetup = axe.testUtils.shadowCheckSetup; var checkContext = { _relatedNodes: [], @@ -289,4 +291,16 @@ describe('color-contrast', function () { document.querySelector('#background') ); }); + + (shadowSupported ? it : xit) + ('returns colors across Shadow DOM boundaries', function () { + var params = shadowCheckSetup( + '
', + '

Text

' + ); + var container = fixture.querySelector('#container'); + var result = checks['color-contrast'].evaluate.apply(checkContext, params); + assert.isFalse(result); + assert.deepEqual(checkContext._relatedNodes, [container]); + }); }); diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js index a98e5786b0..36f452bf8c 100644 --- a/test/commons/color/get-background-color.js +++ b/test/commons/color/get-background-color.js @@ -3,6 +3,8 @@ describe('color.getBackgroundColor', function () { var fixture = document.getElementById('fixture'); + var shadowSupported = axe.testUtils.shadowSupport.v1; + afterEach(function () { document.getElementById('fixture').innerHTML = ''; axe.commons.color.incompleteData.clear(); @@ -233,6 +235,26 @@ describe('color.getBackgroundColor', function () { assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'elmPartiallyObscuring'); }); + it('should return an actual if an absolutely positioned element does not cover background', function () { + fixture.innerHTML = '
' + + '
Text
' + + '
'; + var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []); + assert.equal(Math.round(actual.blue), 255); + assert.equal(Math.round(actual.red), 255); + assert.equal(Math.round(actual.green), 255); + }); + + it('should return null if an absolutely positioned element partially obsures background', function () { + fixture.innerHTML = '
' + + '
' + + '
Text
' + + '
'; + var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []); + assert.isNull(actual); + assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'elmPartiallyObscured'); + }); + it('should count a TR as a background element for TD', function () { fixture.innerHTML = '
' + '' + @@ -315,14 +337,14 @@ describe('color.getBackgroundColor', function () { }); it('should count an implicit label as a background element', function () { - fixture.innerHTML = '