Skip to content
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
3 changes: 2 additions & 1 deletion lib/checks/color/color-contrast-enhanced.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
},
"pseudoSizeThreshold": 0.25,
"shadowOutlineEmMax": 0.1,
"textStrokeEmMin": 0.03
"textStrokeEmMin": 0.03,
"ruleId": "color-contrast-enhanced"
},
"metadata": {
"impact": "serious",
Expand Down
278 changes: 142 additions & 136 deletions lib/checks/color/color-contrast-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,156 +17,162 @@ import {
import { memoize } from '../../core/utils';

export default function colorContrastEvaluate(node, options, virtualNode) {
const {
ignoreUnicode,
ignoreLength,
ignorePseudo,
boldValue,
boldTextPt,
largeTextPt,
contrastRatio,
shadowOutlineEmMax,
pseudoSizeThreshold
} = options;

if (!isVisibleOnScreen(node)) {
this.data({ messageKey: 'hidden' });
return true;
}
try {
const {
ignoreUnicode,
ignoreLength,
ignorePseudo,
boldValue,
boldTextPt,
largeTextPt,
contrastRatio,
shadowOutlineEmMax,
pseudoSizeThreshold
} = options;

if (!isVisibleOnScreen(node)) {
this.data({ messageKey: 'hidden' });
return true;
}

const visibleText = visibleVirtual(virtualNode, false, true);
if (ignoreUnicode && textIsEmojis(visibleText)) {
this.data({ messageKey: 'nonBmp' });
return undefined;
}
const visibleText = visibleVirtual(virtualNode, false, true);
if (ignoreUnicode && textIsEmojis(visibleText)) {
this.data({ messageKey: 'nonBmp' });
return undefined;
}

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

const ptSize = Math.ceil(fontSize * 72) / 96;
const isSmallFont =
(bold && ptSize < boldTextPt) || (!bold && ptSize < largeTextPt);

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

// if element or a parent has pseudo content then we need to mark
// as needs review
const pseudoElm = findPseudoElement(virtualNode, {
ignorePseudo,
pseudoSizeThreshold
});
if (pseudoElm) {
this.data({
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
fontWeight: bold ? 'bold' : 'normal',
messageKey: 'pseudoContent',
expectedContrastRatio: expected + ':1'
const nodeStyle = window.getComputedStyle(node);
const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size'));
const fontWeight = nodeStyle.getPropertyValue('font-weight');
const bold = parseFloat(fontWeight) >= boldValue || fontWeight === 'bold';

const ptSize = Math.ceil(fontSize * 72) / 96;
const isSmallFont =
(bold && ptSize < boldTextPt) || (!bold && ptSize < largeTextPt);

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

// if element or a parent has pseudo content then we need to mark
// as needs review
const pseudoElm = findPseudoElement(virtualNode, {
ignorePseudo,
pseudoSizeThreshold
});
if (pseudoElm) {
this.data({
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
fontWeight: bold ? 'bold' : 'normal',
messageKey: 'pseudoContent',
expectedContrastRatio: expected + ':1'
});

this.relatedNodes(pseudoElm.actualNode);
return undefined;
}

this.relatedNodes(pseudoElm.actualNode);
return undefined;
}
// Thin shadows only. Thicker shadows are included in the background instead
const shadowColors = getTextShadowColors(node, {
minRatio: 0.001,
maxRatio: shadowOutlineEmMax
});
if (shadowColors === null) {
this.data({ messageKey: 'complexTextShadows' });
return undefined;
}

// Thin shadows only. Thicker shadows are included in the background instead
const shadowColors = getTextShadowColors(node, {
minRatio: 0.001,
maxRatio: shadowOutlineEmMax
});
if (shadowColors === null) {
this.data({ messageKey: 'complexTextShadows' });
return undefined;
}
const bgNodes = [];
const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax);
const fgColor = getForegroundColor(node, false, bgColor, options);

let contrast = null;
let contrastContributor = null;
let shadowColor = null;
if (shadowColors.length === 0) {
contrast = getContrast(bgColor, fgColor);
} else if (fgColor && bgColor) {
shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors);
// Compare shadow, bgColor, textColor. Check passes if any is sufficient
const fgBgContrast = getContrast(bgColor, fgColor);
const bgShContrast = getContrast(bgColor, shadowColor);
const fgShContrast = getContrast(shadowColor, fgColor);
contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast);
if (contrast !== fgBgContrast) {
contrastContributor =
bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor';
}
}

const bgNodes = [];
const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax);
const fgColor = getForegroundColor(node, false, bgColor, options);

let contrast = null;
let contrastContributor = null;
let shadowColor = null;
if (shadowColors.length === 0) {
contrast = getContrast(bgColor, fgColor);
} else if (fgColor && bgColor) {
shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors);
// Compare shadow, bgColor, textColor. Check passes if any is sufficient
const fgBgContrast = getContrast(bgColor, fgColor);
const bgShContrast = getContrast(bgColor, shadowColor);
const fgShContrast = getContrast(shadowColor, fgColor);
contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast);
if (contrast !== fgBgContrast) {
contrastContributor =
bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor';
const isValid = contrast > expected;

// ratio is outside range
if (
(typeof minThreshold === 'number' &&
(typeof contrast !== 'number' || contrast < minThreshold)) ||
(typeof maxThreshold === 'number' &&
(typeof contrast !== 'number' || contrast > maxThreshold))
) {
this.data({ contrastRatio: contrast });
return true;
}
}

const isValid = contrast > expected;

// ratio is outside range
if (
(typeof minThreshold === 'number' &&
(typeof contrast !== 'number' || contrast < minThreshold)) ||
(typeof maxThreshold === 'number' &&
(typeof contrast !== 'number' || contrast > maxThreshold))
) {
this.data({ contrastRatio: contrast });
return true;
}
// truncate ratio to three digits while rounding down
// 4.499 = 4.49, 4.019 = 4.01
const truncatedResult = Math.floor(contrast * 100) / 100;

// truncate ratio to three digits while rounding down
// 4.499 = 4.49, 4.019 = 4.01
const truncatedResult = Math.floor(contrast * 100) / 100;
// if fgColor or bgColor are missing, get more information.
let missing;
if (bgColor === null) {
missing = incompleteData.get('bgColor');
} else if (!isValid) {
missing = contrastContributor;
}

// if fgColor or bgColor are missing, get more information.
let missing;
if (bgColor === null) {
missing = incompleteData.get('bgColor');
} else if (!isValid) {
missing = contrastContributor;
}
const equalRatio = truncatedResult === 1;
const shortTextContent = visibleText.length === 1;
if (equalRatio) {
missing = incompleteData.set('bgColor', 'equalRatio');
} else if (!isValid && shortTextContent && !ignoreLength) {
// Check that the text content is a single character long
missing = 'shortTextContent';
}

const equalRatio = truncatedResult === 1;
const shortTextContent = visibleText.length === 1;
if (equalRatio) {
missing = incompleteData.set('bgColor', 'equalRatio');
} else if (!isValid && shortTextContent && !ignoreLength) {
// Check that the text content is a single character long
missing = 'shortTextContent';
}
// need both independently in case both are missing
this.data({
fgColor: fgColor ? fgColor.toHexString() : undefined,
bgColor: bgColor ? bgColor.toHexString() : undefined,
contrastRatio: truncatedResult,
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
fontWeight: bold ? 'bold' : 'normal',
messageKey: missing,
expectedContrastRatio: expected + ':1',
shadowColor: shadowColor ? shadowColor.toHexString() : undefined
});

// need both independently in case both are missing
this.data({
fgColor: fgColor ? fgColor.toHexString() : undefined,
bgColor: bgColor ? bgColor.toHexString() : undefined,
contrastRatio: truncatedResult,
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
fontWeight: bold ? 'bold' : 'normal',
messageKey: missing,
expectedContrastRatio: expected + ':1',
shadowColor: shadowColor ? shadowColor.toHexString() : undefined
});

// We don't know, so we'll put it into Can't Tell
if (
fgColor === null ||
bgColor === null ||
equalRatio ||
(shortTextContent && !ignoreLength && !isValid)
) {
missing = null;
incompleteData.clear();
this.relatedNodes(bgNodes);
return undefined;
}
// We don't know, so we'll put it into Can't Tell
if (
fgColor === null ||
bgColor === null ||
equalRatio ||
(shortTextContent && !ignoreLength && !isValid)
) {
missing = null;
incompleteData.clear();
this.relatedNodes(bgNodes);
return undefined;
}

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

return isValid;
return isValid;
} catch (err) {
this.data({ messageKey: 'Check error' });
a11yEngine.errorHandler.addCheckError(options?.ruleId, err);
return undefined;
}
}

function findPseudoElement(
Expand Down
3 changes: 2 additions & 1 deletion lib/checks/color/color-contrast.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
},
"pseudoSizeThreshold": 0.25,
"shadowOutlineEmMax": 0.2,
"textStrokeEmMin": 0.03
"textStrokeEmMin": 0.03,
"ruleId": "color-contrast"
},
"metadata": {
"impact": "serious",
Expand Down
6 changes: 4 additions & 2 deletions lib/checks/forms/autocomplete-a11y-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { isValidAutocomplete } from '../../commons/text';
import ErrorHandler from '../../core/errors/error-handler';

function checkIsElementValidAutocomplete(node, options, virtualNode) {
const autocomplete = virtualNode.attr('autocomplete')?.toLowerCase().trim();
Expand Down Expand Up @@ -67,7 +66,10 @@ function autocompleteA11yEvaluate(node, options, virtualNode) {
return checkIsElementValidAutocomplete(node, options, virtualNode);
}
} catch (err) {
ErrorHandler.addCheckError('autocomplete-attribute-valid-check', err);
a11yEngine.errorHandler.addCheckError(
'autocomplete-attribute-valid-check',
err
);
return undefined;
}
}
Expand Down