Skip to content

Commit f9d565f

Browse files
Marcy Suttonmarcysutton
authored andcommitted
fix: handle contrast of multiline inline el's
1 parent 7f8491e commit f9d565f

File tree

3 files changed

+129
-10
lines changed

3 files changed

+129
-10
lines changed

lib/checks/color/color-contrast.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"bgOverlap": "Element's background color could not be determined because it is overlapped by another element",
1414
"fgAlpha" : "Element's foreground color could not be determined because of alpha transparency",
1515
"elmPartiallyObscured": "Element's background color could not be determined because it's partially obscured by another element",
16+
"elmPartiallyObscuring": "Element's background color could not be determined because it partially overlaps other elements",
17+
"outsideViewport": "Element's background color could not be determined because it's outside the viewport",
1618
"equalRatio": "Element has a 1:1 contrast ratio with the background",
1719
"default": "Unable to determine contrast ratio"
1820
}

lib/commons/color/get-background-color.js

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,19 +178,15 @@ function sortPageBackground(elmStack) {
178178
}
179179
return bgNodes;
180180
}
181-
182181
/**
183-
* Get all elements rendered underneath the current element, In the order they are displayed (front to back)
184-
* @method getBackgroundStack
182+
* Get coordinates for an element's client rects or bounding client rect
183+
* @method getCoords
185184
* @memberof axe.commons.color
186185
* @instance
187-
* @param {Element} elm
188-
* @return {Array}
186+
* @param {DOMRect} rect
187+
* @return {Object}
189188
*/
190-
color.getBackgroundStack = function(elm) {
191-
// allows inline elements spanning multiple lines to be evaluated
192-
let multipleRects = elm.getClientRects();
193-
let rect = multipleRects.length > 1 ? multipleRects[0] : elm.getBoundingClientRect();
189+
color.getCoords = function(rect) {
194190
let x, y;
195191
if (rect.left > window.innerWidth) {
196192
return;
@@ -205,7 +201,95 @@ color.getBackgroundStack = function(elm) {
205201
Math.ceil(rect.top + (rect.height / 2)),
206202
window.innerHeight - 1);
207203

208-
let elmStack = Array.from(document.elementsFromPoint(x, y));
204+
return {x, y};
205+
};
206+
/**
207+
* Get elements from point for block and inline elements, excluding line breaks
208+
* @method getRectStack
209+
* @memberof axe.commons.color
210+
* @instance
211+
* @param {Element} elm
212+
* @return {Array}
213+
*/
214+
color.getRectStack = function(elm) {
215+
let boundingCoords = color.getCoords(elm.getBoundingClientRect());
216+
if (boundingCoords) {
217+
// allows inline elements spanning multiple lines to be evaluated
218+
let rects = Array.from(elm.getClientRects());
219+
let boundingStack = Array.from(document.elementsFromPoint(boundingCoords.x, boundingCoords.y));
220+
if (rects && rects.length > 1) {
221+
let filteredArr = rects.filter((rect) => {
222+
// exclude manual line breaks in Chrome/Safari
223+
return rect.width && rect.width > 0;
224+
})
225+
.map((rect) => {
226+
let coords = color.getCoords(rect);
227+
if (coords) {
228+
return Array.from(document.elementsFromPoint(coords.x, coords.y));
229+
}
230+
});
231+
// add bounding client rect stack for comparison later
232+
filteredArr.splice(0, 0, boundingStack);
233+
return filteredArr;
234+
} else {
235+
return [boundingStack];
236+
}
237+
}
238+
return null;
239+
};
240+
/**
241+
* Get filtered stack of block and inline elements, excluding line breaks
242+
* @method filteredRectStack
243+
* @memberof axe.commons.color
244+
* @instance
245+
* @param {Element} elm
246+
* @return {Array}
247+
*/
248+
color.filteredRectStack = function(elm) {
249+
let rectStack = color.getRectStack(elm);
250+
if (rectStack && rectStack.length === 1) {
251+
// default case, elm.getBoundingClientRect()
252+
return rectStack[0];
253+
}
254+
else if (rectStack && rectStack.length > 1) {
255+
let boundingStack = rectStack.shift();
256+
let isSame;
257+
// iterating over arrays of DOMRects
258+
rectStack.forEach((rectList, index) => {
259+
if (index === 0) { return; }
260+
// if the stacks are the same, use the first one. otherwise, return null.
261+
let rectA = rectStack[index - 1],
262+
rectB = rectStack[index];
263+
264+
// if elements in clientRects are the same
265+
// or the boundingClientRect contains the differing element, pass it
266+
isSame = rectA.every(function(element, elementIndex) {
267+
return element === rectB[elementIndex];
268+
}) || boundingStack.includes(elm);
269+
});
270+
if (!isSame) {
271+
axe.commons.color.incompleteData.set('bgColor', 'elmPartiallyObscuring');
272+
return null;
273+
}
274+
// pass the first stack if it wasn't partially covered
275+
return rectStack[0];
276+
} else {
277+
// rect outside of viewport
278+
axe.commons.color.incompleteData.set('bgColor', 'outsideViewport');
279+
return null;
280+
}
281+
};
282+
/**
283+
* Get all elements rendered underneath the current element, In the order they are displayed (front to back)
284+
* @method getBackgroundStack
285+
* @memberof axe.commons.color
286+
* @instance
287+
* @param {Element} elm
288+
* @return {Array}
289+
*/
290+
color.getBackgroundStack = function(elm) {
291+
let elmStack = color.filteredRectStack(elm);
292+
if (elmStack === null) { return null; }
209293
elmStack = includeMissingElements(elmStack, elm);
210294
elmStack = dom.reduceToElementsBelowFloating(elmStack, elm);
211295
elmStack = sortPageBackground(elmStack);

test/commons/color/get-background-color.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,39 @@ describe('color.getBackgroundColor', function () {
192192
assert.deepEqual(bgNodes, [target]);
193193
});
194194

195+
it('should return a bgcolor for a multiline inline element fully covering the background', function () {
196+
fixture.innerHTML = '<div style="position:relative;">' +
197+
'<div style="background-color:rgba(0,0,0,1);position:absolute;width:300px;height:200px;"></div>' +
198+
'<p style="position: relative;z-index:1;">Text oh heyyyy <a href="#" id="target">and here\'s <br>a link</a></p>' +
199+
'</div>';
200+
var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []);
201+
assert.isNotNull(actual);
202+
assert.equal(Math.round(actual.blue), 0);
203+
assert.equal(Math.round(actual.red), 0);
204+
assert.equal(Math.round(actual.green), 0);
205+
});
206+
207+
it('should return null for a multiline block element not fully covering the background', function () {
208+
fixture.innerHTML = '<div style="position:relative;">' +
209+
'<div id="background" style="background-color:rgba(0,0,0,1); position:absolute; width:300px; height:20px;"></div>' +
210+
'<p style="position:relative; z-index:1; width:300px;" id="target">Text content Text content Text content '+
211+
'Text content Text content Text content</p>' +
212+
'</div>';
213+
var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []);
214+
assert.isNull(actual);
215+
assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'elmPartiallyObscured');
216+
});
217+
218+
it('should return null if a multiline inline element does not fully cover background', function () {
219+
fixture.innerHTML = '<div style="position:relative;">' +
220+
'<div style="background-color:rgba(0,0,0,1);position:absolute;width:300px;height:20px;"></div>' +
221+
'<p style="position: relative;z-index:1;">Text oh heyyyy <a href="#" id="target">and here\'s <br>a link</a></p>' +
222+
'</div>';
223+
var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []);
224+
assert.isNull(actual);
225+
assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'elmPartiallyObscuring');
226+
});
227+
195228
it('should count a TR as a background element for TD', function () {
196229
fixture.innerHTML = '<div style="background-color:#007acc;">' +
197230
'<table style="width:100%">' +

0 commit comments

Comments
 (0)