From 6b26c13add3695361155dd614ca5e513c98fd429 Mon Sep 17 00:00:00 2001 From: Josh at WLTechBlog Date: Tue, 9 Dec 2025 13:48:27 -0700 Subject: [PATCH] bump --- daemon/daemon.go | 101 ++++++++++-------- docs/contrast_check_fix_container_elements.md | 83 ++++++++++++++ 2 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 docs/contrast_check_fix_container_elements.md diff --git a/daemon/daemon.go b/daemon/daemon.go index a4cda9a..b39a5fc 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -9149,57 +9149,62 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con } } - // Tier 3: Gradient analysis + // Tier 3: Gradient analysis - walks up DOM tree to find gradient backgrounds function analyzeGradientContrast(element, textColor) { try { - const style = window.getComputedStyle(element); - const bgImage = style.backgroundImage; + // Walk up the DOM tree to find a gradient background + let current = element; + while (current && current !== document.documentElement.parentElement) { + const style = window.getComputedStyle(current); + const bgImage = style.backgroundImage; - // Check if background is a gradient - if (!bgImage || !bgImage.includes('gradient')) { - return null; - } + // Check if background is a gradient + if (bgImage && bgImage.includes('gradient')) { + // Extract gradient colors using regex + const colorRegex = /rgba?\([^)]+\)|#[0-9a-f]{3,6}/gi; + const matches = bgImage.match(colorRegex) || []; + // Fix incomplete rgb/rgba matches by adding closing paren + const colors = matches.map(c => c.includes('(') && !c.includes(')') ? c + ')' : c); - // Extract gradient colors using regex - const colorRegex = /rgba?\([^)]+\)|#[0-9a-f]{3,6}/gi; - const matches = bgImage.match(colorRegex) || []; - // Fix incomplete rgb/rgba matches by adding closing paren - const colors = matches.map(c => c.includes('(') && !c.includes(')') ? c + ')' : c); + if (colors.length > 0) { + // Calculate contrast against each color in the gradient + const fg = parseColor(textColor); + if (!fg) { + current = current.parentElement; + continue; + } - if (colors.length === 0) { - return null; - } + const contrastRatios = []; + const parsedColors = []; - // Calculate contrast against each color in the gradient - const fg = parseColor(textColor); - if (!fg) return null; + for (const colorStr of colors) { + const bg = parseColor(colorStr); + if (bg) { + const ratio = getContrastRatio(fg, bg); + contrastRatios.push(ratio); + parsedColors.push(colorStr); + } + } - const contrastRatios = []; - const parsedColors = []; + if (contrastRatios.length > 0) { + // Return worst-case (minimum) contrast ratio + const worstCase = Math.min(...contrastRatios); + const bestCase = Math.max(...contrastRatios); - for (const colorStr of colors) { - const bg = parseColor(colorStr); - if (bg) { - const ratio = getContrastRatio(fg, bg); - contrastRatios.push(ratio); - parsedColors.push(colorStr); + return { + worstCase: worstCase, + bestCase: bestCase, + colors: parsedColors, + isGradient: true + }; + } + } } + + current = current.parentElement; } - if (contrastRatios.length === 0) { - return null; - } - - // Return worst-case (minimum) contrast ratio - const worstCase = Math.min(...contrastRatios); - const bestCase = Math.max(...contrastRatios); - - return { - worstCase: worstCase, - bestCase: bestCase, - colors: parsedColors, - isGradient: true - }; + return null; } catch (e) { return null; } @@ -9293,7 +9298,7 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con let detectionMethod = null; let gradientInfo = null; - // Tier 1: DOM tree walking + // Tier 1: DOM tree walking for solid colors bgColor = getEffectiveBackgroundDOMTree(element); if (bgColor) { detectionMethod = 'dom-tree'; @@ -9307,12 +9312,14 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con } } - // Tier 3: Check for gradient backgrounds - if (bgColor) { - gradientInfo = analyzeGradientContrast(element, fgColor); - if (gradientInfo) { - detectionMethod = 'gradient-analysis'; - } + // Tier 3: Check for gradient backgrounds (always check, even if solid color found) + // Gradients take precedence over solid colors when present + gradientInfo = analyzeGradientContrast(element, fgColor); + if (gradientInfo) { + detectionMethod = 'gradient-analysis'; + // Use a representative color from the gradient for the background_color field + // We'll use the worst-case color for reporting purposes + bgColor = gradientInfo.colors[0] || bgColor; } // Final fallback to white if no background detected diff --git a/docs/contrast_check_fix_container_elements.md b/docs/contrast_check_fix_container_elements.md new file mode 100644 index 0000000..5f454ec --- /dev/null +++ b/docs/contrast_check_fix_container_elements.md @@ -0,0 +1,83 @@ +# Contrast Check Fix: Container Element False Positives + +## Issue Description + +The automated contrast checker was incorrectly flagging parent container elements (DIVs) instead of the actual text elements (H2, P, etc.) when checking color contrast. + +### Example from the Field + +On https://visionleadership.org/gala-sponsorship/: + +**Element 18 (H2 heading):** +- Tag: `H2` +- Text: "Gala Presenting Sponsor" +- Color: `rgb(255, 255, 255)` ✅ WHITE +- Background: `rgba(0, 0, 0, 0)` - transparent +- Parent background: `rgb(12, 113, 195)` - BLUE +- **Actual contrast ratio: 5.04:1 ✅ PASSES AA** + +**Element 17 (parent DIV):** +- Tag: `DIV` +- Color: `rgb(61, 61, 61)` - DARK GRAY +- Background: `rgb(12, 113, 195)` - BLUE +- **Computed contrast ratio: 2.15:1 ❌ FALSE POSITIVE** + +### Root Cause + +The contrast checker was using the selector: +```javascript +"p, h1, h2, h3, h4, h5, h6, a, button, span, div, li, td, th, label, input, textarea" +``` + +When a DIV container had text content (from child elements like H2), it was being checked using the DIV's computed text color (dark gray) instead of the actual H2's color (white). + +## Solution + +Added logic to skip container elements (DIV, SPAN) that don't have **direct text nodes** (text that is a direct child, not in descendant elements). + +### Implementation + +```javascript +// Skip container elements that don't have direct text nodes +const hasDirectTextContent = Array.from(element.childNodes).some(node => + node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0 +); + +// For container elements (div, span), only check if they have direct text +// For semantic text elements (h1-h6, p, a, button, etc.), always check +const isSemanticTextElement = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'A', 'BUTTON', 'LABEL', 'LI', 'TD', 'TH'].includes(element.tagName); +const isContainerElement = ['DIV', 'SPAN'].includes(element.tagName); + +if (isContainerElement && !hasDirectTextContent) { + // Skip this container - its children will be checked separately + return; +} +``` + +### Behavior After Fix + +1. **Semantic text elements** (H1-H6, P, A, BUTTON, LABEL, LI, TD, TH) are **always checked**, regardless of whether they have direct text content +2. **Container elements** (DIV, SPAN) are **only checked if they have direct text nodes** +3. **Container elements without direct text** are **skipped** - their child elements will be checked separately + +## Test Results + +After the fix, the contrast check correctly identifies: + +- ✅ H2 "Gala Presenting Sponsor": White text on blue background = **5.04:1 PASSES AA** +- ✅ H2 "Gala Bar Sponsor": White text on blue background = **5.04:1 PASSES AA** +- ✅ H2 "Table Sponsor": White text on blue background = **5.04:1 PASSES AA** +- ✅ H2 "RSVP Gala Ticket": White text on blue background = **5.04:1 PASSES AA** +- ✅ Parent DIVs are **not checked** (correctly skipped) + +## Files Modified + +- `daemon/daemon.go` - Lines 9221-9241: Added logic to skip container elements without direct text content + +## Impact + +- **Eliminates false positives** from parent containers +- **Improves accuracy** of contrast checking +- **Reduces noise** in accessibility reports +- **No impact** on legitimate contrast violations +