diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index b10ce0c..144a1ea 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -4,7 +4,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta ## Current State Summary -### Overall Status: 770 tests passing across 43 test files +### Overall Status: 781 tests passing across 43 test files ### Library Implementation | File | Status | Gap Analysis | @@ -90,7 +90,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta | `src/app/api/cycle/current/route.test.ts` | **EXISTS** - 10 tests (GET current cycle, auth, all phases, rollover, custom lengths) | | `src/app/api/today/route.test.ts` | **EXISTS** - 22 tests (daily snapshot, auth, decision, overrides, phases, nutrition, biometrics) | | `src/app/api/overrides/route.test.ts` | **EXISTS** - 14 tests (POST/DELETE overrides, auth, validation, type checks) | -| `src/app/login/page.test.tsx` | **EXISTS** - 14 tests (form rendering, auth flow, error handling, validation) | +| `src/app/login/page.test.tsx` | **EXISTS** - 26 tests (form rendering, auth flow, error handling, validation, accessibility) | | `src/app/page.test.tsx` | **EXISTS** - 28 tests (data fetching, component rendering, override toggles, error handling) | | `src/lib/nutrition.test.ts` | **EXISTS** - 17 tests (seed cycling, carb ranges, keto guidance by phase) | | `src/lib/email.test.ts` | **EXISTS** - 24 tests (email content, subject lines, formatting, token expiration warnings) | @@ -109,7 +109,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta | `src/app/api/metrics/route.test.ts` | **EXISTS** - 15 tests (Prometheus format validation, metric types, route handling) | | `src/components/calendar/month-view.test.tsx` | **EXISTS** - 21 tests (calendar grid, phase colors, navigation, legend) | | `src/app/calendar/page.test.tsx` | **EXISTS** - 23 tests (rendering, navigation, ICS subscription, token regeneration) | -| `src/app/settings/page.test.tsx` | **EXISTS** - 24+ tests (form rendering, validation, submission) | +| `src/app/settings/page.test.tsx` | **EXISTS** - 29 tests (form rendering, validation, submission, accessibility) | | `src/app/settings/garmin/page.test.tsx` | **EXISTS** - 27 tests (connection status, token management) | | `src/components/dashboard/decision-card.test.tsx` | **EXISTS** - 11 tests (rendering, status icons, styling) | | `src/components/dashboard/data-panel.test.tsx` | **EXISTS** - 18 tests (biometrics display, null handling, styling) | @@ -117,8 +117,9 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta | `src/components/dashboard/override-toggles.test.tsx` | **EXISTS** - 18 tests (toggle states, callbacks, styling) | | `src/components/dashboard/mini-calendar.test.tsx` | **EXISTS** - 23 tests (calendar grid, phase colors, navigation, legend) | | `src/components/dashboard/onboarding-banner.test.tsx` | **EXISTS** - 16 tests (setup prompts, icons, action buttons, interactions, dismissal) | -| `src/components/calendar/day-cell.test.tsx` | **EXISTS** - 23 tests (phase coloring, today highlighting, click handling) | +| `src/components/calendar/day-cell.test.tsx` | **EXISTS** - 27 tests (phase coloring, today highlighting, click handling, accessibility) | | `src/app/plan/page.test.tsx` | **EXISTS** - 16 tests (loading states, error handling, phase display, exercise reference, rebounding techniques) | +| `src/app/layout.test.tsx` | **EXISTS** - 3 tests (skip navigation link rendering, accessibility) | | E2E tests | **AUTHORIZED SKIP** - Per specs/testing.md | ### Critical Business Rules (from Spec) @@ -726,16 +727,30 @@ Enhancements from spec requirements that improve user experience. - `src/components/dashboard/onboarding-banner.test.tsx` - 16 tests covering rendering, interactions, dismissal - **Why:** Helps new users complete setup for full functionality -### P4.2: Accessibility Improvements -- [ ] Keyboard navigation and focus indicators +### P4.2: Accessibility Improvements (PARTIAL COMPLETE) +- [x] Skip navigation link +- [x] Semantic HTML landmarks (main elements) +- [x] Screen reader labels for calendar buttons +- [ ] Keyboard navigation for calendar - **Spec Reference:** specs/dashboard.md accessibility requirements -- **Requirements:** - - Keyboard navigation for all interactive elements - - Visible focus indicators (focus:ring styles) - - 4.5:1 minimum contrast ratio - - Screen reader labels where needed +- **Implementation Details:** + - Skip navigation link added to layout with sr-only styling + - Semantic HTML landmarks (main element) added to login and settings pages + - Aria-labels added to DayCell calendar buttons with date and phase information + - Tests added: layout.test.tsx (3 tests), accessibility tests in login/settings page tests - **Files:** - - All component files - Add focus:ring classes, aria-labels + - `src/app/layout.tsx` - Added skip navigation link with sr-only styling + - `src/app/layout.test.tsx` - 3 tests for skip link rendering and accessibility + - `src/app/login/page.tsx` - Wrapped content in main element + - `src/app/login/page.test.tsx` - Added 2 accessibility tests (26 total) + - `src/app/settings/page.tsx` - Wrapped content in main element + - `src/app/settings/page.test.tsx` - Added 2 accessibility tests (29 total) + - `src/app/page.tsx` - Added id="main-content" to existing main element + - `src/components/calendar/day-cell.tsx` - Added aria-label with date/phase info + - `src/components/calendar/day-cell.test.tsx` - Added 4 accessibility tests (27 total) + - `src/components/calendar/month-view.test.tsx` - Updated tests to match new aria-labels +- **Remaining Work:** + - Keyboard navigation for calendar (arrow keys to navigate between dates) - **Why:** Required for accessibility compliance ### P4.3: Dark Mode Configuration @@ -822,7 +837,8 @@ P4.* UX Polish ────────> After core functionality complete | Priority | Task | Effort | Notes | |----------|------|--------|-------| -| Low | P4.* UX Polish | Various | After core complete | +| Low | P4.2 Keyboard Navigation | Small | Calendar arrow key navigation pending | +| Low | P4.3-P4.6 UX Polish | Various | After core complete | ### Dependency Summary diff --git a/src/app/layout.test.tsx b/src/app/layout.test.tsx new file mode 100644 index 0000000..5d680f2 --- /dev/null +++ b/src/app/layout.test.tsx @@ -0,0 +1,60 @@ +// ABOUTME: Unit tests for the root layout component. +// ABOUTME: Tests accessibility features including skip navigation link. +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +// Mock next/font/google before importing the layout +vi.mock("next/font/google", () => ({ + Geist: () => ({ + variable: "--font-geist-sans", + className: "geist-sans", + }), + Geist_Mono: () => ({ + variable: "--font-geist-mono", + className: "geist-mono", + }), +})); + +import RootLayout from "./layout"; + +describe("RootLayout", () => { + describe("accessibility", () => { + it("renders a skip navigation link as the first focusable element", () => { + render( + +
Test content
+
, + ); + + const skipLink = screen.getByRole("link", { + name: /skip to main content/i, + }); + expect(skipLink).toBeInTheDocument(); + expect(skipLink).toHaveAttribute("href", "#main-content"); + }); + + it("skip link has visually hidden styles but is focusable", () => { + render( + +
Test content
+
, + ); + + const skipLink = screen.getByRole("link", { + name: /skip to main content/i, + }); + expect(skipLink).toHaveClass("sr-only"); + expect(skipLink).toHaveClass("focus:not-sr-only"); + }); + + it("renders children within the body", () => { + render( + +
Test content
+
, + ); + + expect(screen.getByTestId("child-content")).toBeInTheDocument(); + }); + }); +}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1e51714..72cc07d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -30,6 +30,12 @@ export default function RootLayout({ + + Skip to main content + {children} diff --git a/src/app/login/page.test.tsx b/src/app/login/page.test.tsx index b5df759..87f3593 100644 --- a/src/app/login/page.test.tsx +++ b/src/app/login/page.test.tsx @@ -541,4 +541,24 @@ describe("LoginPage", () => { ).not.toBeInTheDocument(); }); }); + + describe("accessibility", () => { + it("wraps content in a main element", async () => { + render(); + + await waitFor(() => { + expect(screen.getByRole("main")).toBeInTheDocument(); + }); + }); + + it("has proper heading structure with h1", async () => { + render(); + + await waitFor(() => { + const heading = screen.getByRole("heading", { level: 1 }); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent(/phaseflow/i); + }); + }); + }); }); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 79bd616..2c7a4c0 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -97,17 +97,23 @@ export default function LoginPage() { // Show loading state while checking auth methods if (isCheckingAuth) { return ( -
+

PhaseFlow

Loading...
-
+ ); } return ( -
+

PhaseFlow

@@ -181,6 +187,6 @@ export default function LoginPage() { )}
-
+ ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index cb203af..e853630 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -156,7 +156,7 @@ export default function Dashboard() { -
+
{loading && } {error && ( diff --git a/src/app/settings/page.test.tsx b/src/app/settings/page.test.tsx index 5f9d200..96e0c75 100644 --- a/src/app/settings/page.test.tsx +++ b/src/app/settings/page.test.tsx @@ -505,4 +505,24 @@ describe("SettingsPage", () => { expect(screen.queryByText(/settings saved/i)).not.toBeInTheDocument(); }); }); + + describe("accessibility", () => { + it("wraps content in a main element", async () => { + render(); + + await waitFor(() => { + expect(screen.getByRole("main")).toBeInTheDocument(); + }); + }); + + it("has proper heading structure with h1", async () => { + render(); + + await waitFor(() => { + const heading = screen.getByRole("heading", { level: 1 }); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent(/settings/i); + }); + }); + }); }); diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 2cfa3a5..11b8a87 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -104,15 +104,15 @@ export default function SettingsPage() { if (loading) { return ( -
+

Settings

Loading...

-
+
); } return ( -
+

Settings

-
+
); } diff --git a/src/components/calendar/day-cell.test.tsx b/src/components/calendar/day-cell.test.tsx index 36e1f40..1cb5b66 100644 --- a/src/components/calendar/day-cell.test.tsx +++ b/src/components/calendar/day-cell.test.tsx @@ -187,4 +187,55 @@ describe("DayCell", () => { expect(screen.getByText("1")).toBeInTheDocument(); }); }); + + describe("accessibility", () => { + it("has aria-label with date and phase information", () => { + render( + , + ); + + 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( + , + ); + + 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(); + + const button = screen.getByRole("button"); + expect(button.getAttribute("aria-label")).toContain("Early Luteal phase"); + }); + + it("formats LATE_LUTEAL phase name correctly", () => { + render(); + + const button = screen.getByRole("button"); + expect(button.getAttribute("aria-label")).toContain("Late Luteal phase"); + }); + }); }); diff --git a/src/components/calendar/day-cell.tsx b/src/components/calendar/day-cell.tsx index e93ea10..cf33965 100644 --- a/src/components/calendar/day-cell.tsx +++ b/src/components/calendar/day-cell.tsx @@ -18,6 +18,30 @@ const PHASE_COLORS: Record = { LATE_LUTEAL: "bg-red-100", }; +const PHASE_DISPLAY_NAMES: Record = { + 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 (