From db63b97e4b50488c79c0cfb648b39bdfd293c140 Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Wed, 17 Dec 2025 15:24:43 -0300 Subject: [PATCH 1/7] feat: add WCAG Color Contrast Checker tool and related components - Introduced a new ColorPicker component for selecting colors. - Added WcagComplianceBadge for displaying compliance status. - Implemented WCAG color contrast utility functions for calculating contrast ratios and compliance. - Created a new page for the WCAG Color Contrast Checker with input fields for foreground and background colors. - Updated package.json and package-lock.json to include necessary dependencies for react-color and related types. - Enhanced global styles for better presentation of content. --- components/WcagComplianceBadge.tsx | 27 ++ components/ds/ColorPickerComponent.tsx | 53 ++++ .../seo/WcagColorContrastCheckerSEO.tsx | 80 +++++ components/utils/tools-list.ts | 6 + .../utils/wcag-color-contrast.utils.test.ts | 296 ++++++++++++++++++ components/utils/wcag-color-contrast.utils.ts | 191 +++++++++++ package-lock.json | 92 +++++- package.json | 4 +- .../utilities/wcag-color-contrast-checker.tsx | 259 +++++++++++++++ styles/globals.css | 14 +- 10 files changed, 1009 insertions(+), 13 deletions(-) create mode 100644 components/WcagComplianceBadge.tsx create mode 100644 components/ds/ColorPickerComponent.tsx create mode 100644 components/seo/WcagColorContrastCheckerSEO.tsx create mode 100644 components/utils/wcag-color-contrast.utils.test.ts create mode 100644 components/utils/wcag-color-contrast.utils.ts create mode 100644 pages/utilities/wcag-color-contrast-checker.tsx diff --git a/components/WcagComplianceBadge.tsx b/components/WcagComplianceBadge.tsx new file mode 100644 index 0000000..1c77c3f --- /dev/null +++ b/components/WcagComplianceBadge.tsx @@ -0,0 +1,27 @@ +import { CheckCircle2, XCircle } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface ComplianceBadgeProps { + passed: boolean; + level: string; +} + +export function ComplianceBadge({ passed, level }: ComplianceBadgeProps) { + const Icon = passed ? CheckCircle2 : XCircle; + const statusClasses = passed + ? "bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300" + : "bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300"; + + return ( +
+ + {level} +
+ ); +} + diff --git a/components/ds/ColorPickerComponent.tsx b/components/ds/ColorPickerComponent.tsx new file mode 100644 index 0000000..4d58ad6 --- /dev/null +++ b/components/ds/ColorPickerComponent.tsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ds/PopoverComponent"; +import { SketchPicker } from "react-color"; +import { cn } from "@/lib/utils"; + +const HEX_COLOR_WITH_HASH_PATTERN: RegExp = /^#[0-9A-F]{6}$/i; + +const DEFAULT_COLOR = "#000000" as const; + +export interface ColorPickerProps { + value: string; + onChange: (color: string) => void; + className?: string; + disabled?: boolean; +} + +export const ColorPicker = React.forwardRef( + ({ value, onChange, className, disabled = false }, ref) => { + const [open, setOpen] = useState(false); + + const displayValue = value && HEX_COLOR_WITH_HASH_PATTERN.test(value) ? value : DEFAULT_COLOR; + + return ( + + + Date: Thu, 18 Dec 2025 11:35:50 -0300 Subject: [PATCH 4/7] feat: expand WCAG Color Contrast Checker with additional guidelines for graphical objects and UI components --- .../seo/WcagColorContrastCheckerSEO.tsx | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/components/seo/WcagColorContrastCheckerSEO.tsx b/components/seo/WcagColorContrastCheckerSEO.tsx index 24b41b8..9bb25e7 100644 --- a/components/seo/WcagColorContrastCheckerSEO.tsx +++ b/components/seo/WcagColorContrastCheckerSEO.tsx @@ -51,6 +51,11 @@ export default function WcagColorContrastCheckerSEO() { WCAG AA Large text: Requires 3:1 contrast ratio (text 18pt+ or 14pt+ bold) +
  • + WCAG AA Graphical objects and UI components: Requires 3:1 + contrast ratio for essential visual information like icons, buttons, + form controls, and other interactive elements +
  • WCAG AAA Normal text: Requires 7:1 contrast ratio (text smaller than 18pt or 14pt bold) @@ -62,16 +67,28 @@ export default function WcagColorContrastCheckerSEO() { +
    +

    Graphical Objects and User Interface Components

    +

    + WCAG 2.1 also requires a minimum 3:1 contrast ratio for graphical + objects and user interface components. This includes icons, buttons, + form controls, charts, graphs, and other essential visual elements + that convey information or require user interaction. Our tool checks + compliance for these elements, helping you ensure that all parts of + your interface are accessible to users with visual impairments. +

    +
    +

    Why Color Contrast Matters for Accessibility

    Proper color contrast is essential for accessibility. It ensures that people with visual impairments, color blindness, or those viewing - content in bright sunlight can read your text. Meeting WCAG standards - not only improves accessibility but also helps you comply with legal - requirements in many jurisdictions. Our tool calculates the contrast - ratio using the WCAG 2.1 formula, which considers the relative - luminance of colors. + content in bright sunlight can read your text and interact with your + interface. Meeting WCAG standards not only improves accessibility but + also helps you comply with legal requirements in many jurisdictions. + Our tool calculates the contrast ratio using the WCAG 2.1 formula, + which considers the relative luminance of colors.

    From 380c85eaf8eb030d147d9b688688b6cfbc76b405 Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Thu, 18 Dec 2025 11:36:42 -0300 Subject: [PATCH 5/7] feat: enhance WCAG color contrast utilities with additional hex color handling --- .../utils/wcag-color-contrast.utils.test.ts | 199 +++++++++++++++++- components/utils/wcag-color-contrast.utils.ts | 94 ++++++--- 2 files changed, 258 insertions(+), 35 deletions(-) diff --git a/components/utils/wcag-color-contrast.utils.test.ts b/components/utils/wcag-color-contrast.utils.test.ts index deb311a..bc2ec6f 100644 --- a/components/utils/wcag-color-contrast.utils.test.ts +++ b/components/utils/wcag-color-contrast.utils.test.ts @@ -1,6 +1,9 @@ import { hexToRgb, isValidHex, + rgbToHex, + normalizeHexForDisplay, + normalizeHexInput, getRelativeLuminance, getContrastRatio, checkWCAGCompliance, @@ -20,17 +23,46 @@ describe("wcag-color-contrast.utils", () => { expect(hexToRgb("#767676")).toEqual({ r: 118, g: 118, b: 118 }); }); + test("should convert 3-digit hex color to RGB (expanded)", () => { + expect(hexToRgb("#000")).toEqual({ r: 0, g: 0, b: 0 }); + expect(hexToRgb("#fff")).toEqual({ r: 255, g: 255, b: 255 }); + expect(hexToRgb("#FFF")).toEqual({ r: 255, g: 255, b: 255 }); + expect(hexToRgb("#f00")).toEqual({ r: 255, g: 0, b: 0 }); + expect(hexToRgb("#0f0")).toEqual({ r: 0, g: 255, b: 0 }); + expect(hexToRgb("#00f")).toEqual({ r: 0, g: 0, b: 255 }); + expect(hexToRgb("#abc")).toEqual({ r: 170, g: 187, b: 204 }); + }); + test("should convert hex without # prefix", () => { expect(hexToRgb("000000")).toEqual({ r: 0, g: 0, b: 0 }); expect(hexToRgb("FFFFFF")).toEqual({ r: 255, g: 255, b: 255 }); + expect(hexToRgb("fff")).toEqual({ r: 255, g: 255, b: 255 }); + expect(hexToRgb("000")).toEqual({ r: 0, g: 0, b: 0 }); }); test("should return null for invalid hex colors", () => { expect(hexToRgb("#GGGGGG")).toBeNull(); - expect(hexToRgb("#FFF")).toBeNull(); expect(hexToRgb("invalid")).toBeNull(); - expect(hexToRgb("#12345")).toBeNull(); + expect(hexToRgb("#12")).toBeNull(); + expect(hexToRgb("#1234567")).toBeNull(); + expect(hexToRgb("#123456789")).toBeNull(); + }); + + test("should handle 4 and 5 digit hex colors (treated as 3 during typing)", () => { + + const result4 = hexToRgb("#fff0"); + expect(result4).not.toBeNull(); + expect(result4).toEqual({ r: 255, g: 255, b: 255 }); + + + const result5 = hexToRgb("#12345"); + expect(result5).not.toBeNull(); + }); + + test("should return null for invalid lengths", () => { + expect(hexToRgb("#12")).toBeNull(); expect(hexToRgb("#1234567")).toBeNull(); + expect(hexToRgb("#12345678")).toBeNull(); }); }); @@ -44,16 +76,128 @@ describe("wcag-color-contrast.utils", () => { expect(isValidHex("FFFFFF")).toBe(true); }); + test("should validate 3-digit hex colors", () => { + expect(isValidHex("#000")).toBe(true); + expect(isValidHex("#fff")).toBe(true); + expect(isValidHex("#FFF")).toBe(true); + expect(isValidHex("#abc")).toBe(true); + expect(isValidHex("000")).toBe(true); + expect(isValidHex("fff")).toBe(true); + expect(isValidHex("ABC")).toBe(true); + }); + + test("should validate 4 and 5 digit hex colors (accepted during typing)", () => { + expect(isValidHex("#fff0")).toBe(true); + expect(isValidHex("#12345")).toBe(true); + }); + test("should reject invalid hex colors", () => { - expect(isValidHex("#FFF")).toBe(false); expect(isValidHex("#GGGGGG")).toBe(false); expect(isValidHex("invalid")).toBe(false); - expect(isValidHex("#12345")).toBe(false); + expect(isValidHex("#12")).toBe(false); expect(isValidHex("#1234567")).toBe(false); + expect(isValidHex("#123456789")).toBe(false); expect(isValidHex("")).toBe(false); }); }); + describe("rgbToHex", () => { + test("should convert RGB to hex", () => { + expect(rgbToHex({ r: 0, g: 0, b: 0 })).toBe("#000000"); + expect(rgbToHex({ r: 255, g: 255, b: 255 })).toBe("#FFFFFF"); + expect(rgbToHex({ r: 255, g: 0, b: 0 })).toBe("#FF0000"); + expect(rgbToHex({ r: 0, g: 255, b: 0 })).toBe("#00FF00"); + expect(rgbToHex({ r: 0, g: 0, b: 255 })).toBe("#0000FF"); + expect(rgbToHex({ r: 118, g: 118, b: 118 })).toBe("#767676"); + }); + + test("should pad single digit values with zeros", () => { + expect(rgbToHex({ r: 10, g: 5, b: 1 })).toBe("#0A0501"); + expect(rgbToHex({ r: 0, g: 15, b: 255 })).toBe("#000FFF"); + }); + + test("should convert to uppercase", () => { + expect(rgbToHex({ r: 170, g: 187, b: 204 })).toBe("#AABBCC"); + }); + }); + + describe("normalizeHexForDisplay", () => { + test("should normalize 3-digit hex to 6-digit", () => { + expect(normalizeHexForDisplay("#000")).toBe("#000000"); + expect(normalizeHexForDisplay("#fff")).toBe("#FFFFFF"); + expect(normalizeHexForDisplay("#FFF")).toBe("#FFFFFF"); + expect(normalizeHexForDisplay("#abc")).toBe("#AABBCC"); + expect(normalizeHexForDisplay("#f00")).toBe("#FF0000"); + }); + + test("should normalize 4 and 5 digit hex to 6-digit (using first 3)", () => { + expect(normalizeHexForDisplay("#fff0")).toBe("#FFFFFF"); + expect(normalizeHexForDisplay("#12345")).toBe("#112233"); + }); + + test("should return 6-digit hex as is (uppercase)", () => { + expect(normalizeHexForDisplay("#000000")).toBe("#000000"); + expect(normalizeHexForDisplay("#ffffff")).toBe("#FFFFFF"); + expect(normalizeHexForDisplay("#FFFFFF")).toBe("#FFFFFF"); + expect(normalizeHexForDisplay("#FF5733")).toBe("#FF5733"); + }); + + test("should work without # prefix", () => { + expect(normalizeHexForDisplay("000")).toBe("#000000"); + expect(normalizeHexForDisplay("fff")).toBe("#FFFFFF"); + expect(normalizeHexForDisplay("FFFFFF")).toBe("#FFFFFF"); + }); + + test("should return null for invalid hex", () => { + expect(normalizeHexForDisplay("invalid")).toBeNull(); + expect(normalizeHexForDisplay("#GGGGGG")).toBeNull(); + expect(normalizeHexForDisplay("#12")).toBeNull(); + expect(normalizeHexForDisplay("#1234567")).toBeNull(); + expect(normalizeHexForDisplay("")).toBeNull(); + }); + }); + + describe("normalizeHexInput", () => { + test("should add # prefix if missing", () => { + expect(normalizeHexInput("000000")).toBe("#000000"); + expect(normalizeHexInput("fff")).toBe("#FFF"); + expect(normalizeHexInput("FFFFFF")).toBe("#FFFFFF"); + }); + + test("should keep # prefix if present", () => { + expect(normalizeHexInput("#000000")).toBe("#000000"); + expect(normalizeHexInput("#fff")).toBe("#FFF"); + expect(normalizeHexInput("#FFFFFF")).toBe("#FFFFFF"); + }); + + test("should convert to uppercase", () => { + expect(normalizeHexInput("#ffffff")).toBe("#FFFFFF"); + expect(normalizeHexInput("#abc")).toBe("#ABC"); + expect(normalizeHexInput("abc")).toBe("#ABC"); + }); + + test("should remove invalid characters", () => { + expect(normalizeHexInput("#FF-33")).toBe("#FF33"); + expect(normalizeHexInput("FF 33")).toBe("#FF33"); + expect(normalizeHexInput("#123!@#456")).toBe("#123#45"); + }); + + test("should trim whitespace", () => { + expect(normalizeHexInput(" #fff ")).toBe("#FFF"); + expect(normalizeHexInput(" fff ")).toBe("#FFF"); + }); + + test("should limit to 7 characters (# + 6 digits)", () => { + expect(normalizeHexInput("#123456789")).toBe("#123456"); + expect(normalizeHexInput("123456789")).toBe("#123456"); + }); + + test("should handle empty string", () => { + expect(normalizeHexInput("")).toBe(""); + expect(normalizeHexInput(" ")).toBe(""); + }); + }); + describe("getRelativeLuminance", () => { test("should calculate luminance for black", () => { const black: RGB = { r: 0, g: 0, b: 0 }; @@ -229,16 +373,59 @@ describe("wcag-color-contrast.utils", () => { ).toBeNull(); expect( calculateContrast({ - foregroundHex: "#FFF", + foregroundHex: "#12", backgroundHex: "#FFFFFF", }) ).toBeNull(); expect( calculateContrast({ foregroundHex: "#000000", - backgroundHex: "#FFF", + backgroundHex: "#123456789", + }) + ).toBeNull(); + }); + + test("should work with 4 and 5 digit hex colors (treated as 3)", () => { + const result = calculateContrast({ + foregroundHex: "#fff0", + backgroundHex: "#12345", + }); + expect(result).not.toBeNull(); + }); + + test("should return null for invalid lengths", () => { + expect( + calculateContrast({ + foregroundHex: "#1234567", + backgroundHex: "#000", }) ).toBeNull(); + expect( + calculateContrast({ + foregroundHex: "#ffffff00", + backgroundHex: "#000", + }) + ).toBeNull(); + }); + + test("should work with 3-digit hex colors", () => { + const result = calculateContrast({ + foregroundHex: "#000", + backgroundHex: "#fff", + }); + expect(result).not.toBeNull(); + if (result) { + expect(result.ratio).toBeCloseTo(21, 1); + } + + const result2 = calculateContrast({ + foregroundHex: "#f00", + backgroundHex: "#0f0", + }); + expect(result2).not.toBeNull(); + if (result2) { + expect(result2.ratio).toBeGreaterThan(1); + } }); test("should work with hex colors without # prefix", () => { diff --git a/components/utils/wcag-color-contrast.utils.ts b/components/utils/wcag-color-contrast.utils.ts index 64498a7..df578d5 100644 --- a/components/utils/wcag-color-contrast.utils.ts +++ b/components/utils/wcag-color-contrast.utils.ts @@ -17,6 +17,13 @@ export interface ContrastResult { }; } +interface ContrastDescriptionRule { + condition: boolean; + result: string; +} + +type HexLength = 3 | 4 | 5 | 6; + export const WCAG = { AA: { NORMAL_THRESHOLD: 4.5, @@ -44,26 +51,71 @@ const LUMINANCE_B_COEFFICIENT = 0.0722 as const; const CONTRAST_RATIO_CONSTANT = 0.05 as const; -const HEX_COLOR_PATTERN: RegExp = /^[0-9A-Fa-f]{6}$/; +const HEX_COLOR_PATTERN: RegExp = /^[0-9A-Fa-f]{3,6}$/; const INVALID_HEX_CHARS: RegExp = /[^#0-9A-F]/gi; -export const hexToRgb = (hex: string): RGB | null => { - const normalizedHex = hex.startsWith("#") ? hex.slice(1) : hex; +const parseHexToRgb = (normalizedHex: string, length: HexLength): RGB => { + const shouldExpand = [3, 4, 5].includes(length); + const expandedLength = shouldExpand ? 3 : length; - if (!HEX_COLOR_PATTERN.test(normalizedHex)) { - return null; + let hexToParse = normalizedHex; + + if (shouldExpand) { + hexToParse = hexToParse.substring(0, expandedLength).split("").map((char) => char + char).join(""); } - const r = parseInt(normalizedHex.substring(0, 2), 16); - const g = parseInt(normalizedHex.substring(2, 4), 16); - const b = parseInt(normalizedHex.substring(4, 6), 16); + const r = parseInt(hexToParse.substring(0, 2), 16); + const g = parseInt(hexToParse.substring(2, 4), 16); + const b = parseInt(hexToParse.substring(4, 6), 16); return { r, g, b }; }; +const hexToRgbMapper: Record RGB> = { + 3: (normalizedHex) => parseHexToRgb(normalizedHex, 3), + 4: (normalizedHex) => parseHexToRgb(normalizedHex, 4), + 5: (normalizedHex) => parseHexToRgb(normalizedHex, 5), + 6: (normalizedHex) => parseHexToRgb(normalizedHex, 6), +}; + +export const hexToRgb = (hex: string): RGB | null => { + const normalizedHex = hex.startsWith("#") ? hex.slice(1) : hex; + + if (!HEX_COLOR_PATTERN.test(normalizedHex)) return null; + + const length = normalizedHex.length; + + if (length < 3 || length > 6) return null; + + const lengthKey = length as HexLength; + const parser = hexToRgbMapper[lengthKey]; + + if (!parser) return null; + + + return parser(normalizedHex); +}; + export const isValidHex = (hex: string): boolean => { const normalizedHex = hex.startsWith("#") ? hex.slice(1) : hex; - return HEX_COLOR_PATTERN.test(normalizedHex); + return HEX_COLOR_PATTERN.test(normalizedHex) && normalizedHex.length >= 3 && normalizedHex.length <= 6; +}; + +const rgbComponentToHex = (component: number): string => { + return component.toString(16).padStart(2, "0").toUpperCase(); +}; + +export const rgbToHex = (rgb: RGB): string => { + return `#${rgbComponentToHex(rgb.r)}${rgbComponentToHex(rgb.g)}${rgbComponentToHex(rgb.b)}`; +}; + +export const normalizeHexForDisplay = (hex: string): string | null => { + if (!isValidHex(hex)) return null; + + const rgb = hexToRgb(hex); + if (!rgb) return null; + + return rgbToHex(rgb); }; /** @@ -76,9 +128,9 @@ export const getRelativeLuminance = (rgb: RGB): number => { return val <= SRGB_GAMMA_THRESHOLD ? val / SRGB_LOW_VALUE_DIVISOR : Math.pow( - (val + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_DIVISOR, - SRGB_GAMMA_EXPONENT - ); + (val + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_DIVISOR, + SRGB_GAMMA_EXPONENT + ); }; const r = normalize(rgb.r); @@ -155,6 +207,7 @@ export const normalizeHexInput = (value: string): string => { } normalized = normalized.replace(INVALID_HEX_CHARS, ""); + if (normalized.length > 7) { normalized = normalized.slice(0, 7); } @@ -162,24 +215,7 @@ export const normalizeHexInput = (value: string): string => { return normalized; }; -export const validateAndLimitFontSize = (value: string): string | undefined => { - if (!value) return ""; - - const numValue = parseFloat(value); - const rules: Array<{ condition: boolean; result: string | undefined }> = [ - { condition: isNaN(numValue), result: undefined }, - { condition: numValue > 90, result: "90" }, - { condition: numValue >= 1, result: value }, - ]; - - const matchedRule = rules.find((rule) => rule.condition); - return matchedRule?.result ?? undefined; -}; -interface ContrastDescriptionRule { - condition: boolean; - result: string; -} export const getContrastDescription = (ratio: number): string => { const rules: ContrastDescriptionRule[] = [ From 31b0ddb5c402e7df5f2d712208266282f458d1e2 Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Thu, 18 Dec 2025 11:37:15 -0300 Subject: [PATCH 6/7] refactor: streamline color input handling in WCAG Color Contrast Checker --- .../utilities/wcag-color-contrast-checker.tsx | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/pages/utilities/wcag-color-contrast-checker.tsx b/pages/utilities/wcag-color-contrast-checker.tsx index 6835614..e671793 100644 --- a/pages/utilities/wcag-color-contrast-checker.tsx +++ b/pages/utilities/wcag-color-contrast-checker.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useCallback } from "react"; import PageHeader from "@/components/PageHeader"; import { Card } from "@/components/ds/CardComponent"; import { Label } from "@/components/ds/LabelComponent"; @@ -14,6 +14,7 @@ import { calculateContrast, isValidHex, normalizeHexInput, + normalizeHexForDisplay, getContrastDescription, WCAG, } from "@/components/utils/wcag-color-contrast.utils"; @@ -30,23 +31,20 @@ export default function WcagColorContrastChecker() { const [foreground, setForeground] = useState(DEFAULT_FOREGROUND_COLOR); const [background, setBackground] = useState(DEFAULT_BACKGROUND_COLOR); - const handleForegroundChange = ( - event: React.ChangeEvent - ) => { - const normalized = normalizeHexInput(event.target.value); - setForeground(normalized); - }; - - const handleBackgroundChange = ( - event: React.ChangeEvent - ) => { - const normalized = normalizeHexInput(event.target.value); - setBackground(normalized); - }; + const handleColorChange = useCallback( + (setter: (value: string) => void) => (event: React.ChangeEvent) => { + const normalized = normalizeHexInput(event.target.value); + setter(normalized); + }, + [] + ); const fgValid = isValidHex(foreground); const bgValid = isValidHex(background); + const normalizedForeground = normalizeHexForDisplay(foreground) ?? DEFAULT_FOREGROUND_COLOR; + const normalizedBackground = normalizeHexForDisplay(background) ?? DEFAULT_BACKGROUND_COLOR; + const contrastResult = useMemo( () => calculateContrast({ @@ -81,13 +79,13 @@ export default function WcagColorContrastChecker() {
    @@ -98,13 +96,13 @@ export default function WcagColorContrastChecker() {
    @@ -148,11 +146,11 @@ export default function WcagColorContrastChecker() {

    From e910037787c7a1b4e42d63e123b56ab6f8e5e0e8 Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Thu, 18 Dec 2025 11:40:40 -0300 Subject: [PATCH 7/7] refactor: improve code formatting and readability in color utilities and components --- components/ds/ColorPickerComponent.tsx | 10 ++++++-- .../utils/wcag-color-contrast.utils.test.ts | 2 -- components/utils/wcag-color-contrast.utils.ts | 21 ++++++++++------ .../utilities/wcag-color-contrast-checker.tsx | 25 ++++++++----------- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/components/ds/ColorPickerComponent.tsx b/components/ds/ColorPickerComponent.tsx index 5164299..e5e60eb 100644 --- a/components/ds/ColorPickerComponent.tsx +++ b/components/ds/ColorPickerComponent.tsx @@ -6,7 +6,10 @@ import { } from "@/components/ds/PopoverComponent"; import { SketchPicker } from "react-color"; import { cn } from "@/lib/utils"; -import { normalizeHexForDisplay, isValidHex } from "@/components/utils/wcag-color-contrast.utils"; +import { + normalizeHexForDisplay, + isValidHex, +} from "@/components/utils/wcag-color-contrast.utils"; import { X } from "lucide-react"; const DEFAULT_COLOR = "#000000" as const; @@ -41,7 +44,10 @@ export const ColorPicker = React.forwardRef( aria-label="Pick a color" > {!isValid && value && ( - + )} diff --git a/components/utils/wcag-color-contrast.utils.test.ts b/components/utils/wcag-color-contrast.utils.test.ts index bc2ec6f..a2a0c98 100644 --- a/components/utils/wcag-color-contrast.utils.test.ts +++ b/components/utils/wcag-color-contrast.utils.test.ts @@ -49,12 +49,10 @@ describe("wcag-color-contrast.utils", () => { }); test("should handle 4 and 5 digit hex colors (treated as 3 during typing)", () => { - const result4 = hexToRgb("#fff0"); expect(result4).not.toBeNull(); expect(result4).toEqual({ r: 255, g: 255, b: 255 }); - const result5 = hexToRgb("#12345"); expect(result5).not.toBeNull(); }); diff --git a/components/utils/wcag-color-contrast.utils.ts b/components/utils/wcag-color-contrast.utils.ts index df578d5..984f2e8 100644 --- a/components/utils/wcag-color-contrast.utils.ts +++ b/components/utils/wcag-color-contrast.utils.ts @@ -61,7 +61,11 @@ const parseHexToRgb = (normalizedHex: string, length: HexLength): RGB => { let hexToParse = normalizedHex; if (shouldExpand) { - hexToParse = hexToParse.substring(0, expandedLength).split("").map((char) => char + char).join(""); + hexToParse = hexToParse + .substring(0, expandedLength) + .split("") + .map((char) => char + char) + .join(""); } const r = parseInt(hexToParse.substring(0, 2), 16); @@ -92,13 +96,16 @@ export const hexToRgb = (hex: string): RGB | null => { if (!parser) return null; - return parser(normalizedHex); }; export const isValidHex = (hex: string): boolean => { const normalizedHex = hex.startsWith("#") ? hex.slice(1) : hex; - return HEX_COLOR_PATTERN.test(normalizedHex) && normalizedHex.length >= 3 && normalizedHex.length <= 6; + return ( + HEX_COLOR_PATTERN.test(normalizedHex) && + normalizedHex.length >= 3 && + normalizedHex.length <= 6 + ); }; const rgbComponentToHex = (component: number): string => { @@ -128,9 +135,9 @@ export const getRelativeLuminance = (rgb: RGB): number => { return val <= SRGB_GAMMA_THRESHOLD ? val / SRGB_LOW_VALUE_DIVISOR : Math.pow( - (val + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_DIVISOR, - SRGB_GAMMA_EXPONENT - ); + (val + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_DIVISOR, + SRGB_GAMMA_EXPONENT + ); }; const r = normalize(rgb.r); @@ -215,8 +222,6 @@ export const normalizeHexInput = (value: string): string => { return normalized; }; - - export const getContrastDescription = (ratio: number): string => { const rules: ContrastDescriptionRule[] = [ { diff --git a/pages/utilities/wcag-color-contrast-checker.tsx b/pages/utilities/wcag-color-contrast-checker.tsx index e671793..0a3f0c3 100644 --- a/pages/utilities/wcag-color-contrast-checker.tsx +++ b/pages/utilities/wcag-color-contrast-checker.tsx @@ -32,18 +32,21 @@ export default function WcagColorContrastChecker() { const [background, setBackground] = useState(DEFAULT_BACKGROUND_COLOR); const handleColorChange = useCallback( - (setter: (value: string) => void) => (event: React.ChangeEvent) => { - const normalized = normalizeHexInput(event.target.value); - setter(normalized); - }, + (setter: (value: string) => void) => + (event: React.ChangeEvent) => { + const normalized = normalizeHexInput(event.target.value); + setter(normalized); + }, [] ); const fgValid = isValidHex(foreground); const bgValid = isValidHex(background); - const normalizedForeground = normalizeHexForDisplay(foreground) ?? DEFAULT_FOREGROUND_COLOR; - const normalizedBackground = normalizeHexForDisplay(background) ?? DEFAULT_BACKGROUND_COLOR; + const normalizedForeground = + normalizeHexForDisplay(foreground) ?? DEFAULT_FOREGROUND_COLOR; + const normalizedBackground = + normalizeHexForDisplay(background) ?? DEFAULT_BACKGROUND_COLOR; const contrastResult = useMemo( () => @@ -84,10 +87,7 @@ export default function WcagColorContrastChecker() { value={foreground} maxLength={7} /> - +
    @@ -101,10 +101,7 @@ export default function WcagColorContrastChecker() { value={background} maxLength={7} /> - +