diff --git a/frontend/components/CurrencyInput.test.tsx b/frontend/components/CurrencyInput.test.tsx
index 81d1005..441e2e7 100644
--- a/frontend/components/CurrencyInput.test.tsx
+++ b/frontend/components/CurrencyInput.test.tsx
@@ -1,12 +1,15 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
+import { axe, toHaveNoViolations } from "jest-axe";
import { CurrencyInput } from "./CurrencyInput";
+expect.extend(toHaveNoViolations);
+
describe("CurrencyInput Component", () => {
it("renders input prefixed with ₦ symbol and formats thousands separator", () => {
render();
- const input = screen.getByRole("textbox") as HTMLInputElement;
+ const input = screen.getByRole("spinbutton") as HTMLInputElement;
expect(input.value).toBe("50,000");
expect(screen.getByText("₦")).toBeInTheDocument();
});
@@ -15,7 +18,7 @@ describe("CurrencyInput Component", () => {
const handleChange = jest.fn();
render();
- const input = screen.getByRole("textbox");
+ const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "₦ 50,000abc" } });
expect(handleChange).toHaveBeenCalledWith(50000);
@@ -25,7 +28,7 @@ describe("CurrencyInput Component", () => {
const handleChange = jest.fn();
render();
- const input = screen.getByRole("textbox");
+ const input = screen.getByRole("spinbutton");
fireEvent.paste(input, {
clipboardData: { getData: () => "1,234.56abc" },
});
@@ -44,7 +47,7 @@ describe("CurrencyInput Component", () => {
const handleChange = jest.fn();
render();
- const input = screen.getByRole("textbox");
+ const input = screen.getByRole("spinbutton");
fireEvent.keyDown(input, { key: "ArrowUp" });
expect(handleChange).toHaveBeenCalledWith(600);
@@ -52,6 +55,21 @@ describe("CurrencyInput Component", () => {
fireEvent.keyDown(input, { key: "ArrowDown" });
expect(handleChange).toHaveBeenCalledWith(400);
});
+
+ it("exposes its name, bounds, current value, and announced validation", async () => {
+ const { container } = render(
+
+ );
+ const input = screen.getByRole("spinbutton", { name: "Amount in Nigerian naira" });
+
+ expect(input).toHaveAttribute("aria-valuemin", "100");
+ expect(input).toHaveAttribute("aria-valuemax", "500");
+ expect(input).toHaveAttribute("aria-valuenow", "50");
+ expect(input).toHaveAttribute("aria-valuetext", "₦50");
+ expect(input).toHaveAttribute("aria-invalid", "true");
+ expect(screen.getByRole("alert")).toHaveAttribute("aria-live", "assertive");
+ expect(await axe(container)).toHaveNoViolations();
+ });
});
describe("CurrencyInput available-amount caption (#447)", () => {
diff --git a/frontend/components/CurrencyInput.tsx b/frontend/components/CurrencyInput.tsx
index 1c5b3ae..291a621 100644
--- a/frontend/components/CurrencyInput.tsx
+++ b/frontend/components/CurrencyInput.tsx
@@ -116,6 +116,7 @@ export function CurrencyInput({
id={id}
name={name}
type="text"
+ role="spinbutton"
inputMode="numeric"
value={displayValue}
onChange={handleChange}
@@ -123,6 +124,11 @@ export function CurrencyInput({
onPaste={handlePaste}
placeholder={placeholder}
disabled={disabled}
+ aria-label={rest["aria-label"] ?? "Amount in Nigerian naira"}
+ aria-valuemin={min ?? 0}
+ aria-valuemax={max}
+ aria-valuenow={numericValue}
+ aria-valuetext={`₦${displayValue || "0"}`}
aria-invalid={isBelowMin || Boolean(errorText) ? "true" : undefined}
aria-describedby={
minErrorMessage || errorText ? `${id}-error` : rest["aria-describedby"]
@@ -133,7 +139,7 @@ export function CurrencyInput({
{(minErrorMessage || errorText) && (
-
+
{minErrorMessage || errorText}
)}