Add accessibility improvements (P4.2 partial)
All checks were successful
Deploy / deploy (push) Successful in 1m36s
All checks were successful
Deploy / deploy (push) Successful in 1m36s
- Add skip navigation link to root layout - Add semantic HTML landmarks (main element) to login and settings pages - Add aria-labels to calendar day buttons with date, cycle day, and phase info - Add id="main-content" to dashboard main element for skip link target - Fix pre-existing type error in auth-middleware.test.ts Tests: 781 passing (11 new accessibility tests) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -187,4 +187,55 @@ describe("DayCell", () => {
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("accessibility", () => {
|
||||
it("has aria-label with date and phase information", () => {
|
||||
render(
|
||||
<DayCell
|
||||
{...baseProps}
|
||||
date={new Date("2026-01-15")}
|
||||
cycleDay={5}
|
||||
phase="FOLLICULAR"
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveAttribute(
|
||||
"aria-label",
|
||||
"January 15, 2026 - Cycle day 5 - Follicular phase",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes today indicator in aria-label when isToday", () => {
|
||||
render(
|
||||
<DayCell
|
||||
{...baseProps}
|
||||
date={new Date("2026-01-15")}
|
||||
cycleDay={5}
|
||||
phase="FOLLICULAR"
|
||||
isToday={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveAttribute(
|
||||
"aria-label",
|
||||
"January 15, 2026 - Cycle day 5 - Follicular phase (today)",
|
||||
);
|
||||
});
|
||||
|
||||
it("formats phase name correctly for screen readers", () => {
|
||||
render(<DayCell {...baseProps} phase="EARLY_LUTEAL" />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button.getAttribute("aria-label")).toContain("Early Luteal phase");
|
||||
});
|
||||
|
||||
it("formats LATE_LUTEAL phase name correctly", () => {
|
||||
render(<DayCell {...baseProps} phase="LATE_LUTEAL" />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button.getAttribute("aria-label")).toContain("Late Luteal phase");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,30 @@ const PHASE_COLORS: Record<CyclePhase, string> = {
|
||||
LATE_LUTEAL: "bg-red-100",
|
||||
};
|
||||
|
||||
const PHASE_DISPLAY_NAMES: Record<CyclePhase, string> = {
|
||||
MENSTRUAL: "Menstrual",
|
||||
FOLLICULAR: "Follicular",
|
||||
OVULATION: "Ovulation",
|
||||
EARLY_LUTEAL: "Early Luteal",
|
||||
LATE_LUTEAL: "Late Luteal",
|
||||
};
|
||||
|
||||
function formatAriaLabel(
|
||||
date: Date,
|
||||
cycleDay: number,
|
||||
phase: CyclePhase,
|
||||
isToday: boolean,
|
||||
): string {
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
const phaseName = PHASE_DISPLAY_NAMES[phase];
|
||||
const todaySuffix = isToday ? " (today)" : "";
|
||||
return `${dateStr} - Cycle day ${cycleDay} - ${phaseName} phase${todaySuffix}`;
|
||||
}
|
||||
|
||||
export function DayCell({
|
||||
date,
|
||||
cycleDay,
|
||||
@@ -25,10 +49,13 @@ export function DayCell({
|
||||
isToday,
|
||||
onClick,
|
||||
}: DayCellProps) {
|
||||
const ariaLabel = formatAriaLabel(date, cycleDay, phase, isToday);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
className={`p-2 rounded ${PHASE_COLORS[phase]} ${isToday ? "ring-2 ring-black" : ""}`}
|
||||
>
|
||||
<span className="text-sm font-medium">{date.getDate()}</span>
|
||||
|
||||
@@ -70,16 +70,20 @@ describe("MonthView", () => {
|
||||
it("highlights today's date", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Jan 15 is "today" - find the button containing "15"
|
||||
const todayCell = screen.getByRole("button", { name: /^15\s*Day 15/i });
|
||||
// Jan 15 is "today" - aria-label includes date, cycle day, and phase
|
||||
const todayCell = screen.getByRole("button", {
|
||||
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
|
||||
});
|
||||
expect(todayCell).toHaveClass("ring-2", "ring-black");
|
||||
});
|
||||
|
||||
it("does not highlight non-today dates", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Jan 1 is not today
|
||||
const otherCell = screen.getByRole("button", { name: /^1\s*Day 1/i });
|
||||
// Jan 1 is not today - aria-label includes date, cycle day, and phase
|
||||
const otherCell = screen.getByRole("button", {
|
||||
name: /January 1, 2026 - Cycle day 1 - Menstrual phase$/i,
|
||||
});
|
||||
expect(otherCell).not.toHaveClass("ring-2");
|
||||
});
|
||||
});
|
||||
@@ -89,7 +93,9 @@ describe("MonthView", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Days 1-3 are MENSTRUAL (bg-blue-100)
|
||||
const day1 = screen.getByRole("button", { name: /^1\s*Day 1/i });
|
||||
const day1 = screen.getByRole("button", {
|
||||
name: /January 1, 2026 - Cycle day 1 - Menstrual phase/i,
|
||||
});
|
||||
expect(day1).toHaveClass("bg-blue-100");
|
||||
});
|
||||
|
||||
@@ -97,7 +103,9 @@ describe("MonthView", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Day 5 is FOLLICULAR (bg-green-100)
|
||||
const day5 = screen.getByRole("button", { name: /^5\s*Day 5/i });
|
||||
const day5 = screen.getByRole("button", {
|
||||
name: /January 5, 2026 - Cycle day 5 - Follicular phase/i,
|
||||
});
|
||||
expect(day5).toHaveClass("bg-green-100");
|
||||
});
|
||||
|
||||
@@ -105,7 +113,9 @@ describe("MonthView", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Day 15 is OVULATION (bg-purple-100)
|
||||
const day15 = screen.getByRole("button", { name: /^15\s*Day 15/i });
|
||||
const day15 = screen.getByRole("button", {
|
||||
name: /January 15, 2026 - Cycle day 15 - Ovulation phase/i,
|
||||
});
|
||||
expect(day15).toHaveClass("bg-purple-100");
|
||||
});
|
||||
|
||||
@@ -113,7 +123,9 @@ describe("MonthView", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Day 20 is EARLY_LUTEAL (bg-yellow-100)
|
||||
const day20 = screen.getByRole("button", { name: /^20\s*Day 20/i });
|
||||
const day20 = screen.getByRole("button", {
|
||||
name: /January 20, 2026 - Cycle day 20 - Early Luteal phase/i,
|
||||
});
|
||||
expect(day20).toHaveClass("bg-yellow-100");
|
||||
});
|
||||
|
||||
@@ -121,7 +133,9 @@ describe("MonthView", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Day 25 is LATE_LUTEAL (bg-red-100)
|
||||
const day25 = screen.getByRole("button", { name: /^25\s*Day 25/i });
|
||||
const day25 = screen.getByRole("button", {
|
||||
name: /January 25, 2026 - Cycle day 25 - Late Luteal phase/i,
|
||||
});
|
||||
expect(day25).toHaveClass("bg-red-100");
|
||||
});
|
||||
});
|
||||
@@ -219,11 +233,16 @@ describe("MonthView", () => {
|
||||
);
|
||||
|
||||
// Jan 1 should be day 28 (late luteal)
|
||||
const jan1 = screen.getByRole("button", { name: /^1\s*Day 28/i });
|
||||
// Button now has aria-label with full date, cycle day, and phase
|
||||
const jan1 = screen.getByRole("button", {
|
||||
name: /January 1, 2026 - Cycle day 28 - Late Luteal phase/i,
|
||||
});
|
||||
expect(jan1).toHaveClass("bg-red-100"); // LATE_LUTEAL
|
||||
|
||||
// Jan 2 should be day 1 (menstrual)
|
||||
const jan2 = screen.getByRole("button", { name: /^2\s*Day 1/i });
|
||||
const jan2 = screen.getByRole("button", {
|
||||
name: /January 2, 2026 - Cycle day 1 - Menstrual phase/i,
|
||||
});
|
||||
expect(jan2).toHaveClass("bg-blue-100"); // MENSTRUAL
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user