feat: add color contrast calculation utility with WCAG standards
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
interface ColorRGB {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
interface ContrastResult {
|
||||
passes: boolean;
|
||||
ratio: number;
|
||||
requiredRatio: number;
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): ColorRGB {
|
||||
const cleanHex = hex.replace('#', '');
|
||||
const r = parseInt(cleanHex.substring(0, 2), 16);
|
||||
const g = parseInt(cleanHex.substring(2, 4), 16);
|
||||
const b = parseInt(cleanHex.substring(4, 6), 16);
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
function getLuminance(rgb: ColorRGB): number {
|
||||
const { r, g, b } = rgb;
|
||||
const a = [r, g, b].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;
|
||||
}
|
||||
|
||||
export function calculateContrastRatio(foreground: string, background: string): number {
|
||||
const fgRgb = hexToRgb(foreground);
|
||||
const bgRgb = hexToRgb(background);
|
||||
|
||||
const fgLuminance = getLuminance(fgRgb);
|
||||
const bgLuminance = getLuminance(bgRgb);
|
||||
|
||||
const lighter = Math.max(fgLuminance, bgLuminance);
|
||||
const darker = Math.min(fgLuminance, bgLuminance);
|
||||
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
export function meetsWCAGStandard(
|
||||
foreground: string,
|
||||
background: string,
|
||||
level: 'AA' | 'AAA',
|
||||
textSize: 'normal' | 'large'
|
||||
): ContrastResult {
|
||||
const ratio = calculateContrastRatio(foreground, background);
|
||||
|
||||
let requiredRatio: number;
|
||||
if (level === 'AA') {
|
||||
requiredRatio = textSize === 'normal' ? 4.5 : 3;
|
||||
} else {
|
||||
requiredRatio = textSize === 'normal' ? 7 : 4.5;
|
||||
}
|
||||
|
||||
return {
|
||||
passes: ratio >= requiredRatio,
|
||||
ratio,
|
||||
requiredRatio
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { calculateContrastRatio, meetsWCAGStandard } from '@/lib/color-contrast';
|
||||
|
||||
test('should calculate correct contrast ratio for black on white', () => {
|
||||
const ratio = calculateContrastRatio('#000000', '#FFFFFF');
|
||||
expect(ratio).toBeCloseTo(21, 1);
|
||||
});
|
||||
|
||||
test('should identify WCAG AA compliance for normal text', () => {
|
||||
const result = meetsWCAGStandard('#000000', '#FFFFFF', 'AA', 'normal');
|
||||
expect(result.passes).toBe(true);
|
||||
expect(result.ratio).toBeGreaterThan(4.5);
|
||||
});
|
||||
|
||||
test('should fail WCAG AA for low contrast colors', () => {
|
||||
const result = meetsWCAGStandard('#808080', '#FFFFFF', 'AA', 'normal');
|
||||
expect(result.passes).toBe(false);
|
||||
expect(result.ratio).toBeLessThan(4.5);
|
||||
});
|
||||
Reference in New Issue
Block a user