Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/pages/AmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -72,7 +73,7 @@ export const AmountInput: React.FC<AmountInputProps> = ({
};

return (
<div className="flex flex-col gap-2 w-full max-w-sm">
<div className="amount-input flex flex-col gap-2 w-full max-w-sm">
<label htmlFor="amount-input" className="text-sm font-medium text-foreground">
Amount
</label>
Expand Down
38 changes: 38 additions & 0 deletions src/pages/__tests__/AmountInput.contrast.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<AmountInput />);
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(<AmountInput status={status} />);
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(<AmountInput />);
const input = screen.getByLabelText("Amount");
expect(input).toBeInTheDocument();
expect(input.tagName).toBe("INPUT");
});
});
56 changes: 56 additions & 0 deletions src/styles/contrast.css
Original file line number Diff line number Diff line change
@@ -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;
}
}