Skip to content

Commit 733d1e8

Browse files
authored
Merge pull request #133 from browserstack/AXE-1305-color-contrast-null-handling
fix: Null handling for color contrast algo
2 parents 5830a47 + db50966 commit 733d1e8

File tree

4 files changed

+150
-140
lines changed

4 files changed

+150
-140
lines changed

lib/checks/color/color-contrast-enhanced.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
},
2121
"pseudoSizeThreshold": 0.25,
2222
"shadowOutlineEmMax": 0.1,
23-
"textStrokeEmMin": 0.03
23+
"textStrokeEmMin": 0.03,
24+
"ruleId": "color-contrast-enhanced"
2425
},
2526
"metadata": {
2627
"impact": "serious",

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

Lines changed: 142 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -17,156 +17,162 @@ 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-
const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax);
86-
const fgColor = getForegroundColor(node, false, bgColor, options);
87-
88-
let contrast = null;
89-
let contrastContributor = null;
90-
let shadowColor = null;
91-
if (shadowColors.length === 0) {
92-
contrast = getContrast(bgColor, fgColor);
93-
} else if (fgColor && bgColor) {
94-
shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors);
95-
// Compare shadow, bgColor, textColor. Check passes if any is sufficient
96-
const fgBgContrast = getContrast(bgColor, fgColor);
97-
const bgShContrast = getContrast(bgColor, shadowColor);
98-
const fgShContrast = getContrast(shadowColor, fgColor);
99-
contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast);
100-
if (contrast !== fgBgContrast) {
101-
contrastContributor =
102-
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;
103118
}
104-
}
105119

106-
const isValid = contrast > expected;
107-
108-
// ratio is outside range
109-
if (
110-
(typeof minThreshold === 'number' &&
111-
(typeof contrast !== 'number' || contrast < minThreshold)) ||
112-
(typeof maxThreshold === 'number' &&
113-
(typeof contrast !== 'number' || contrast > maxThreshold))
114-
) {
115-
this.data({ contrastRatio: contrast });
116-
return true;
117-
}
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;
118123

119-
// truncate ratio to three digits while rounding down
120-
// 4.499 = 4.49, 4.019 = 4.01
121-
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+
}
122131

123-
// if fgColor or bgColor are missing, get more information.
124-
let missing;
125-
if (bgColor === null) {
126-
missing = incompleteData.get('bgColor');
127-
} else if (!isValid) {
128-
missing = contrastContributor;
129-
}
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+
}
130140

131-
const equalRatio = truncatedResult === 1;
132-
const shortTextContent = visibleText.length === 1;
133-
if (equalRatio) {
134-
missing = incompleteData.set('bgColor', 'equalRatio');
135-
} else if (!isValid && shortTextContent && !ignoreLength) {
136-
// Check that the text content is a single character long
137-
missing = 'shortTextContent';
138-
}
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+
});
139152

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

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

169-
return isValid;
170+
return isValid;
171+
} catch (err) {
172+
this.data({ messageKey: 'Check error' });
173+
a11yEngine.errorHandler.addCheckError(options?.ruleId, err);
174+
return undefined;
175+
}
170176
}
171177

172178
function findPseudoElement(

lib/checks/color/color-contrast.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
},
1919
"pseudoSizeThreshold": 0.25,
2020
"shadowOutlineEmMax": 0.2,
21-
"textStrokeEmMin": 0.03
21+
"textStrokeEmMin": 0.03,
22+
"ruleId": "color-contrast"
2223
},
2324
"metadata": {
2425
"impact": "serious",

lib/checks/forms/autocomplete-a11y-evaluate.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { isValidAutocomplete } from '../../commons/text';
2-
import ErrorHandler from '../../core/errors/error-handler';
32

43
function checkIsElementValidAutocomplete(node, options, virtualNode) {
54
const autocomplete = virtualNode.attr('autocomplete')?.toLowerCase().trim();
@@ -67,7 +66,10 @@ function autocompleteA11yEvaluate(node, options, virtualNode) {
6766
return checkIsElementValidAutocomplete(node, options, virtualNode);
6867
}
6968
} catch (err) {
70-
ErrorHandler.addCheckError('autocomplete-attribute-valid-check', err);
69+
a11yEngine.errorHandler.addCheckError(
70+
'autocomplete-attribute-valid-check',
71+
err
72+
);
7173
return undefined;
7274
}
7375
}

0 commit comments

Comments
 (0)