Skip to content

Commit

Permalink
fix: handle contrast of multiline inline el's
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcy Sutton authored and marcysutton committed Dec 12, 2017
1 parent 7f8491e commit f9d565f
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 10 deletions.
2 changes: 2 additions & 0 deletions lib/checks/color/color-contrast.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"bgOverlap": "Element's background color could not be determined because it is overlapped by another element",
"fgAlpha" : "Element's foreground color could not be determined because of alpha transparency",
"elmPartiallyObscured": "Element's background color could not be determined because it's partially obscured by another element",
"elmPartiallyObscuring": "Element's background color could not be determined because it partially overlaps other elements",
"outsideViewport": "Element's background color could not be determined because it's outside the viewport",
"equalRatio": "Element has a 1:1 contrast ratio with the background",
"default": "Unable to determine contrast ratio"
}
Expand Down
104 changes: 94 additions & 10 deletions lib/commons/color/get-background-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,19 +178,15 @@ function sortPageBackground(elmStack) {
}
return bgNodes;
}

/**
* Get all elements rendered underneath the current element, In the order they are displayed (front to back)
* @method getBackgroundStack
* Get coordinates for an element's client rects or bounding client rect
* @method getCoords
* @memberof axe.commons.color
* @instance
* @param {Element} elm
* @return {Array}
* @param {DOMRect} rect
* @return {Object}
*/
color.getBackgroundStack = function(elm) {
// allows inline elements spanning multiple lines to be evaluated
let multipleRects = elm.getClientRects();
let rect = multipleRects.length > 1 ? multipleRects[0] : elm.getBoundingClientRect();
color.getCoords = function(rect) {
let x, y;
if (rect.left > window.innerWidth) {
return;
Expand All @@ -205,7 +201,95 @@ color.getBackgroundStack = function(elm) {
Math.ceil(rect.top + (rect.height / 2)),
window.innerHeight - 1);

let elmStack = Array.from(document.elementsFromPoint(x, y));
return {x, y};
};
/**
* Get elements from point for block and inline elements, excluding line breaks
* @method getRectStack
* @memberof axe.commons.color
* @instance
* @param {Element} elm
* @return {Array}
*/
color.getRectStack = function(elm) {
let boundingCoords = color.getCoords(elm.getBoundingClientRect());
if (boundingCoords) {
// 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
return rect.width && rect.width > 0;
})
.map((rect) => {
let coords = color.getCoords(rect);
if (coords) {
return Array.from(document.elementsFromPoint(coords.x, coords.y));
}
});
// add bounding client rect stack for comparison later
filteredArr.splice(0, 0, boundingStack);
return filteredArr;
} else {
return [boundingStack];
}
}
return null;
};
/**
* Get filtered stack of block and inline elements, excluding line breaks
* @method filteredRectStack
* @memberof axe.commons.color
* @instance
* @param {Element} elm
* @return {Array}
*/
color.filteredRectStack = function(elm) {
let rectStack = color.getRectStack(elm);
if (rectStack && rectStack.length === 1) {
// default case, elm.getBoundingClientRect()
return rectStack[0];
}
else if (rectStack && rectStack.length > 1) {
let boundingStack = rectStack.shift();
let isSame;
// iterating over arrays of DOMRects
rectStack.forEach((rectList, index) => {
if (index === 0) { return; }
// if the stacks are the same, use the first one. otherwise, return null.
let rectA = rectStack[index - 1],
rectB = rectStack[index];

// if elements in clientRects are the same
// or the boundingClientRect contains the differing element, pass it
isSame = rectA.every(function(element, elementIndex) {
return element === rectB[elementIndex];
}) || boundingStack.includes(elm);
});
if (!isSame) {
axe.commons.color.incompleteData.set('bgColor', 'elmPartiallyObscuring');
return null;
}
// pass the first stack if it wasn't partially covered
return rectStack[0];
} else {
// rect outside of viewport
axe.commons.color.incompleteData.set('bgColor', 'outsideViewport');
return null;
}
};
/**
* Get all elements rendered underneath the current element, In the order they are displayed (front to back)
* @method getBackgroundStack
* @memberof axe.commons.color
* @instance
* @param {Element} elm
* @return {Array}
*/
color.getBackgroundStack = function(elm) {
let elmStack = color.filteredRectStack(elm);
if (elmStack === null) { return null; }
elmStack = includeMissingElements(elmStack, elm);
elmStack = dom.reduceToElementsBelowFloating(elmStack, elm);
elmStack = sortPageBackground(elmStack);
Expand Down
33 changes: 33 additions & 0 deletions test/commons/color/get-background-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,39 @@ describe('color.getBackgroundColor', function () {
assert.deepEqual(bgNodes, [target]);
});

it('should return a bgcolor for a multiline inline element fully covering the background', function () {
fixture.innerHTML = '<div style="position:relative;">' +
'<div style="background-color:rgba(0,0,0,1);position:absolute;width:300px;height:200px;"></div>' +
'<p style="position: relative;z-index:1;">Text oh heyyyy <a href="#" id="target">and here\'s <br>a link</a></p>' +
'</div>';
var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []);
assert.isNotNull(actual);
assert.equal(Math.round(actual.blue), 0);
assert.equal(Math.round(actual.red), 0);
assert.equal(Math.round(actual.green), 0);
});

it('should return null for a multiline block element not fully covering the background', function () {
fixture.innerHTML = '<div style="position:relative;">' +
'<div id="background" style="background-color:rgba(0,0,0,1); position:absolute; width:300px; height:20px;"></div>' +
'<p style="position:relative; z-index:1; width:300px;" id="target">Text content Text content Text content '+
'Text content Text content Text content</p>' +
'</div>';
var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []);
assert.isNull(actual);
assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'elmPartiallyObscured');
});

it('should return null if a multiline inline element does not fully cover background', function () {
fixture.innerHTML = '<div style="position:relative;">' +
'<div style="background-color:rgba(0,0,0,1);position:absolute;width:300px;height:20px;"></div>' +
'<p style="position: relative;z-index:1;">Text oh heyyyy <a href="#" id="target">and here\'s <br>a link</a></p>' +
'</div>';
var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []);
assert.isNull(actual);
assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'elmPartiallyObscuring');
});

it('should count a TR as a background element for TD', function () {
fixture.innerHTML = '<div style="background-color:#007acc;">' +
'<table style="width:100%">' +
Expand Down

0 comments on commit f9d565f

Please sign in to comment.