UNPKG

@helptheweb/accessibility-engine

Version:

Custom accessibility testing engine for WCAG compliance

159 lines (136 loc) 4.54 kB
/** * Color contrast calculation utilities */ /** * Calculate contrast ratio between two colors * @param {string} fgColor - Foreground color * @param {string} bgColor - Background color * @returns {number} Contrast ratio */ export function calculateContrast(fgColor, bgColor) { const rgb1 = parseColor(fgColor); const rgb2 = parseColor(bgColor); const l1 = relativeLuminance(rgb1); const l2 = relativeLuminance(rgb2); const lighter = Math.max(l1, l2); const darker = Math.min(l1, l2); return (lighter + 0.05) / (darker + 0.05); } /** * Parse color string to RGB values * @param {string} color - Color string (rgb, rgba, or hex) * @returns {object} RGB values */ export function parseColor(color) { if (!color || typeof color !== 'string') { return { r: 0, g: 0, b: 0 }; } // Handle rgb/rgba const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (rgbMatch) { return { r: parseInt(rgbMatch[1]), g: parseInt(rgbMatch[2]), b: parseInt(rgbMatch[3]) }; } // Handle hex colors const hexMatch = color.match(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i); if (hexMatch) { let hex = hexMatch[1]; if (hex.length === 3) { hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; } return { r: parseInt(hex.substr(0, 2), 16), g: parseInt(hex.substr(2, 2), 16), b: parseInt(hex.substr(4, 2), 16) }; } // Default to black if unable to parse return { r: 0, g: 0, b: 0 }; } /** * Calculate relative luminance * @param {object} rgb - RGB color values * @returns {number} Relative luminance */ export function relativeLuminance(rgb) { const rsRGB = rgb.r / 255; const gsRGB = rgb.g / 255; const bsRGB = rgb.b / 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; } /** * Get background color considering transparency and inheritance * @param {Element} element - DOM element * @returns {string} Computed background color */ export function getBackgroundColor(element) { // Get window from element's document const elementWindow = element.ownerDocument.defaultView || element.ownerDocument.parentWindow; if (!elementWindow) { return 'rgb(255, 255, 255)'; } let bgColor = elementWindow.getComputedStyle(element).backgroundColor; let currentElement = element; // Traverse up the DOM tree to find non-transparent background while (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)') { currentElement = currentElement.parentElement; if (!currentElement || currentElement === element.ownerDocument.body) { // Assume white background if we reach body return 'rgb(255, 255, 255)'; } bgColor = elementWindow.getComputedStyle(currentElement).backgroundColor; } return bgColor; } /** * Check if element has sufficient contrast * @param {Element} element - DOM element * @param {number} minRatio - Minimum contrast ratio required * @returns {object} Result with passed status and contrast value */ export function checkElementContrast(element, minRatio = 4.5) { const elementWindow = element.ownerDocument.defaultView || element.ownerDocument.parentWindow; if (!elementWindow) { return { passed: true, incomplete: true, message: 'Unable to compute styles in this environment' }; } const style = elementWindow.getComputedStyle(element); const fgColor = style.color; const bgColor = getBackgroundColor(element); const contrast = calculateContrast(fgColor, bgColor); return { passed: contrast >= minRatio, contrast, foreground: fgColor, background: bgColor, requiredRatio: minRatio }; } /** * Safe getComputedStyle that works in Node/JSDOM */ export function safeGetComputedStyle(element) { const elementWindow = element.ownerDocument.defaultView || element.ownerDocument.parentWindow; if (!elementWindow || !elementWindow.getComputedStyle) { // Return a mock object with default values return { color: 'rgb(0, 0, 0)', backgroundColor: 'rgb(255, 255, 255)', display: 'block', visibility: 'visible', fontSize: '16px', fontWeight: 'normal', getPropertyValue: () => '' }; } return elementWindow.getComputedStyle(element); }