@qualweb/act-rules
Version:
ACT rules module for qualweb web accessibility evaluator
365 lines (364 loc) • 17.2 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QW_ACT_R76 = void 0;
const applicability_1 = require("@qualweb/util/applicability");
const evaluation_1 = require("@qualweb/core/evaluation");
const AtomicRule_object_1 = require("../lib/AtomicRule.object");
const colorjs_io_1 = __importDefault(require("colorjs.io"));
class QW_ACT_R76 extends AtomicRule_object_1.AtomicRule {
execute(element) {
const disabledWidgets = window.disabledWidgets;
const test = new evaluation_1.Test();
const visible = window.DomUtils.isElementVisible(element);
if (!visible) {
return;
}
const hasTextNode = element.hasTextNode();
const elementText = element.getElementOwnText();
if (!hasTextNode && elementText.trim() === '') {
return;
}
const isHTML = element.isElementHTMLElement();
if (!isHTML) {
return;
}
const elementSelectors = element.getElementSelector();
for (const disableWidget of disabledWidgets || []) {
const selectors = window.AccessibilityUtils.getAccessibleNameSelector(disableWidget);
if (disableWidget && selectors && selectors.includes(elementSelectors)) {
return;
}
if (disableWidget.getElementSelector() === elementSelectors) {
return;
}
}
const role = window.AccessibilityUtils.getElementRole(element);
if (role === 'group') {
const disable = element.getElementAttribute('disabled') !== null;
const ariaDisable = element.getElementAttribute('aria-disabled') !== null;
if (disable || ariaDisable) {
return;
}
}
const fgColor = element.getElementStyleProperty('color', null);
let bgColor = this.getBackground(element);
const opacity = parseFloat(element.getElementStyleProperty('opacity', null));
const fontSize = element.getElementStyleProperty('font-size', null);
const fontWeight = element.getElementStyleProperty('font-weight', null);
const fontFamily = element.getElementStyleProperty('font-family', null);
const fontStyle = element.getElementStyleProperty('font-style', null);
const textShadow = element.getElementStyleProperty('text-shadow', null);
if (textShadow.trim() !== 'none') {
const properties = textShadow.trim().split(' ');
if (properties.length === 6) {
const vs = parseInt(properties[3], 0);
const hs = parseInt(properties[4], 0);
const blur = parseInt(properties[5], 0);
const validateTextShadow = vs === 0 && hs === 0 && blur > 0 && blur <= 15;
if (validateTextShadow) {
test.verdict = evaluation_1.Verdict.WARNING;
test.resultCode = 'W1';
test.addElement(element);
this.addTestResult(test);
return;
}
}
}
if (this.isImage(bgColor)) {
test.verdict = evaluation_1.Verdict.WARNING;
test.resultCode = 'W2';
test.addElement(element);
this.addTestResult(test);
return;
}
const regexGradient = /((\w-?)*gradient.*)/gm;
let regexGradientMatches = bgColor.match(regexGradient);
if (regexGradientMatches) {
if (this.isHumanLanguage(elementText)) {
const parsedGradientString = regexGradientMatches[0];
this.evaluateGradient(test, element, parsedGradientString, fgColor, opacity, fontSize, fontWeight, fontStyle, fontFamily, elementText);
}
else {
test.verdict = evaluation_1.Verdict.PASSED;
test.resultCode = 'P2';
test.addElement(element);
this.addTestResult(test);
}
}
else {
let parsedBG = this.parseRGBString(bgColor, opacity);
let elementAux = element;
let opacityAUX;
while (parsedBG === undefined ||
(parsedBG.red === 0 && parsedBG.green === 0 && parsedBG.blue === 0 && parsedBG.alpha === 0)) {
const parent = elementAux.getElementParent();
if (parent) {
bgColor = this.getBackground(parent);
if (this.isImage(bgColor)) {
test.verdict = evaluation_1.Verdict.WARNING;
test.resultCode = 'W2';
test.addElement(element);
this.addTestResult(test);
return;
}
else {
regexGradientMatches = bgColor.match(regexGradient);
if (regexGradientMatches) {
const parsedGradientString = regexGradientMatches[0];
if (this.evaluateGradient(test, element, parsedGradientString, fgColor, opacity, fontSize, fontWeight, fontStyle, fontFamily, elementText)) {
return;
}
}
else {
opacityAUX = parseFloat(parent.getElementStyleProperty('opacity', null));
parsedBG = this.parseRGBString(parent.getElementStyleProperty('background', null), opacityAUX);
elementAux = parent;
}
}
}
else {
break;
}
}
if (parsedBG === undefined ||
(parsedBG.red === 0 && parsedBG.green === 0 && parsedBG.blue === 0 && parsedBG.alpha === 0)) {
parsedBG = { red: 255, green: 255, blue: 255, alpha: 1 };
}
const parsedFG = this.parseRGBString(fgColor, opacity);
if (!this.equals(parsedBG, parsedFG)) {
if (this.isHumanLanguage(elementText)) {
const contrastRatio = this.getContrast(parsedBG, parsedFG);
const isValid = this.hasValidContrastRatio(contrastRatio, fontSize, this.isBold(fontWeight));
if (isValid) {
test.verdict = evaluation_1.Verdict.PASSED;
test.resultCode = 'P1';
test.addElement(element);
this.addTestResult(test);
}
else {
test.verdict = evaluation_1.Verdict.FAILED;
test.resultCode = 'F1';
test.addElement(element);
this.addTestResult(test);
}
}
else {
test.verdict = evaluation_1.Verdict.PASSED;
test.resultCode = 'P2';
test.addElement(element);
this.addTestResult(test);
}
}
}
}
getBackground(element) {
const backgroundImage = element.getElementStyleProperty('background-image', null);
if (backgroundImage === 'none') {
let bg = element.getElementStyleProperty('background', null);
if (bg === '') {
bg = element.getElementStyleProperty('background-color', null);
}
return bg;
}
else {
return backgroundImage;
}
}
isImage(color) {
return (color.toLowerCase().includes('.jpeg') ||
color.toLowerCase().includes('.jpg') ||
color.toLowerCase().includes('.png') ||
color.toLowerCase().includes('.svg'));
}
evaluateGradient(test, element, parsedGradientString, fgColor, opacity, fontSize, fontWeight, fontStyle, fontFamily, elementText) {
if (parsedGradientString.startsWith('linear-gradient')) {
const gradientDirection = this.getGradientDirection(parsedGradientString);
if (gradientDirection === 'to right') {
const colors = this.parseGradientString(parsedGradientString, opacity);
let isValid = true;
let contrastRatio;
const textSize = this.getTextSize(fontFamily.toLowerCase().replace(/['"]+/g, ''), parseInt(fontSize.replace('px', '')), this.isBold(fontWeight), fontStyle.toLowerCase().includes('italic'), elementText);
if (textSize !== -1) {
const elementWidth = element.getElementStyleProperty('width', null);
const lastCharRatio = textSize / parseInt(elementWidth.replace('px', ''));
const lastCharBgColor = this.getColorInGradient(colors[0], colors[colors.length - 1], lastCharRatio);
contrastRatio = this.getContrast(colors[0], this.parseRGBString(fgColor, opacity));
isValid = isValid && this.hasValidContrastRatio(contrastRatio, fontSize, this.isBold(fontWeight));
contrastRatio = this.getContrast(lastCharBgColor, this.parseRGBString(fgColor, opacity));
isValid = isValid && this.hasValidContrastRatio(contrastRatio, fontSize, this.isBold(fontWeight));
}
else {
for (const color of colors) {
contrastRatio = this.getContrast(color, this.parseRGBString(fgColor, opacity));
isValid = isValid && this.hasValidContrastRatio(contrastRatio, fontSize, this.isBold(fontWeight));
}
}
if (isValid) {
test.verdict = evaluation_1.Verdict.PASSED;
test.resultCode = 'P3';
}
else {
test.verdict = evaluation_1.Verdict.FAILED;
test.resultCode = 'F2';
}
}
else if (gradientDirection === 'to left') {
test.verdict = evaluation_1.Verdict.WARNING;
test.resultCode = 'W3';
}
else {
test.verdict = evaluation_1.Verdict.WARNING;
test.resultCode = 'W3';
}
}
else {
test.resultCode = 'W3';
}
test.addElement(element);
this.addTestResult(test);
return true;
}
isHumanLanguage(text) {
return window.DomUtils.isHumanLanguage(text);
}
equals(color1, color2) {
return (color1.red === color2.red &&
color1.green === color2.green &&
color1.blue === color2.blue &&
color1.alpha === color2.alpha);
}
getGradientDirection(gradient) {
const direction = gradient.replace('linear-gradient(', '').split(',')[0];
if (direction) {
if (direction === '90deg')
return 'to right';
if (direction === '-90deg')
return 'to left';
return direction;
}
return undefined;
}
parseGradientString(gradient, opacity) {
const regex = /rgb(a?)\((\d+), (\d+), (\d+)+(, +(\d)+)?\)/gm;
const colorsMatch = gradient.match(regex);
const colors = [];
for (const stringColor of colorsMatch || []) {
colors.push(this.parseRGBString(stringColor, opacity));
}
return colors;
}
parseRGBString(colorString, opacity) {
const rgbRegex = /^rgb\((\d+), (\d+), (\d+)\)/;
const rgbaRegex = /^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/;
const oklchRegex = /^oklch\((\d*(\.\d+)?) (\d*(\.\d+)?) (\d*(\.\d+)?)\)/;
const oklch2Regex = /^oklch\((\d*(\.\d+)?) (\d*(\.\d+)?) (\d*(\.\d+)?) \/ (\d*(\.\d+)?)\)/;
if (colorString === 'transparent') {
return { red: 0, green: 0, blue: 0, alpha: 0 };
}
let match = colorString.match(rgbRegex);
if (match) {
return {
red: parseInt(match[1], 10),
green: parseInt(match[2], 10),
blue: parseInt(match[3], 10),
alpha: opacity
};
}
match = colorString.match(rgbaRegex);
if (match) {
return {
red: parseInt(match[1], 10),
green: parseInt(match[2], 10),
blue: parseInt(match[3], 10),
alpha: Math.round(parseFloat(match[4]) * 100) / 100
};
}
match = colorString.match(oklch2Regex);
if (match) {
const oklchColor = new colorjs_io_1.default("oklch", [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])]);
const rgba = oklchColor.to("srgb");
return {
red: rgba.srgb.red,
green: rgba.srgb.green,
blue: rgba.srgb.blue,
alpha: parseFloat(match[4])
};
}
match = colorString.match(oklchRegex);
if (match) {
const oklchColor = new colorjs_io_1.default("oklch", [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])]);
const rgba = oklchColor.to("srgb");
return {
red: rgba.srgb.red,
green: rgba.srgb.green,
blue: rgba.srgb.blue,
alpha: rgba.alpha
};
}
}
getRelativeLuminance(red, green, blue) {
const rSRGB = red / 255;
const gSRGB = green / 255;
const bSRGB = blue / 255;
const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow((rSRGB + 0.055) / 1.055, 2.4);
const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow((gSRGB + 0.055) / 1.055, 2.4);
const b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow((bSRGB + 0.055) / 1.055, 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
flattenColors(fgColor, bgColor) {
const fgAlpha = fgColor['alpha'];
const red = (1 - fgAlpha) * bgColor['red'] + fgAlpha * fgColor['red'];
const green = (1 - fgAlpha) * bgColor['green'] + fgAlpha * fgColor['green'];
const blue = (1 - fgAlpha) * bgColor['blue'] + fgAlpha * fgColor['blue'];
const alpha = fgColor['alpha'] + bgColor['alpha'] * (1 - fgColor['alpha']);
return { red: red, green: green, blue: blue, alpha: alpha };
}
isBold(fontWeight) {
return !!fontWeight && ['bold', 'bolder', '700', '800', '900'].includes(fontWeight);
}
getContrast(bgColor, fgColor) {
if (fgColor.alpha < 1) {
fgColor = this.flattenColors(fgColor, bgColor);
}
const bL = this.getRelativeLuminance(bgColor['red'], bgColor['green'], bgColor['blue']);
const fL = this.getRelativeLuminance(fgColor['red'], fgColor['green'], fgColor['blue']);
return (Math.max(fL, bL) + 0.05) / (Math.min(fL, bL) + 0.05);
}
hasValidContrastRatio(contrast, fontSize, isBold) {
const isSmallFont = (isBold && parseFloat(fontSize) < 18.6667) || (!isBold && parseFloat(fontSize) < 24);
const expectedContrastRatio = isSmallFont ? 7 : 4.5;
return contrast >= expectedContrastRatio;
}
getTextSize(font, fontSize, bold, italic, text) {
return window.DomUtils.getTextSize(font, fontSize, bold, italic, text);
}
getColorInGradient(fromColor, toColor, ratio) {
const red = fromColor['red'] + (toColor['red'] - fromColor['red']) * ratio;
const green = fromColor['green'] + (toColor['green'] - fromColor['green']) * ratio;
const blue = fromColor['blue'] + (toColor['blue'] - fromColor['blue']) * ratio;
return { red: red, green: green, blue: blue, alpha: 1 };
}
}
exports.QW_ACT_R76 = QW_ACT_R76;
__decorate([
applicability_1.ElementExists,
applicability_1.ElementIsHTMLElement,
(0, applicability_1.ElementIsNot)(['html', 'head', 'body', 'script', 'style', 'meta']),
applicability_1.ElementIsVisible,
applicability_1.ElementHasText,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Function]),
__metadata("design:returntype", void 0)
], QW_ACT_R76.prototype, "execute", null);