UNPKG

@qualweb/act-rules

Version:

ACT rules module for qualweb web accessibility evaluator

282 lines (281 loc) 13.9 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_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);