UNPKG

@qualweb/act-rules

Version:

ACT rules module for qualweb web accessibility evaluator

365 lines (364 loc) 17.2 kB
"use strict"; 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);