diff --git a/src/pages/AmountInput.tsx b/src/pages/AmountInput.tsx index 178ec7d..2eb3c90 100644 --- a/src/pages/AmountInput.tsx +++ b/src/pages/AmountInput.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; import { Badge } from "@/components/ui/badge"; import "@/styles/patterns.css"; // Ensure patterns are loaded if used directly +import "../styles/contrast.css"; export type AmountStatus = "idle" | "success" | "warning" | "error"; @@ -72,7 +73,7 @@ export const AmountInput: React.FC = ({ }; return ( -
+
diff --git a/src/pages/__tests__/AmountInput.contrast.test.tsx b/src/pages/__tests__/AmountInput.contrast.test.tsx new file mode 100644 index 0000000..c2b027b --- /dev/null +++ b/src/pages/__tests__/AmountInput.contrast.test.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { AmountInput, AmountStatus } from "../AmountInput"; + +describe("AmountInput high-contrast mode", () => { + it("applies the amount-input wrapper class for high-contrast styling", () => { + const { container } = render(); + expect(container.querySelector(".amount-input")).not.toBeNull(); + }); + + it("imports the contrast stylesheet so prefers-contrast overrides are applied", () => { + const source = require("fs").readFileSync( + "src/pages/AmountInput.tsx", + "utf-8" + ); + expect(source).toContain("../styles/contrast.css"); + }); + + it.each(["success", "warning", "error"] as AmountStatus[])( + "renders a status badge with visible border tokens for status %s", + (status) => { + render(); + const badge = screen.getByText( + status.charAt(0).toUpperCase() + status.slice(1) + ); + expect(badge).toBeInTheDocument(); + expect(badge.closest(".rounded-sm")).toBeInTheDocument(); + } + ); + + it("renders the input field with a focusable native element", () => { + render(); + const input = screen.getByLabelText("Amount"); + expect(input).toBeInTheDocument(); + expect(input.tagName).toBe("INPUT"); + }); +}); \ No newline at end of file diff --git a/src/styles/contrast.css b/src/styles/contrast.css new file mode 100644 index 0000000..ad61aec --- /dev/null +++ b/src/styles/contrast.css @@ -0,0 +1,56 @@ +/** + * contrast.css — High-contrast mode overrides + * + * When the user's OS is set to "more contrast" (prefers-contrast: more), + * translucent borders, subtle background tints, and low-contrast text + * become hard to read. These overrides ensure every interactive and + * informational element has a distinct visual boundary. + * + * WCAG 2.1 AA compliance: + * - Explicit 1px solid borders replace the default translucent borders + * so they meet the 1.5:1 minimum for non-text contrast (SC 1.4.11). + * - Text colours are forced to the full design-token value (no opacity + * reduction) so body copy meets the 4.5:1 ratio (SC 1.4.3). + * - Status chips get a stronger border so they are distinguishable + * without relying on hue alone. + * - The input field gets a solid, fully opaque border. + */ + +/* ── AmountInput high-contrast overrides ─────────────────────────────── */ +@media (prefers-contrast: more) { + .amount-input label { + color: #000 !important; + } + + .amount-input input { + border-color: #000 !important; + border-width: 1.5px !important; + background: #fff !important; + color: #000 !important; + } + + .amount-input input::placeholder { + color: #374151 !important; + opacity: 1 !important; + } + + .amount-input [role="status"] { + font-weight: 600 !important; + } + + .amount-input [role="status"] .rounded-sm { + border-width: 1.5px !important; + border-color: #000 !important; + } + + .amount-input [role="status"] span.text-muted-foreground, + .amount-input [class*="text-muted-foreground"] { + color: #374151 !important; + } + + .amount-input [class*="bg-card"], + .amount-input [class*="bg-background"], + .amount-input [class*="bg-muted"] { + background: #fff !important; + } +} \ No newline at end of file