Skip to content

Commit ceddad8

Browse files
committed
added guard rails for color contrast
1 parent 6cee221 commit ceddad8

File tree

2 files changed

+142
-143
lines changed

2 files changed

+142
-143
lines changed

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

Lines changed: 141 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -17,157 +17,161 @@ import {
1717
import { memoize } from '../../core/utils';
1818

1919
export default function colorContrastEvaluate(node, options, virtualNode) {
20-
const {
21-
ignoreUnicode,
22-
ignoreLength,
23-
ignorePseudo,
24-
boldValue,
25-
boldTextPt,
26-
largeTextPt,
27-
contrastRatio,
28-
shadowOutlineEmMax,
29-
pseudoSizeThreshold
30-
} = options;
31-
32-
if (!isVisibleOnScreen(node)) {
33-
this.data({ messageKey: 'hidden' });
34-
return true;
35-
}
20+
try {
21+
const {
22+
ignoreUnicode,
23+
ignoreLength,
24+
ignorePseudo,
25+
boldValue,
26+
boldTextPt,
27+
largeTextPt,
28+
contrastRatio,
29+
shadowOutlineEmMax,
30+
pseudoSizeThreshold
31+
} = options;
32+
33+
if (!isVisibleOnScreen(node)) {
34+
this.data({ messageKey: 'hidden' });
35+
return true;
36+
}
3637

37-
const visibleText = visibleVirtual(virtualNode, false, true);
38-
if (ignoreUnicode && textIsEmojis(visibleText)) {
39-
this.data({ messageKey: 'nonBmp' });
40-
return undefined;
41-
}
38+
const visibleText = visibleVirtual(virtualNode, false, true);
39+
if (ignoreUnicode && textIsEmojis(visibleText)) {
40+
this.data({ messageKey: 'nonBmp' });
41+
return undefined;
42+
}
4243

43-
const nodeStyle = window.getComputedStyle(node);
44-
const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size'));
45-
const fontWeight = nodeStyle.getPropertyValue('font-weight');
46-
const bold = parseFloat(fontWeight) >= boldValue || fontWeight === 'bold';
47-
48-
const ptSize = Math.ceil(fontSize * 72) / 96;
49-
const isSmallFont =
50-
(bold && ptSize < boldTextPt) || (!bold && ptSize < largeTextPt);
51-
52-
const { expected, minThreshold, maxThreshold } = isSmallFont
53-
? contrastRatio.normal
54-
: contrastRatio.large;
55-
56-
// if element or a parent has pseudo content then we need to mark
57-
// as needs review
58-
const pseudoElm = findPseudoElement(virtualNode, {
59-
ignorePseudo,
60-
pseudoSizeThreshold
61-
});
62-
if (pseudoElm) {
63-
this.data({
64-
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
65-
fontWeight: bold ? 'bold' : 'normal',
66-
messageKey: 'pseudoContent',
67-
expectedContrastRatio: expected + ':1'
44+
const nodeStyle = window.getComputedStyle(node);
45+
const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size'));
46+
const fontWeight = nodeStyle.getPropertyValue('font-weight');
47+
const bold = parseFloat(fontWeight) >= boldValue || fontWeight === 'bold';
48+
49+
const ptSize = Math.ceil(fontSize * 72) / 96;
50+
const isSmallFont =
51+
(bold && ptSize < boldTextPt) || (!bold && ptSize < largeTextPt);
52+
53+
const { expected, minThreshold, maxThreshold } = isSmallFont
54+
? contrastRatio.normal
55+
: contrastRatio.large;
56+
57+
// if element or a parent has pseudo content then we need to mark
58+
// as needs review
59+
const pseudoElm = findPseudoElement(virtualNode, {
60+
ignorePseudo,
61+
pseudoSizeThreshold
6862
});
63+
if (pseudoElm) {
64+
this.data({
65+
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
66+
fontWeight: bold ? 'bold' : 'normal',
67+
messageKey: 'pseudoContent',
68+
expectedContrastRatio: expected + ':1'
69+
});
70+
71+
this.relatedNodes(pseudoElm.actualNode);
72+
return undefined;
73+
}
6974

70-
this.relatedNodes(pseudoElm.actualNode);
71-
return undefined;
72-
}
75+
// Thin shadows only. Thicker shadows are included in the background instead
76+
const shadowColors = getTextShadowColors(node, {
77+
minRatio: 0.001,
78+
maxRatio: shadowOutlineEmMax
79+
});
80+
if (shadowColors === null) {
81+
this.data({ messageKey: 'complexTextShadows' });
82+
return undefined;
83+
}
7384

74-
// Thin shadows only. Thicker shadows are included in the background instead
75-
const shadowColors = getTextShadowColors(node, {
76-
minRatio: 0.001,
77-
maxRatio: shadowOutlineEmMax
78-
});
79-
if (shadowColors === null) {
80-
this.data({ messageKey: 'complexTextShadows' });
81-
return undefined;
82-
}
85+
const bgNodes = [];
86+
const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax);
87+
const fgColor = getForegroundColor(node, false, bgColor, options);
88+
89+
let contrast = null;
90+
let contrastContributor = null;
91+
let shadowColor = null;
92+
if (shadowColors.length === 0) {
93+
contrast = getContrast(bgColor, fgColor);
94+
} else if (fgColor && bgColor) {
95+
shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors);
96+
// Compare shadow, bgColor, textColor. Check passes if any is sufficient
97+
const fgBgContrast = getContrast(bgColor, fgColor);
98+
const bgShContrast = getContrast(bgColor, shadowColor);
99+
const fgShContrast = getContrast(shadowColor, fgColor);
100+
contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast);
101+
if (contrast !== fgBgContrast) {
102+
contrastContributor =
103+
bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor';
104+
}
105+
}
83106

84-
const bgNodes = [];
85-
axe._cache.set('ruleId', 'axe-color-contrast');
86-
const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax);
87-
const fgColor = getForegroundColor(node, false, bgColor, options);
88-
89-
let contrast = null;
90-
let contrastContributor = null;
91-
let shadowColor = null;
92-
if (shadowColors.length === 0) {
93-
contrast = getContrast(bgColor, fgColor);
94-
} else if (fgColor && bgColor) {
95-
shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors);
96-
// Compare shadow, bgColor, textColor. Check passes if any is sufficient
97-
const fgBgContrast = getContrast(bgColor, fgColor);
98-
const bgShContrast = getContrast(bgColor, shadowColor);
99-
const fgShContrast = getContrast(shadowColor, fgColor);
100-
contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast);
101-
if (contrast !== fgBgContrast) {
102-
contrastContributor =
103-
bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor';
107+
const isValid = contrast > expected;
108+
109+
// ratio is outside range
110+
if (
111+
(typeof minThreshold === 'number' &&
112+
(typeof contrast !== 'number' || contrast < minThreshold)) ||
113+
(typeof maxThreshold === 'number' &&
114+
(typeof contrast !== 'number' || contrast > maxThreshold))
115+
) {
116+
this.data({ contrastRatio: contrast });
117+
return true;
104118
}
105-
}
106119

107-
const isValid = contrast > expected;
108-
109-
// ratio is outside range
110-
if (
111-
(typeof minThreshold === 'number' &&
112-
(typeof contrast !== 'number' || contrast < minThreshold)) ||
113-
(typeof maxThreshold === 'number' &&
114-
(typeof contrast !== 'number' || contrast > maxThreshold))
115-
) {
116-
this.data({ contrastRatio: contrast });
117-
return true;
118-
}
120+
// truncate ratio to three digits while rounding down
121+
// 4.499 = 4.49, 4.019 = 4.01
122+
const truncatedResult = Math.floor(contrast * 100) / 100;
119123

120-
// truncate ratio to three digits while rounding down
121-
// 4.499 = 4.49, 4.019 = 4.01
122-
const truncatedResult = Math.floor(contrast * 100) / 100;
124+
// if fgColor or bgColor are missing, get more information.
125+
let missing;
126+
if (bgColor === null) {
127+
missing = incompleteData.get('bgColor');
128+
} else if (!isValid) {
129+
missing = contrastContributor;
130+
}
123131

124-
// if fgColor or bgColor are missing, get more information.
125-
let missing;
126-
if (bgColor === null) {
127-
missing = incompleteData.get('bgColor');
128-
} else if (!isValid) {
129-
missing = contrastContributor;
130-
}
132+
const equalRatio = truncatedResult === 1;
133+
const shortTextContent = visibleText.length === 1;
134+
if (equalRatio) {
135+
missing = incompleteData.set('bgColor', 'equalRatio');
136+
} else if (!isValid && shortTextContent && !ignoreLength) {
137+
// Check that the text content is a single character long
138+
missing = 'shortTextContent';
139+
}
131140

132-
const equalRatio = truncatedResult === 1;
133-
const shortTextContent = visibleText.length === 1;
134-
if (equalRatio) {
135-
missing = incompleteData.set('bgColor', 'equalRatio');
136-
} else if (!isValid && shortTextContent && !ignoreLength) {
137-
// Check that the text content is a single character long
138-
missing = 'shortTextContent';
139-
}
141+
// need both independently in case both are missing
142+
this.data({
143+
fgColor: fgColor ? fgColor.toHexString() : undefined,
144+
bgColor: bgColor ? bgColor.toHexString() : undefined,
145+
contrastRatio: truncatedResult,
146+
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
147+
fontWeight: bold ? 'bold' : 'normal',
148+
messageKey: missing,
149+
expectedContrastRatio: expected + ':1',
150+
shadowColor: shadowColor ? shadowColor.toHexString() : undefined
151+
});
140152

141-
// need both independently in case both are missing
142-
this.data({
143-
fgColor: fgColor ? fgColor.toHexString() : undefined,
144-
bgColor: bgColor ? bgColor.toHexString() : undefined,
145-
contrastRatio: truncatedResult,
146-
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
147-
fontWeight: bold ? 'bold' : 'normal',
148-
messageKey: missing,
149-
expectedContrastRatio: expected + ':1',
150-
shadowColor: shadowColor ? shadowColor.toHexString() : undefined
151-
});
152-
153-
// We don't know, so we'll put it into Can't Tell
154-
if (
155-
fgColor === null ||
156-
bgColor === null ||
157-
equalRatio ||
158-
(shortTextContent && !ignoreLength && !isValid)
159-
) {
160-
missing = null;
161-
incompleteData.clear();
162-
this.relatedNodes(bgNodes);
163-
return undefined;
164-
}
153+
// We don't know, so we'll put it into Can't Tell
154+
if (
155+
fgColor === null ||
156+
bgColor === null ||
157+
equalRatio ||
158+
(shortTextContent && !ignoreLength && !isValid)
159+
) {
160+
missing = null;
161+
incompleteData.clear();
162+
this.relatedNodes(bgNodes);
163+
return undefined;
164+
}
165165

166-
if (!isValid) {
167-
this.relatedNodes(bgNodes);
168-
}
166+
if (!isValid) {
167+
this.relatedNodes(bgNodes);
168+
}
169169

170-
return isValid;
170+
return isValid;
171+
} catch (err) {
172+
a11yEngine.axeErrorHandlers.addCheckError('axe-color-contrast-check', err);
173+
return undefined;
174+
}
171175
}
172176

173177
function findPseudoElement(

lib/commons/dom/get-rect-stack.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import visuallySort from './visually-sort';
22
import { getRectCenter } from '../math';
3-
import cache from '../../core/base/cache';
43

54
// Additional props isCoordsPassed, x, y for a11y-engine-domforge
65
export function getRectStack(
@@ -43,11 +42,7 @@ export function getRectStack(
4342

4443
const gridContainer = grid.container;
4544
//adding just if color contrast is being run then only then the extra added condition should run
46-
if (
47-
gridContainer &&
48-
(!(cache.get('ruleId') && cache.get('ruleId') === 'axe-color-contrast') ||
49-
gridContainer._grid)
50-
) {
45+
if (gridContainer) {
5146
stack = getRectStack(
5247
gridContainer._grid,
5348
gridContainer.boundingClientRect,

0 commit comments

Comments
 (0)