@qualweb/act-rules
Version:
ACT rules module for qualweb web accessibility evaluator
282 lines (281 loc) • 13.9 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_R37 = 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_R37 extends AtomicRule_object_1.AtomicRule {
execute(element) {
var _a;
const visible = window.DomUtils.isElementVisible(element);
if (!visible)
return;
const nodeName = element.getElementTagName();
const isInputField = ['input', 'select', 'textarea'].includes(nodeName);
const elementText = element.getElementOwnText().trim();
const placeholder = (_a = element.getElementAttribute('placeholder')) === null || _a === void 0 ? void 0 : _a.trim();
if (elementText === '' && !isInputField && !placeholder) {
return;
}
if (!element.isElementHTMLElement())
return;
const disabledWidgets = window.disabledWidgets;
const elementSelectors = element.getElementSelector();
for (const disableWidget of disabledWidgets || []) {
const selectorsResult = window.AccessibilityUtils.getAccessibleNameSelector(disableWidget);
const selectors = typeof selectorsResult === 'string' ? [selectorsResult] : selectorsResult;
if (disableWidget && selectors && selectors.includes(elementSelectors))
return;
if (disableWidget.getElementSelector() === elementSelectors)
return;
const children = disableWidget.getElementChildren();
if (children) {
for (const child of children) {
if (child.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 test = new evaluation_1.Test();
const fgColor = element.getElementStyleProperty('color', null);
let bgColor = this.getBackground(element);
const opacity = parseFloat(element.getElementStyleProperty('opacity', null) || '1');
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 && textShadow.trim() !== 'none' && textShadow.trim() !== '') {
const pixelValues = textShadow.match(/(-?\d+px)/g);
if (pixelValues && pixelValues.length >= 3) {
const hs = Math.abs(parseInt(pixelValues[0].replace('px', ''), 10));
const vs = Math.abs(parseInt(pixelValues[1].replace('px', ''), 10));
const blur = parseInt(pixelValues[2].replace('px', ''), 10);
if (blur > 0 || hs > 1 || vs > 1) {
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 || placeholder || "")) {
this.evaluateGradient(test, element, regexGradientMatches[0], fgColor, opacity, fontSize, fontWeight, fontStyle, fontFamily, elementText || placeholder || "");
}
else {
test.verdict = evaluation_1.Verdict.PASSED;
test.resultCode = 'P2';
test.addElement(element);
this.addTestResult(test);
}
return;
}
let parsedBG = this.parseRGBString(bgColor);
if (parsedBG)
parsedBG.alpha *= opacity;
let elementAux = element;
while (!parsedBG || (parsedBG.red === 0 && parsedBG.green === 0 && parsedBG.blue === 0 && parsedBG.alpha === 0)) {
const parent = elementAux.getElementParent();
if (!parent)
break;
const parentOpacity = parseFloat(parent.getElementStyleProperty('opacity', null) || '1');
parsedBG = this.parseRGBString(this.getBackground(parent));
if (parsedBG)
parsedBG.alpha *= parentOpacity;
elementAux = parent;
}
if (!parsedBG || parsedBG.alpha === 0) {
parsedBG = { red: 255, green: 255, blue: 255, alpha: 1 };
}
if (parsedBG.alpha < 1) {
parsedBG = this.flattenColors(parsedBG, { red: 255, green: 255, blue: 255, alpha: 1 });
}
const parsedFG = this.parseRGBString(fgColor);
if (parsedFG) {
parsedFG.alpha *= opacity;
if (!this.equals(parsedBG, parsedFG)) {
const textToVerify = elementText || placeholder || "";
if (this.isHumanLanguage(textToVerify)) {
const contrastRatio = this.getContrast(parsedBG, parsedFG);
const isValid = this.hasValidContrastRatio(contrastRatio, fontSize, this.isBold(fontWeight));
test.verdict = isValid ? evaluation_1.Verdict.PASSED : evaluation_1.Verdict.FAILED;
test.resultCode = isValid ? 'P1' : 'F1';
test.addElement(element);
this.addTestResult(test);
}
else {
test.verdict = evaluation_1.Verdict.PASSED;
test.resultCode = 'P2';
test.addElement(element);
this.addTestResult(test);
}
}
}
}
isHumanLanguage(text) {
return window.DomUtils.isHumanLanguage(text);
}
evaluateGradient(test, element, parsedGradientString, fgColor, opacity, fontSize, fontWeight, fontStyle, fontFamily, elementText) {
if (parsedGradientString.startsWith('linear-gradient')) {
const colors = this.parseGradientString(parsedGradientString);
let isValid = true;
const parsedFG = this.parseRGBString(fgColor);
if (!parsedFG)
return false;
parsedFG.alpha *= opacity;
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);
isValid = isValid && this.hasValidContrastRatio(this.getContrast(colors[0], parsedFG), fontSize, this.isBold(fontWeight));
isValid = isValid && this.hasValidContrastRatio(this.getContrast(lastCharBgColor, parsedFG), fontSize, this.isBold(fontWeight));
}
else {
for (const color of colors) {
isValid = isValid && this.hasValidContrastRatio(this.getContrast(color, parsedFG), fontSize, this.isBold(fontWeight));
}
}
test.verdict = isValid ? evaluation_1.Verdict.PASSED : evaluation_1.Verdict.FAILED;
test.resultCode = isValid ? 'P3' : 'F2';
}
else {
test.verdict = evaluation_1.Verdict.WARNING;
test.resultCode = 'W3';
}
test.addElement(element);
this.addTestResult(test);
return true;
}
parseGradientString(gradient) {
const regex = /rgb(a?)\((\d+), (\d+), (\d+)+(, +(\d)+)?\)/gm;
const colorsMatch = gradient.match(regex);
const colors = [];
for (const stringColor of colorsMatch || []) {
const parsed = this.parseRGBString(stringColor);
if (parsed)
colors.push(parsed);
}
return colors;
}
getColorInGradient(fromColor, toColor, ratio) {
return {
red: fromColor.red + (toColor.red - fromColor.red) * ratio,
green: fromColor.green + (toColor.green - fromColor.green) * ratio,
blue: fromColor.blue + (toColor.blue - fromColor.blue) * ratio,
alpha: 1
};
}
getTextSize(font, fontSize, bold, italic, text) {
return window.DomUtils.getTextSize(font, fontSize, bold, italic, text);
}
getBackground(element) {
const bgImg = element.getElementStyleProperty('background-image', null);
if (bgImg && bgImg !== 'none' && bgImg !== '')
return bgImg;
const bgColor = element.getElementStyleProperty('background-color', null);
return (bgColor && bgColor !== '' && bgColor !== 'transparent') ? bgColor : element.getElementStyleProperty('background', null);
}
isImage(s) {
const lower = s.toLowerCase();
return lower.includes('.jpg') || lower.includes('.png') || lower.includes('.svg') || lower.includes('url(');
}
parseRGBString(colorString) {
var _a;
if (!colorString || colorString === 'transparent' || colorString === 'none')
return { red: 0, green: 0, blue: 0, alpha: 0 };
const rgb = colorString.match(/^rgb\((\d+), (\d+), (\d+)\)/);
if (rgb)
return { red: parseInt(rgb[1]), green: parseInt(rgb[2]), blue: parseInt(rgb[3]), alpha: 1.0 };
const rgba = colorString.match(/^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/);
if (rgba)
return { red: parseInt(rgba[1]), green: parseInt(rgba[2]), blue: parseInt(rgba[3]), alpha: Math.round(parseFloat(rgba[4]) * 100) / 100 };
try {
const color = new colorjs_io_1.default(colorString);
const srgb = color.to('srgb');
return {
red: Math.round(srgb.coords[0] * 255),
green: Math.round(srgb.coords[1] * 255),
blue: Math.round(srgb.coords[2] * 255),
alpha: (_a = color.alpha) !== null && _a !== void 0 ? _a : 1
};
}
catch (e) {
return undefined;
}
}
flattenColors(fg, bg) {
const alpha = fg.alpha;
return {
red: Math.round((1 - alpha) * bg.red + alpha * fg.red),
green: Math.round((1 - alpha) * bg.green + alpha * fg.green),
blue: Math.round((1 - alpha) * bg.blue + alpha * fg.blue),
alpha: fg.alpha + bg.alpha * (1 - fg.alpha)
};
}
getContrast(bg, fg) {
const finalFG = fg.alpha < 1 ? this.flattenColors(fg, bg) : fg;
const L1 = this.getLuminance(bg);
const L2 = this.getLuminance(finalFG);
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
}
getLuminance(c) {
const a = [c.red, c.green, c.blue].map(v => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
hasValidContrastRatio(contrast, fontSize, isBold) {
const size = parseFloat(fontSize);
const threshold = (isBold && size >= 18.66) || size >= 24 ? 3 : 4.5;
return (contrast + 0.02) >= threshold;
}
isBold(fontWeight) {
return !!fontWeight && ['bold', 'bolder', '700', '800', '900'].includes(fontWeight);
}
equals(c1, c2) {
return c1.red === c2.red && c1.green === c2.green && c1.blue === c2.blue && c1.alpha === c2.alpha;
}
}
exports.QW_ACT_R37 = QW_ACT_R37;
__decorate([
applicability_1.ElementExists,
applicability_1.ElementIsHTMLElement,
(0, applicability_1.ElementIsNot)(['html', 'head', 'body', 'script', 'style', 'meta']),
applicability_1.ElementIsVisible,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Function]),
__metadata("design:returntype", void 0)
], QW_ACT_R37.prototype, "execute", null);