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