Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color fixes 2x #639

Merged
merged 8 commits into from
Dec 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
123 changes: 105 additions & 18 deletions lib/commons/color/get-background-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +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) {
let rect = elm.getBoundingClientRect();
color.getCoords = function(rect) {
let x, y;
if (rect.left > window.innerWidth) {
return;
Expand All @@ -203,7 +201,94 @@ color.getBackgroundStack = function(elm) {
Math.ceil(rect.top + (rect.height / 2)),
window.innerHeight - 1);

let elmStack = 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 All @@ -217,18 +302,20 @@ color.getBackgroundStack = function(elm) {
}
return elmIndex !== -1 ? elmStack : null;
};

/**
* Returns background color for element
* Returns a background color for an element, if one exists
* Uses color.getBackgroundStack() to get all elements rendered underneath the current element to
* help determine the background color.
* @param {Element} elm Element to determine background color
* @param {Array} [bgElms=[]] [description]
* @param {Boolean} [noScroll=false] [description]
* @return {Color} [description]
*/
* @method getBackgroundColor
* @memberof axe.commons.color
* @instance
* @param {Element} elm The node under test
* @param {Array} [bgElms=[]] An array to fill with background stack elements
* @param {Boolean} [noScroll=false] Prevent scrolling in overflow:hidden containers
* @return {Color|null}
**/
color.getBackgroundColor = function(elm, bgElms = [], noScroll = false) {
if(noScroll !== true) {
if (noScroll !== true) {
// Avoid scrolling overflow:hidden containers, by only aligning to top
// when not doing so would move the center point above the viewport top.
const alignToTop = elm.clientHeight - 2 >= window.innerHeight * 2;
Expand Down Expand Up @@ -269,8 +356,8 @@ color.getBackgroundColor = function(elm, bgElms = [], noScroll = false) {

if (bgColors !== null && elmStack !== null) {
// Mix the colors together, on top of a default white
bgColors.push( new color.Color(255, 255, 255, 1));
var colors = bgColors.reduce( color.flattenColors);
bgColors.push(new color.Color(255, 255, 255, 1));
var colors = bgColors.reduce(color.flattenColors);
return colors;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/rules/color-contrast-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ if (nodeName === 'LABEL' || nodeParentLabel) {
return false;
}

var candidate = node.querySelector('input:not([type="hidden"]):not([type="image"])' +
var candidate = relevantNode.querySelector('input:not([type="hidden"]):not([type="image"])' +
':not([type="button"]):not([type="submit"]):not([type="reset"]), select, textarea');
if (candidate && candidate.disabled) {
return false;
Expand Down
35 changes: 33 additions & 2 deletions test/checks/color/color-contrast.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,33 @@ describe('color-contrast', function () {
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should return true for inline elements with sufficient contrast spanning multiple lines', function () {
fixture.innerHTML = '<p>Text oh heyyyy <a href="#" id="target">and here\'s <br>a link</a></p>';
var target = fixture.querySelector('#target');
if (window.PHANTOMJS) {
assert.ok('PhantomJS is a liar');
} else {
assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target));
assert.deepEqual(checkContext._relatedNodes, []);
}
});

it('should return undefined for inline elements spanning multiple lines that are overlapped', function () {
fixture.innerHTML = '<div style="position:relative;"><div style="background-color:rgba(0,0,0,1);position:absolute;width:300px;height:200px;"></div>' +
'<p>Text oh heyyyy <a href="#" id="target">and here\'s <br>a link</a></p></div>';
var target = fixture.querySelector('#target');
assert.isUndefined(checks['color-contrast'].evaluate.call(checkContext, target));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should return true for inline elements with sufficient contrast', function () {
fixture.innerHTML = '<p>Text oh heyyyy <b id="target">and here\'s bold text</b></p>';
var target = fixture.querySelector('#target');
var result = checks['color-contrast'].evaluate.call(checkContext, target);
assert.isTrue(result);
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should return false when there is not sufficient contrast', function () {
fixture.innerHTML = '<div style="color: yellow; background-color: white;" id="target">' +
'My text</div>';
Expand Down Expand Up @@ -178,8 +205,12 @@ describe('color-contrast', function () {
fixture.innerHTML = '<label id="target">' +
'My text <input type="text"></label>';
var target = fixture.querySelector('#target');
var result = checks['color-contrast'].evaluate.call(checkContext, target);
assert.isTrue(result);
if (window.PHANTOMJS) {
assert.ok('PhantomJS is a liar');
} else {
var result = checks['color-contrast'].evaluate.call(checkContext, target);
assert.isTrue(result);
}
});

it('should return true when a label wraps a text input but doesn\'t overlap', function () {
Expand Down
26 changes: 26 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,32 @@ 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'), []);
if (window.PHANTOMJS) {
assert.ok('PhantomJS is a liar');
} else {
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 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
5 changes: 0 additions & 5 deletions test/integration/rules/color-contrast/color-contrast.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
<div style="background-color: rgba(255, 255, 255, 0.1); color: white;" id="pass5">Pass.</div>
</div>

<label id="pass6">
Default label
<input type="text">
</label>

<div style="position:relative; height: 40px;">
<label style="background-color:black; color: white;" id="pass7">
Label
Expand Down
2 changes: 0 additions & 2 deletions test/integration/rules/color-contrast/color-contrast.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
["#pass3"],
["#pass4"],
["#pass5"],
["#pass6"],
["#pass6 > input[type=\"text\"]"],
["#pass7"],
["#pass7 > input"]
],
Expand Down
10 changes: 10 additions & 0 deletions test/rule-matches/color-contrast-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,17 @@ describe('color-contrast-matches', function () {
fixture.innerHTML = '<input type="text" disabled>';
var target = fixture.querySelector('input');
assert.isFalse(rule.matches(target));
});

it('should not match a disabled implicit label child', function () {
fixture.innerHTML = '<label>' +
'<input type="checkbox" style="position: absolute;display: inline-block;width: 1.5rem;height: 1.5rem;opacity: 0;" disabled checked>' +
'<span style="background-color:rgba(0, 0, 0, 0.26);display:inline-block;width: 1.5rem;height: 1.5rem;" aria-hidden="true"></span>' +
'<span style="color:rgba(0, 0, 0, 0.38);" id="target">Baseball</span>' +
'</label>';
var target = fixture.querySelector('#target');
var result = rule.matches(target);
assert.isFalse(result);
});

it('should not match <textarea disabled>', function () {
Expand Down