bump
This commit is contained in:
101
daemon/daemon.go
101
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) {
|
function analyzeGradientContrast(element, textColor) {
|
||||||
try {
|
try {
|
||||||
const style = window.getComputedStyle(element);
|
// Walk up the DOM tree to find a gradient background
|
||||||
const bgImage = style.backgroundImage;
|
let current = element;
|
||||||
|
while (current && current !== document.documentElement.parentElement) {
|
||||||
|
const style = window.getComputedStyle(current);
|
||||||
|
const bgImage = style.backgroundImage;
|
||||||
|
|
||||||
// Check if background is a gradient
|
// Check if background is a gradient
|
||||||
if (!bgImage || !bgImage.includes('gradient')) {
|
if (bgImage && bgImage.includes('gradient')) {
|
||||||
return null;
|
// 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
|
if (colors.length > 0) {
|
||||||
const colorRegex = /rgba?\([^)]+\)|#[0-9a-f]{3,6}/gi;
|
// Calculate contrast against each color in the gradient
|
||||||
const matches = bgImage.match(colorRegex) || [];
|
const fg = parseColor(textColor);
|
||||||
// Fix incomplete rgb/rgba matches by adding closing paren
|
if (!fg) {
|
||||||
const colors = matches.map(c => c.includes('(') && !c.includes(')') ? c + ')' : c);
|
current = current.parentElement;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (colors.length === 0) {
|
const contrastRatios = [];
|
||||||
return null;
|
const parsedColors = [];
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate contrast against each color in the gradient
|
for (const colorStr of colors) {
|
||||||
const fg = parseColor(textColor);
|
const bg = parseColor(colorStr);
|
||||||
if (!fg) return null;
|
if (bg) {
|
||||||
|
const ratio = getContrastRatio(fg, bg);
|
||||||
|
contrastRatios.push(ratio);
|
||||||
|
parsedColors.push(colorStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const contrastRatios = [];
|
if (contrastRatios.length > 0) {
|
||||||
const parsedColors = [];
|
// Return worst-case (minimum) contrast ratio
|
||||||
|
const worstCase = Math.min(...contrastRatios);
|
||||||
|
const bestCase = Math.max(...contrastRatios);
|
||||||
|
|
||||||
for (const colorStr of colors) {
|
return {
|
||||||
const bg = parseColor(colorStr);
|
worstCase: worstCase,
|
||||||
if (bg) {
|
bestCase: bestCase,
|
||||||
const ratio = getContrastRatio(fg, bg);
|
colors: parsedColors,
|
||||||
contrastRatios.push(ratio);
|
isGradient: true
|
||||||
parsedColors.push(colorStr);
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
current = current.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contrastRatios.length === 0) {
|
return null;
|
||||||
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
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -9293,7 +9298,7 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
|
|||||||
let detectionMethod = null;
|
let detectionMethod = null;
|
||||||
let gradientInfo = null;
|
let gradientInfo = null;
|
||||||
|
|
||||||
// Tier 1: DOM tree walking
|
// Tier 1: DOM tree walking for solid colors
|
||||||
bgColor = getEffectiveBackgroundDOMTree(element);
|
bgColor = getEffectiveBackgroundDOMTree(element);
|
||||||
if (bgColor) {
|
if (bgColor) {
|
||||||
detectionMethod = 'dom-tree';
|
detectionMethod = 'dom-tree';
|
||||||
@@ -9307,12 +9312,14 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 3: Check for gradient backgrounds
|
// Tier 3: Check for gradient backgrounds (always check, even if solid color found)
|
||||||
if (bgColor) {
|
// Gradients take precedence over solid colors when present
|
||||||
gradientInfo = analyzeGradientContrast(element, fgColor);
|
gradientInfo = analyzeGradientContrast(element, fgColor);
|
||||||
if (gradientInfo) {
|
if (gradientInfo) {
|
||||||
detectionMethod = 'gradient-analysis';
|
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
|
// Final fallback to white if no background detected
|
||||||
|
|||||||
83
docs/contrast_check_fix_container_elements.md
Normal file
83
docs/contrast_check_fix_container_elements.md
Normal file
@@ -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
|
||||||
|
|
||||||
Reference in New Issue
Block a user