From 6cee221be027b5362659748defe588be2cf6e341 Mon Sep 17 00:00:00 2001 From: Arjun Chikara Date: Tue, 22 Apr 2025 15:16:21 +0530 Subject: [PATCH 1/4] null handling for color contrast --- lib/checks/color/color-contrast-evaluate.js | 1 + lib/commons/dom/get-rect-stack.js | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index 8916bd097..7c023c95c 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -82,6 +82,7 @@ export default function colorContrastEvaluate(node, options, virtualNode) { } const bgNodes = []; + axe._cache.set('ruleId', 'axe-color-contrast'); const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax); const fgColor = getForegroundColor(node, false, bgColor, options); diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index 36489a504..953577034 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -1,5 +1,6 @@ import visuallySort from './visually-sort'; import { getRectCenter } from '../math'; +import cache from '../../core/base/cache'; // Additional props isCoordsPassed, x, y for a11y-engine-domforge export function getRectStack( @@ -41,7 +42,12 @@ export function getRectStack( }); const gridContainer = grid.container; - if (gridContainer) { + //adding just if color contrast is being run then only then the extra added condition should run + if ( + gridContainer && + (!(cache.get('ruleId') && cache.get('ruleId') === 'axe-color-contrast') || + gridContainer._grid) + ) { stack = getRectStack( gridContainer._grid, gridContainer.boundingClientRect, From ceddad8f43032632c63c38facdac2bb755c97809 Mon Sep 17 00:00:00 2001 From: Arjun Chikara Date: Tue, 22 Apr 2025 17:24:33 +0530 Subject: [PATCH 2/4] added guard rails for color contrast --- lib/checks/color/color-contrast-evaluate.js | 278 ++++++++++---------- lib/commons/dom/get-rect-stack.js | 7 +- 2 files changed, 142 insertions(+), 143 deletions(-) diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index 7c023c95c..739b37e25 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -17,157 +17,161 @@ import { import { memoize } from '../../core/utils'; export default function colorContrastEvaluate(node, options, virtualNode) { - const { - ignoreUnicode, - ignoreLength, - ignorePseudo, - boldValue, - boldTextPt, - largeTextPt, - contrastRatio, - shadowOutlineEmMax, - pseudoSizeThreshold - } = options; - - if (!isVisibleOnScreen(node)) { - this.data({ messageKey: 'hidden' }); - return true; - } + try { + const { + ignoreUnicode, + ignoreLength, + ignorePseudo, + boldValue, + boldTextPt, + largeTextPt, + contrastRatio, + shadowOutlineEmMax, + pseudoSizeThreshold + } = options; + + if (!isVisibleOnScreen(node)) { + this.data({ messageKey: 'hidden' }); + return true; + } - const visibleText = visibleVirtual(virtualNode, false, true); - if (ignoreUnicode && textIsEmojis(visibleText)) { - this.data({ messageKey: 'nonBmp' }); - return undefined; - } + const visibleText = visibleVirtual(virtualNode, false, true); + if (ignoreUnicode && textIsEmojis(visibleText)) { + this.data({ messageKey: 'nonBmp' }); + return undefined; + } - const nodeStyle = window.getComputedStyle(node); - const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size')); - const fontWeight = nodeStyle.getPropertyValue('font-weight'); - const bold = parseFloat(fontWeight) >= boldValue || fontWeight === 'bold'; - - const ptSize = Math.ceil(fontSize * 72) / 96; - const isSmallFont = - (bold && ptSize < boldTextPt) || (!bold && ptSize < largeTextPt); - - const { expected, minThreshold, maxThreshold } = isSmallFont - ? contrastRatio.normal - : contrastRatio.large; - - // if element or a parent has pseudo content then we need to mark - // as needs review - const pseudoElm = findPseudoElement(virtualNode, { - ignorePseudo, - pseudoSizeThreshold - }); - if (pseudoElm) { - this.data({ - fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`, - fontWeight: bold ? 'bold' : 'normal', - messageKey: 'pseudoContent', - expectedContrastRatio: expected + ':1' + const nodeStyle = window.getComputedStyle(node); + const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size')); + const fontWeight = nodeStyle.getPropertyValue('font-weight'); + const bold = parseFloat(fontWeight) >= boldValue || fontWeight === 'bold'; + + const ptSize = Math.ceil(fontSize * 72) / 96; + const isSmallFont = + (bold && ptSize < boldTextPt) || (!bold && ptSize < largeTextPt); + + const { expected, minThreshold, maxThreshold } = isSmallFont + ? contrastRatio.normal + : contrastRatio.large; + + // if element or a parent has pseudo content then we need to mark + // as needs review + const pseudoElm = findPseudoElement(virtualNode, { + ignorePseudo, + pseudoSizeThreshold }); + if (pseudoElm) { + this.data({ + fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`, + fontWeight: bold ? 'bold' : 'normal', + messageKey: 'pseudoContent', + expectedContrastRatio: expected + ':1' + }); + + this.relatedNodes(pseudoElm.actualNode); + return undefined; + } - this.relatedNodes(pseudoElm.actualNode); - return undefined; - } + // Thin shadows only. Thicker shadows are included in the background instead + const shadowColors = getTextShadowColors(node, { + minRatio: 0.001, + maxRatio: shadowOutlineEmMax + }); + if (shadowColors === null) { + this.data({ messageKey: 'complexTextShadows' }); + return undefined; + } - // Thin shadows only. Thicker shadows are included in the background instead - const shadowColors = getTextShadowColors(node, { - minRatio: 0.001, - maxRatio: shadowOutlineEmMax - }); - if (shadowColors === null) { - this.data({ messageKey: 'complexTextShadows' }); - return undefined; - } + const bgNodes = []; + const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax); + const fgColor = getForegroundColor(node, false, bgColor, options); + + let contrast = null; + let contrastContributor = null; + let shadowColor = null; + if (shadowColors.length === 0) { + contrast = getContrast(bgColor, fgColor); + } else if (fgColor && bgColor) { + shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors); + // Compare shadow, bgColor, textColor. Check passes if any is sufficient + const fgBgContrast = getContrast(bgColor, fgColor); + const bgShContrast = getContrast(bgColor, shadowColor); + const fgShContrast = getContrast(shadowColor, fgColor); + contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast); + if (contrast !== fgBgContrast) { + contrastContributor = + bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor'; + } + } - const bgNodes = []; - axe._cache.set('ruleId', 'axe-color-contrast'); - const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax); - const fgColor = getForegroundColor(node, false, bgColor, options); - - let contrast = null; - let contrastContributor = null; - let shadowColor = null; - if (shadowColors.length === 0) { - contrast = getContrast(bgColor, fgColor); - } else if (fgColor && bgColor) { - shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors); - // Compare shadow, bgColor, textColor. Check passes if any is sufficient - const fgBgContrast = getContrast(bgColor, fgColor); - const bgShContrast = getContrast(bgColor, shadowColor); - const fgShContrast = getContrast(shadowColor, fgColor); - contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast); - if (contrast !== fgBgContrast) { - contrastContributor = - bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor'; + const isValid = contrast > expected; + + // ratio is outside range + if ( + (typeof minThreshold === 'number' && + (typeof contrast !== 'number' || contrast < minThreshold)) || + (typeof maxThreshold === 'number' && + (typeof contrast !== 'number' || contrast > maxThreshold)) + ) { + this.data({ contrastRatio: contrast }); + return true; } - } - const isValid = contrast > expected; - - // ratio is outside range - if ( - (typeof minThreshold === 'number' && - (typeof contrast !== 'number' || contrast < minThreshold)) || - (typeof maxThreshold === 'number' && - (typeof contrast !== 'number' || contrast > maxThreshold)) - ) { - this.data({ contrastRatio: contrast }); - return true; - } + // truncate ratio to three digits while rounding down + // 4.499 = 4.49, 4.019 = 4.01 + const truncatedResult = Math.floor(contrast * 100) / 100; - // truncate ratio to three digits while rounding down - // 4.499 = 4.49, 4.019 = 4.01 - const truncatedResult = Math.floor(contrast * 100) / 100; + // if fgColor or bgColor are missing, get more information. + let missing; + if (bgColor === null) { + missing = incompleteData.get('bgColor'); + } else if (!isValid) { + missing = contrastContributor; + } - // if fgColor or bgColor are missing, get more information. - let missing; - if (bgColor === null) { - missing = incompleteData.get('bgColor'); - } else if (!isValid) { - missing = contrastContributor; - } + const equalRatio = truncatedResult === 1; + const shortTextContent = visibleText.length === 1; + if (equalRatio) { + missing = incompleteData.set('bgColor', 'equalRatio'); + } else if (!isValid && shortTextContent && !ignoreLength) { + // Check that the text content is a single character long + missing = 'shortTextContent'; + } - const equalRatio = truncatedResult === 1; - const shortTextContent = visibleText.length === 1; - if (equalRatio) { - missing = incompleteData.set('bgColor', 'equalRatio'); - } else if (!isValid && shortTextContent && !ignoreLength) { - // Check that the text content is a single character long - missing = 'shortTextContent'; - } + // need both independently in case both are missing + this.data({ + fgColor: fgColor ? fgColor.toHexString() : undefined, + bgColor: bgColor ? bgColor.toHexString() : undefined, + contrastRatio: truncatedResult, + fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`, + fontWeight: bold ? 'bold' : 'normal', + messageKey: missing, + expectedContrastRatio: expected + ':1', + shadowColor: shadowColor ? shadowColor.toHexString() : undefined + }); - // need both independently in case both are missing - this.data({ - fgColor: fgColor ? fgColor.toHexString() : undefined, - bgColor: bgColor ? bgColor.toHexString() : undefined, - contrastRatio: truncatedResult, - fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`, - fontWeight: bold ? 'bold' : 'normal', - messageKey: missing, - expectedContrastRatio: expected + ':1', - shadowColor: shadowColor ? shadowColor.toHexString() : undefined - }); - - // We don't know, so we'll put it into Can't Tell - if ( - fgColor === null || - bgColor === null || - equalRatio || - (shortTextContent && !ignoreLength && !isValid) - ) { - missing = null; - incompleteData.clear(); - this.relatedNodes(bgNodes); - return undefined; - } + // We don't know, so we'll put it into Can't Tell + if ( + fgColor === null || + bgColor === null || + equalRatio || + (shortTextContent && !ignoreLength && !isValid) + ) { + missing = null; + incompleteData.clear(); + this.relatedNodes(bgNodes); + return undefined; + } - if (!isValid) { - this.relatedNodes(bgNodes); - } + if (!isValid) { + this.relatedNodes(bgNodes); + } - return isValid; + return isValid; + } catch (err) { + a11yEngine.axeErrorHandlers.addCheckError('axe-color-contrast-check', err); + return undefined; + } } function findPseudoElement( diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index 953577034..c479036d8 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -1,6 +1,5 @@ import visuallySort from './visually-sort'; import { getRectCenter } from '../math'; -import cache from '../../core/base/cache'; // Additional props isCoordsPassed, x, y for a11y-engine-domforge export function getRectStack( @@ -43,11 +42,7 @@ export function getRectStack( const gridContainer = grid.container; //adding just if color contrast is being run then only then the extra added condition should run - if ( - gridContainer && - (!(cache.get('ruleId') && cache.get('ruleId') === 'axe-color-contrast') || - gridContainer._grid) - ) { + if (gridContainer) { stack = getRectStack( gridContainer._grid, gridContainer.boundingClientRect, From b8d2c878d11c6eb187290f50fa0536af7b1f94c4 Mon Sep 17 00:00:00 2001 From: Arjun Chikara Date: Tue, 22 Apr 2025 17:27:54 +0530 Subject: [PATCH 3/4] Removed extra comment --- lib/commons/dom/get-rect-stack.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index c479036d8..36489a504 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -41,7 +41,6 @@ export function getRectStack( }); const gridContainer = grid.container; - //adding just if color contrast is being run then only then the extra added condition should run if (gridContainer) { stack = getRectStack( gridContainer._grid, From d10d529355247184bbfff642e9568a1ddee911ca Mon Sep 17 00:00:00 2001 From: Arjun Chikara Date: Wed, 23 Apr 2025 14:15:33 +0530 Subject: [PATCH 4/4] updated logic --- lib/checks/color/color-contrast-enhanced.json | 3 ++- lib/checks/color/color-contrast-evaluate.js | 3 ++- lib/checks/color/color-contrast.json | 3 ++- lib/checks/forms/autocomplete-a11y-evaluate.js | 6 ++++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/checks/color/color-contrast-enhanced.json b/lib/checks/color/color-contrast-enhanced.json index b09a32690..e14d8eb16 100644 --- a/lib/checks/color/color-contrast-enhanced.json +++ b/lib/checks/color/color-contrast-enhanced.json @@ -20,7 +20,8 @@ }, "pseudoSizeThreshold": 0.25, "shadowOutlineEmMax": 0.1, - "textStrokeEmMin": 0.03 + "textStrokeEmMin": 0.03, + "ruleId": "color-contrast-enhanced" }, "metadata": { "impact": "serious", diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index 739b37e25..4ccbbe33b 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -169,7 +169,8 @@ export default function colorContrastEvaluate(node, options, virtualNode) { return isValid; } catch (err) { - a11yEngine.axeErrorHandlers.addCheckError('axe-color-contrast-check', err); + this.data({ messageKey: 'Check error' }); + a11yEngine.errorHandler.addCheckError(options?.ruleId, err); return undefined; } } diff --git a/lib/checks/color/color-contrast.json b/lib/checks/color/color-contrast.json index 135eeae4e..dd5b1f5b1 100644 --- a/lib/checks/color/color-contrast.json +++ b/lib/checks/color/color-contrast.json @@ -18,7 +18,8 @@ }, "pseudoSizeThreshold": 0.25, "shadowOutlineEmMax": 0.2, - "textStrokeEmMin": 0.03 + "textStrokeEmMin": 0.03, + "ruleId": "color-contrast" }, "metadata": { "impact": "serious", diff --git a/lib/checks/forms/autocomplete-a11y-evaluate.js b/lib/checks/forms/autocomplete-a11y-evaluate.js index b635c9bf3..11d4863e9 100644 --- a/lib/checks/forms/autocomplete-a11y-evaluate.js +++ b/lib/checks/forms/autocomplete-a11y-evaluate.js @@ -1,5 +1,4 @@ import { isValidAutocomplete } from '../../commons/text'; -import ErrorHandler from '../../core/errors/error-handler'; function checkIsElementValidAutocomplete(node, options, virtualNode) { const autocomplete = virtualNode.attr('autocomplete')?.toLowerCase().trim(); @@ -67,7 +66,10 @@ function autocompleteA11yEvaluate(node, options, virtualNode) { return checkIsElementValidAutocomplete(node, options, virtualNode); } } catch (err) { - ErrorHandler.addCheckError('autocomplete-attribute-valid-check', err); + a11yEngine.errorHandler.addCheckError( + 'autocomplete-attribute-valid-check', + err + ); return undefined; } }