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

fix(engine): fix color contrast issues for text with image/gradient background or shadow #1405

Merged
merged 13 commits into from
May 15, 2023
Merged
23 changes: 17 additions & 6 deletions accessibility-checker-engine/src/v2/dom/ColorUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export class ColorUtil {
var retVal = {
"hasGradient": false,
"hasBGImage": false,
"textShadow": false,
"fg": null,
"bg": null
};
Expand Down Expand Up @@ -267,7 +268,7 @@ export class ColorUtil {
overallWorst = worstColor;
}
}
return overallWorst;
return overallWorst; // return the darkest color
} catch (e) {
console.log(e);
}
Expand All @@ -285,7 +286,7 @@ export class ColorUtil {
// cStyle is the computed style of this layer
var cStyle = win.getComputedStyle(procNext);
if (cStyle === null) continue;

// thisBgColor is the color of this layer or null if the layer is transparent
var thisBgColor = null;
if (cStyle.backgroundColor && cStyle.backgroundColor != "transparent" && cStyle.backgroundColor != "rgba(0, 0, 0, 0)") {
Expand All @@ -300,13 +301,19 @@ export class ColorUtil {
if (!gradColors[i].length) {
gradColors.splice(i--, 1);
} else {
gradColorComp.push(ColorUtil.Color(gradColors[i]));
let colorComp = ColorUtil.Color(gradColors[i]);
if (colorComp.alpha !== undefined && colorComp.alpha < 1) {
// mix the grdient bg color wit parent bg if alpha < 1
let compStackBg = thisStackBG || priorStackBG;
colorComp = colorComp.getOverlayColor(compStackBg);
}
gradColorComp.push(colorComp);
}
}
thisBgColor = guessGradColor(gradColorComp, thisStackBG || priorStackBG, fg);
}
}

// Handle non-solid opacity
if (thisStackOpacity === null || (cStyle.opacity && cStyle.opacity.length > 0 && parseFloat(cStyle.opacity) < 1)) {
// New stack, reset
Expand Down Expand Up @@ -370,6 +377,10 @@ export class ColorUtil {
}
retVal.fg = fg;
retVal.bg = priorStackBG;

if (cStyle.textShadow && cStyle.textShadow !== 'none')
retVal.textShadow = true;

return retVal;
} catch (err) {
// something happened, then...
Expand Down Expand Up @@ -413,10 +424,10 @@ export class ColorObj {

contrastRatio(bgColor : ColorObj) {
let fgColor: ColorObj = this;

if (typeof (this.alpha) != "undefined")
fgColor = this.getOverlayColor(bgColor);

let lum1 = fgColor.relativeLuminance();
if (!bgColor.relativeLuminance) {
let s = "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { VisUtil } from "../../v2/dom/VisUtil";
import { ColorUtil } from "../../v2/dom/ColorUtil";
import { Rule, RuleResult, RuleFail, RuleContext, RulePotential, RulePass, RuleContextHierarchy } from "../api/IRule";
import { eRulePolicy, eToolkitLevel } from "../api/IRule";
import { setCache } from "../util/CacheUtil";
//import { setCache } from "../util/CacheUtil";
import { getWeightNumber, getFontInPixels } from "../util/CSSUtil";

export let text_contrast_sufficient: Rule = {
Expand All @@ -26,23 +26,31 @@ export let text_contrast_sufficient: Rule = {
"IBMA_Color_Contrast_WCAG2AA": {
"Pass_0": "Pass_0",
"Fail_1": "Fail_1",
"Potential_1": "Potential_1"
"Potential_1": "Potential_same_color"
},
"IBMA_Color_Contrast_WCAG2AA_PV": {
"Pass_0": "Pass_0",
"Potential_1": "Potential_graphic_background"
}
},
help: {
"en-US": {
"group": `text_contrast_sufficient.html`,
"Pass_0": `text_contrast_sufficient.html`,
"Fail_1": `text_contrast_sufficient.html`,
"Potential_1": `text_contrast_sufficient.html`
"Potential_same_color": `text_contrast_sufficient.html`,
"Potential_graphic_background": `text_contrast_sufficient.html`,
"Potential_text_shadow": `text_contrast_sufficient.html`
}
},
messages: {
"en-US": {
"group": "The contrast ratio of text with its background must meet WCAG 2.1 AA requirements",
"Pass_0": "Rule Passed",
"Fail_1": "Text contrast of {0} with its background is less than the WCAG AA minimum requirements for text of size {1}px and weight of {2}",
"Potential_1": "The foreground text and its background color are both detected as {3}. Verify the text meets the WCAG 2.1 AA requirements for minimum contrast"
"Potential_same_color": "The foreground text and its background color are both detected as {3}. Verify the text meets the WCAG 2.1 AA requirements for minimum contrast",
"Potential_graphic_background": "Verify the contrast ratio of the text against the lightest and the darkest colors of the background meets the WCAG 2.1 AA minimum requirements for text of size {1}px and weight of {2}",
"Potential_text_shadow": "Verify the contrast ratio of the text with shadow meets the WCAG 2.1 AA minimum requirements for text of size {1}px and weight of {2}"
}
},
rulesets: [{
Expand Down Expand Up @@ -85,9 +93,23 @@ export let text_contrast_sufficient: Rule = {
// Ensure that this element has children with actual text.
let childStr = RPTUtil.getNodeText(ruleContext);

if (childStr.trim().length == 0 && (!RPTUtil.isShadowHostElement(ruleContext) || (RPTUtil.isShadowHostElement(ruleContext) && RPTUtil.getNodeText(ruleContext.shadowRoot) === '')))
return null;

if (!RPTUtil.isShadowHostElement(ruleContext) || (RPTUtil.isShadowHostElement(ruleContext) && RPTUtil.getNodeText(ruleContext.shadowRoot) === '')) {
if (childStr.trim().length == 0 )
return null;

// ignore if the text does not convey anything in human language
/**
* (1) ignore non-alphanumeric or special characters in ASCI: ^(a-zA-Z\d\s)
* (2) ignore non-printable unicode characters: \u0000-\u0008\u000B-\u001F\u007F-\u009F\u2000-\u200F\u2028-\u202F\u205F-\u206F\u3000\uFEFF
* see https://stackoverflow.com/questions/3770117/what-is-the-range-of-unicode-printable-characters
* (3) for now not consider unicode special characters that are different in different languages
*/
let regex = /[^(a-zA-Z\d\s)\u0000-\u0008\u000B-\u001F\u007F-\u009F\u2000-\u200F\u2028-\u202F\u205F-\u206F\u3000\uFEFF]+/g;
const removed = childStr.trim().replace(regex, '');
if (removed.length === 0)
return null;
}

let elem = ruleContext;
// the child elements (rather than shadow root) of a shadow host is either re-assigned to the shadow slot if the slot exists
// or not displayed, so shouldn't be checked from the light DOM, rather it should be checked as reassginged slot element(s) in the shadow DOM.
Expand All @@ -104,7 +126,7 @@ export let text_contrast_sufficient: Rule = {
}
if (elem === null) return;
}

let style = win.getComputedStyle(elem);

// JCH clip INFO:
Expand Down Expand Up @@ -216,7 +238,7 @@ export let text_contrast_sufficient: Rule = {
// Corner case where item is hidden (accessibility hiding technique)
return null;
}

// First determine the color contrast ratio
let colorCombo = ColorUtil.ColorCombo(elem);
if (colorCombo === null) {
Expand All @@ -232,14 +254,15 @@ export let text_contrast_sufficient: Rule = {
let isLargeScale = size >= 24 || size >= 18.6 && weight >= 700;
let passed = ratio >= 4.5 || (ratio >= 3 && isLargeScale);
let hasBackground = colorCombo.hasBGImage || colorCombo.hasGradient;
let textShadow = colorCombo.textShadow;
let isDisabled = RPTUtil.isNodeDisabled(elem);
if (!isDisabled) {
let control = RPTUtil.getControlOfLabel(elem);
if (control) {
isDisabled = RPTUtil.isNodeDisabled(control);
}
}

if (!isDisabled && nodeName === 'label' && RPTUtil.isDisabledByFirstChildFormElement(elem)) {
isDisabled = true;
}
Expand All @@ -248,30 +271,33 @@ export let text_contrast_sufficient: Rule = {
isDisabled = true;
}

setCache(ruleContext, "EXT_Color_Contrast_WCAG2AA", {
/**setCache(ruleContext, "EXT_Color_Contrast_WCAG2AA", {
"ratio": ratio,
"isLargeScale": isLargeScale,
"weight": weight,
"size": size,
"hasBackground": hasBackground,
"isDisabled": isDisabled
});
if (hasBackground) {
// Allow other color rule to fire if we have a background
return null;
}

});*/

// If element or parent is disabled, this rule does not apply (but may be 3:1 in future)
if (!passed && isDisabled) {
passed = true;
}
//return new ValidationResult(passed, [ruleContext], '', '', [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]);
if (!passed) {
if (fg.toHex() === bg.toHex()) {
return RulePotential("Potential_1", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]);
if (hasBackground) {
// fire potential since a text on an image or gradient may be still viewable, depending on the text location on the gradient or image
return RulePotential("Potential_graphic_background", [ratio.toFixed(2), size, weight]);;
} else if (textShadow) {
// fire potential since a text with shadow may be still viewable, depending on the shadow efffects
return RulePotential("Potential_text_shadow", [ratio.toFixed(2), size, weight]);;
} else {
return RuleFail("Fail_1", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]);
}
if (fg.toHex() === bg.toHex()) {
return RulePotential("Potential_same_color", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]);
} else {
return RuleFail("Fail_1", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]);
}
}
} else {
return RulePass("Pass_0", [ratio.toFixed(2), size, weight, fg.toHex(), bg.toHex(), colorCombo.hasBGImage, colorCombo.hasGradient]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ <h3>Color using Class Tests</h3>
passedXpaths: [
],
failedXpaths: [
"/html/body/div[1]",
/**"/html/body/div[1]",
"/html/body/div[2]",
"/html/body/div[3]",
"/html/body/div[4]",
Expand All @@ -348,7 +348,7 @@ <h3>Color using Class Tests</h3>
"/html/body/div[42]",
"/html/body/div[43]",
"/html/body/div[44]",
"/html/body/div[45]"
"/html/body/div[45]"*/
]
}
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ <h3>Color using inline code Tests</h3>
passedXpaths: [
],
failedXpaths: [
"/html/body/div[1]",
/**"/html/body/div[1]",
"/html/body/div[2]",
"/html/body/div[3]",
"/html/body/div[4]",
Expand All @@ -133,7 +133,7 @@ <h3>Color using inline code Tests</h3>
"/html/body/div[42]",
"/html/body/div[43]",
"/html/body/div[44]",
"/html/body/div[45]"
"/html/body/div[45]"*/
]
}
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ <h3>Color using Class Tests</h3>
passedXpaths: [
],
failedXpaths: [
"/html[1]/body[1]/div[1]"
]
}
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ <h3>Color using inline code Tests</h3>
passedXpaths: [
],
failedXpaths: [
"/html[1]/body[1]/div[1]"
]
}
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ <h3>Color using Class Tests</h3>
<div class="alphaContrast11">This text is pass - div[52].</div>
<div class="alphaContrast12">This text is pass - div[53].</div>
<div class="alphaContrast13">This text is pass - div[54].</div>

<div aria-disabled="true" class="lowContrast1">This text is pass - div[55].</div>
<a name="navskip"></a>


Expand All @@ -325,6 +327,28 @@ <h3>Color using Class Tests</h3>
passedXpaths: [
],
failedXpaths: [
"/html/body/div[1]",
"/html/body/div[2]",
"/html/body/div[3]",
"/html/body/div[4]",
"/html/body/div[5]",
"/html/body/div[6]",
"/html/body/div[7]",
"/html/body/div[8]",
"/html/body/div[9]",
"/html/body/div[10]",
"/html/body/div[11]",
"/html/body/div[12]",
"/html/body/div[13]",
"/html/body/div[14]",
"/html/body/div[15]",
"/html/body/div[16]",
"/html/body/div[17]",
"/html/body/div[41]",
"/html/body/div[42]",
"/html/body/div[43]",
"/html/body/div[44]",
"/html/body/div[45]"
]
}
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,28 @@ <h3>Color using inline code Tests</h3>
passedXpaths: [
],
failedXpaths: [
"/html/body/div[1]",
"/html/body/div[2]",
"/html/body/div[3]",
"/html/body/div[4]",
"/html/body/div[5]",
"/html/body/div[6]",
"/html/body/div[7]",
"/html/body/div[8]",
"/html/body/div[9]",
"/html/body/div[10]",
"/html/body/div[11]",
"/html/body/div[12]",
"/html/body/div[13]",
"/html/body/div[14]",
"/html/body/div[15]",
"/html/body/div[16]",
"/html/body/div[17]",
"/html/body/div[41]",
"/html/body/div[42]",
"/html/body/div[43]",
"/html/body/div[44]",
"/html/body/div[45]"
]
}
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,26 @@
UnitTest = {
ruleIds: ["text_contrast_sufficient"],
results: [

{
"ruleId": "IBMA_Color_Contrast_WCAG2AA",
"value": [
"INFORMATION",
"POTENTIAL"
],
"path": {
"dom": "/html[1]/body[1]/p[1]",
"aria": "/document[1]/paragraph[1]"
},
"reasonId": "Potential_graphic_background",
"message": "Verify the contrast ratio of the text against the lightest and the darkest colors of the background meets the WCAG 2.1 AA minimum requirements for text of size 16px and weight of 400",
"messageArgs": [
"1.00",
16,
400
],
"apiArgs": [],
"category": "Accessibility"
}
]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,26 @@
UnitTest = {
ruleIds: ["text_contrast_sufficient"],
results: [

{
"ruleId": "IBMA_Color_Contrast_WCAG2AA",
"value": [
"INFORMATION",
"POTENTIAL"
],
"path": {
"dom": "/html[1]/body[1]/p[1]",
"aria": "/document[1]/paragraph[1]"
},
"reasonId": "Potential_graphic_background",
"message": "Verify the contrast ratio of the text against the lightest and the darkest colors of the background meets the WCAG 2.1 AA minimum requirements for text of size 16px and weight of 400",
"messageArgs": [
"2.82",
16,
400
],
"apiArgs": [],
"category": "Accessibility"
}
]
}

Expand Down
Loading