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

feat(color-contrast, utils): add more options to color-contrast, add utils.deepMerge, deprecate commons.color.hasValidContrastRatio #2256

Merged
merged 11 commits into from
Jul 8, 2020
51 changes: 35 additions & 16 deletions lib/checks/color/color-contrast-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,25 @@ import {
import {
getBackgroundColor,
getForegroundColor,
hasValidContrastRatio,
incompleteData
incompleteData,
getContrast
} from '../../commons/color';

function colorContrastEvaluate(node, options = {}, virtualNode) {
function colorContrastEvaluate(node, options, virtualNode) {
if (!isVisible(node, false)) {
return true;
}

const {
ignoreUnicode,
ignoreLength,
boldValue,
boldPt,
nonBoldPt,
straker marked this conversation as resolved.
Show resolved Hide resolved
contrastRatio
} = options;

const visibleText = visibleVirtual(virtualNode, false, true);
const ignoreUnicode = !!options.ignoreUnicode;
const textContainsOnlyUnicode =
hasUnicode(visibleText, {
nonBmp: true
Expand All @@ -34,21 +42,33 @@ function colorContrastEvaluate(node, options = {}, virtualNode) {
return undefined;
}

const noScroll = !!(options || {}).noScroll;
const bgNodes = [];
const bgColor = getBackgroundColor(node, bgNodes, noScroll);
const fgColor = getForegroundColor(node, noScroll, bgColor);
const bgColor = getBackgroundColor(node, bgNodes, false);
const fgColor = getForegroundColor(node, false, bgColor);

const nodeStyle = window.getComputedStyle(node);
const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size'));
const fontWeight = nodeStyle.getPropertyValue('font-weight');
const bold = parseFloat(fontWeight) >= 700 || fontWeight === 'bold';
const bold = parseFloat(fontWeight) >= boldValue || fontWeight === 'bold';

const contrast = getContrast(bgColor, fgColor);
const ptSize = Math.ceil(fontSize * 72) / 96;
const isSmallFont =
(bold && ptSize < boldPt) || (!bold && ptSize < nonBoldPt);
straker marked this conversation as resolved.
Show resolved Hide resolved

const { expected, minThreshold, maxThreshold } = isSmallFont
? contrastRatio.normal
: contrastRatio.large;
const isValid = contrast > expected;

const cr = hasValidContrastRatio(bgColor, fgColor, fontSize, bold);
straker marked this conversation as resolved.
Show resolved Hide resolved
// ratio is outside range
if (contrast < minThreshold || contrast > maxThreshold) {
straker marked this conversation as resolved.
Show resolved Hide resolved
return true;
}

// truncate ratio to three digits while rounding down
// 4.499 = 4.49, 4.019 = 4.01
const truncatedResult = Math.floor(cr.contrastRatio * 100) / 100;
const truncatedResult = Math.floor(contrast * 100) / 100;

// if fgColor or bgColor are missing, get more information.
let missing;
Expand All @@ -58,7 +78,6 @@ function colorContrastEvaluate(node, options = {}, virtualNode) {

const equalRatio = truncatedResult === 1;
const shortTextContent = visibleText.length === 1;
const ignoreLength = !!(options || {}).ignoreLength;
if (equalRatio) {
missing = incompleteData.set('bgColor', 'equalRatio');
} else if (shortTextContent && !ignoreLength) {
Expand All @@ -70,11 +89,11 @@ function colorContrastEvaluate(node, options = {}, virtualNode) {
const data = {
fgColor: fgColor ? fgColor.toHexString() : undefined,
bgColor: bgColor ? bgColor.toHexString() : undefined,
contrastRatio: cr ? truncatedResult : undefined,
contrastRatio: truncatedResult,
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
fontWeight: bold ? 'bold' : 'normal',
messageKey: missing,
expectedContrastRatio: cr.expectedContrastRatio + ':1'
expectedContrastRatio: expected + ':1'
};

this.data(data);
Expand All @@ -84,19 +103,19 @@ function colorContrastEvaluate(node, options = {}, virtualNode) {
fgColor === null ||
bgColor === null ||
equalRatio ||
(shortTextContent && !ignoreLength && !cr.isValid)
(shortTextContent && !ignoreLength && !isValid)
) {
missing = null;
incompleteData.clear();
this.relatedNodes(bgNodes);
return undefined;
}

if (!cr.isValid) {
if (!isValid) {
this.relatedNodes(bgNodes);
}

return cr.isValid;
return isValid;
}

export default colorContrastEvaluate;
18 changes: 16 additions & 2 deletions lib/checks/color/color-contrast.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@
"id": "color-contrast",
"evaluate": "color-contrast-evaluate",
"options": {
"noScroll": false,
"ignoreUnicode": true,
"ignoreLength": false
"ignoreLength": false,
"boldValue": 700,
"boldPt": 14,
"nonBoldPt": 18,
straker marked this conversation as resolved.
Show resolved Hide resolved
"contrastRatio": {
"normal": {
"expected": 4.5,
"minThreshold": 0,
"maxThreshold": 999
Copy link
Contributor Author

@straker straker Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This number is arbitrary. It just needed to be something larger than the contrast ratio could ever go (#fff vs #000 = 21:1)

straker marked this conversation as resolved.
Show resolved Hide resolved
},
"large": {
"expected": 3,
"minThreshold": 0,
"maxThreshold": 999
straker marked this conversation as resolved.
Show resolved Hide resolved
}
}
},
"metadata": {
"impact": "serious",
Expand Down
2 changes: 2 additions & 0 deletions lib/commons/color/has-valid-contrast-ratio.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import getContrast from './get-contrast';
* @param {number} fontSize Font size of text, in pixels
* @param {boolean} isBold Whether the text is bold
* @return {{isValid: boolean, contrastRatio: number, expectedContrastRatio: number}}
*
* @deprecated
*/
function hasValidContrastRatio(bg, fg, fontSize, isBold) {
var contrast = getContrast(bg, fg);
Expand Down
4 changes: 2 additions & 2 deletions lib/core/base/check.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import metadataFunctionMap from './metadata-function-map';
import CheckResult from './check-result';
import { DqElement, checkHelper } from '../utils';
import { DqElement, checkHelper, deepMerge } from '../utils';

export function createExecutionContext(spec) {
/*eslint no-eval:0 */
Expand Down Expand Up @@ -187,7 +187,7 @@ Check.prototype.configure = function(spec) {
};

Check.prototype.getOptions = function getOptions(options = {}) {
return Object.assign({}, this.options, normalizeOptions(options));
return deepMerge(this.options, normalizeOptions(options));
};

export default Check;
31 changes: 31 additions & 0 deletions lib/core/utils/deep-merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Deeply merge two objects into a new object without changing any of the source objects.
* @see https://medium.com/javascript-in-plain-english/how-to-merge-objects-in-javascript-98f2209710e3
* @param {...Object} sources
* @return {Object}
*/
function deepMerge(...sources) {
const target = {};

sources.forEach(source => {
if (!source || typeof source !== 'object' || Array.isArray(source)) {
return;
}

for (const key of Object.keys(source)) {
if (
!target.hasOwnProperty(key) ||
typeof source[key] !== 'object' ||
Array.isArray(target[key])
) {
target[key] = source[key];
} else {
target[key] = deepMerge(target[key], source[key]);
}
}
});

return target;
}

export default deepMerge;
1 change: 1 addition & 0 deletions lib/core/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as closest } from './closest';
export { default as collectResultsFromFrames } from './collect-results-from-frames';
export { default as contains } from './contains';
export { default as cssParser } from './css-parser';
export { default as deepMerge } from './deep-merge';
export { default as DqElement } from './dq-element';
export { default as matchesSelector } from './element-matches';
export { default as escapeSelector } from './escape-selector';
Expand Down
17 changes: 2 additions & 15 deletions lib/standards/index.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
import ariaAttrs from './aria-attrs';
import { clone } from '../core/utils';
import { clone, deepMerge } from '../core/utils';

const origAriaAttrs = clone(ariaAttrs);
const standards = {
ariaAttrs
};

// @see https://stackoverflow.com/a/59008477/2124254
function merge(current, updates) {
for (const key of Object.keys(updates)) {
if (!current.hasOwnProperty(key) || typeof updates[key] !== 'object' || Array.isArray(current[key])) {
current[key] = updates[key];
} else {
merge(current[key], updates[key]);
}
}
return current;
}

export function configureStandards(config) {
if (config.ariaAttrs) {
standards.ariaAttrs = merge(ariaAttrs, config.ariaAttrs);
standards.ariaAttrs = deepMerge(standards.ariaAttrs, config.ariaAttrs);
}
}

Expand Down
141 changes: 141 additions & 0 deletions test/checks/color/color-contrast.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,147 @@ describe('color-contrast', function() {
assert.isFalse(actual);
});

it('should support options.boldValue', function() {
var params = checkSetup(
'<div style="color: gray; background-color: white; font-size: 14pt; font-weight: 100" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
boldValue: 100
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should support options.boldPt', function() {
var params = checkSetup(
'<div style="color: gray; background-color: white; font-size: 6pt; font-weight: 700" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
boldPt: 6
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should support options.nonBoldPt', function() {
var params = checkSetup(
'<div style="color: gray; background-color: white; font-size: 6pt; font-weight: 100" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
nonBoldPt: 6
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should support options.contrastRatio.normal.expected', function() {
var params = checkSetup(
'<div style="color: #999; background-color: white; font-size: 14pt; font-weight: 100" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
contrastRatio: {
normal: {
expected: 2.5
}
}
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should support options.contrastRatio.normal.minThreshold', function() {
var params = checkSetup(
'<div style="color: #999; background-color: white; font-size: 14pt; font-weight: 100" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
contrastRatio: {
normal: {
minThreshold: 3
}
}
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should support options.contrastRatio.normal.maxThreshold', function() {
var params = checkSetup(
'<div style="color: #999; background-color: white; font-size: 14pt; font-weight: 100" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
contrastRatio: {
normal: {
maxThreshold: 2
}
}
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should support options.contrastRatio.large.expected', function() {
var params = checkSetup(
'<div style="color: #ccc; background-color: white; font-size: 18pt; font-weight: 100" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
contrastRatio: {
large: {
expected: 1.5
}
}
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should support options.contrastRatio.large.minThreshold', function() {
var params = checkSetup(
'<div style="color: #ccc; background-color: white; font-size: 18pt; font-weight: 100" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
contrastRatio: {
large: {
minThreshold: 2
}
}
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should support options.contrastRatio.large.maxThreshold', function() {
var params = checkSetup(
'<div style="color: #ccc; background-color: white; font-size: 18pt; font-weight: 100" id="target">' +
'<span style="font-weight:bolder">My text</span></div>',
{
contrastRatio: {
large: {
maxThreshold: 1.2
}
}
}
);

assert.isTrue(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes, []);
});

(shadowSupported ? it : xit)(
'returns colors across Shadow DOM boundaries',
function() {
Expand Down
Loading