Skip to content

Commit

Permalink
feat(color-contrast, utils): add more options to color-contrast, add …
Browse files Browse the repository at this point in the history
…utils.deepMerge, deprecate commons.color.hasValidContrastRatio (#2256)

* feat(color-contrast): add options for bold size, font size, and contrast ratio requirements

* better option names, properly deep merge options

* fix tests

* Update lib/checks/color/color-contrast.json

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/checks/color/color-contrast-evaluate.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/checks/color/color-contrast-evaluate.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/checks/color/color-contrast.json

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/checks/color/color-contrast-evaluate.js

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* Update lib/checks/color/color-contrast.json

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

* fixes

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
  • Loading branch information
straker and WilcoFiers authored Jul 8, 2020
1 parent 67d2dca commit 49fdb46
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 35 deletions.
54 changes: 38 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,
boldTextPt,
largeTextPt,
contrastRatio
} = options;

const visibleText = visibleVirtual(virtualNode, false, true);
const ignoreUnicode = !!options.ignoreUnicode;
const textContainsOnlyUnicode =
hasUnicode(visibleText, {
nonBmp: true
Expand All @@ -34,21 +42,36 @@ 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 < boldTextPt) || (!bold && ptSize < largeTextPt);

const cr = hasValidContrastRatio(bgColor, fgColor, fontSize, bold);
const { expected, minThreshold, maxThreshold } = isSmallFont
? contrastRatio.normal
: contrastRatio.large;
const isValid = contrast > expected;

// ratio is outside range
if (
(typeof minThreshold === 'number' && contrast < minThreshold) ||
(typeof maxThreshold === 'number' && contrast > maxThreshold)
) {
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 +81,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 +92,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 +106,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;
14 changes: 12 additions & 2 deletions lib/checks/color/color-contrast.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@
"id": "color-contrast",
"evaluate": "color-contrast-evaluate",
"options": {
"noScroll": false,
"ignoreUnicode": true,
"ignoreLength": false
"ignoreLength": false,
"boldValue": 700,
"boldTextPt": 14,
"largeTextPt": 18,
"contrastRatio": {
"normal": {
"expected": 4.5
},
"large": {
"expected": 3
}
}
},
"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.boldTextPt', 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>',
{
boldTextPt: 6
}
);

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

it('should support options.largeTextPt', 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>',
{
largeTextPt: 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

0 comments on commit 49fdb46

Please sign in to comment.