Add calendar keyboard navigation (P4.2 complete)
All checks were successful
Deploy / deploy (push) Successful in 2m26s
All checks were successful
Deploy / deploy (push) Successful in 2m26s
Implement keyboard navigation for MonthView calendar: - ArrowLeft/Right: navigate to previous/next day - ArrowUp/Down: navigate to previous/next week (7 days) - Home/End: navigate to first/last day of month - Boundary navigation triggers month change Features: - Added role="grid" for proper ARIA semantics - Added data-day attribute to DayCell for focus management - Wrapped navigation handlers in useCallback for stability Tests: 9 new tests for keyboard navigation (790 total) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -265,4 +265,154 @@ describe("MonthView", () => {
|
||||
expect(screen.getByText("29")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyboard navigation", () => {
|
||||
it("moves focus to next day when pressing ArrowRight", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Focus on Jan 15 (today)
|
||||
const jan15 = screen.getByRole("button", {
|
||||
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
|
||||
});
|
||||
jan15.focus();
|
||||
|
||||
// Press ArrowRight to move to Jan 16
|
||||
fireEvent.keyDown(jan15, { key: "ArrowRight" });
|
||||
|
||||
const jan16 = screen.getByRole("button", {
|
||||
name: /January 16, 2026 - Cycle day 16 - Ovulation phase$/i,
|
||||
});
|
||||
expect(document.activeElement).toBe(jan16);
|
||||
});
|
||||
|
||||
it("moves focus to previous day when pressing ArrowLeft", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Focus on Jan 15 (today)
|
||||
const jan15 = screen.getByRole("button", {
|
||||
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
|
||||
});
|
||||
jan15.focus();
|
||||
|
||||
// Press ArrowLeft to move to Jan 14
|
||||
fireEvent.keyDown(jan15, { key: "ArrowLeft" });
|
||||
|
||||
const jan14 = screen.getByRole("button", {
|
||||
name: /January 14, 2026 - Cycle day 14 - Follicular phase$/i,
|
||||
});
|
||||
expect(document.activeElement).toBe(jan14);
|
||||
});
|
||||
|
||||
it("moves focus to same day next week when pressing ArrowDown", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Focus on Jan 15 (today)
|
||||
const jan15 = screen.getByRole("button", {
|
||||
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
|
||||
});
|
||||
jan15.focus();
|
||||
|
||||
// Press ArrowDown to move to Jan 22 (7 days later)
|
||||
fireEvent.keyDown(jan15, { key: "ArrowDown" });
|
||||
|
||||
const jan22 = screen.getByRole("button", {
|
||||
name: /January 22, 2026 - Cycle day 22 - Early Luteal phase$/i,
|
||||
});
|
||||
expect(document.activeElement).toBe(jan22);
|
||||
});
|
||||
|
||||
it("moves focus to same day previous week when pressing ArrowUp", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Focus on Jan 15 (today)
|
||||
const jan15 = screen.getByRole("button", {
|
||||
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
|
||||
});
|
||||
jan15.focus();
|
||||
|
||||
// Press ArrowUp to move to Jan 8 (7 days earlier)
|
||||
fireEvent.keyDown(jan15, { key: "ArrowUp" });
|
||||
|
||||
const jan8 = screen.getByRole("button", {
|
||||
name: /January 8, 2026 - Cycle day 8 - Follicular phase$/i,
|
||||
});
|
||||
expect(document.activeElement).toBe(jan8);
|
||||
});
|
||||
|
||||
it("calls onMonthChange when navigating past end of month with ArrowRight", () => {
|
||||
const onMonthChange = vi.fn();
|
||||
render(<MonthView {...baseProps} onMonthChange={onMonthChange} />);
|
||||
|
||||
// Focus on Jan 31 (last day of month)
|
||||
// With lastPeriod Jan 1, cycleLength 28: Jan 31 = cycle day 3 (MENSTRUAL)
|
||||
const jan31 = screen.getByRole("button", {
|
||||
name: /January 31, 2026 - Cycle day 3 - Menstrual phase$/i,
|
||||
});
|
||||
jan31.focus();
|
||||
|
||||
// Press ArrowRight - should trigger month change to February
|
||||
fireEvent.keyDown(jan31, { key: "ArrowRight" });
|
||||
|
||||
expect(onMonthChange).toHaveBeenCalledWith(2026, 1);
|
||||
});
|
||||
|
||||
it("calls onMonthChange when navigating before start of month with ArrowLeft", () => {
|
||||
const onMonthChange = vi.fn();
|
||||
render(<MonthView {...baseProps} onMonthChange={onMonthChange} />);
|
||||
|
||||
// Focus on Jan 1 (first day of month)
|
||||
const jan1 = screen.getByRole("button", {
|
||||
name: /January 1, 2026 - Cycle day 1 - Menstrual phase$/i,
|
||||
});
|
||||
jan1.focus();
|
||||
|
||||
// Press ArrowLeft - should trigger month change to December 2025
|
||||
fireEvent.keyDown(jan1, { key: "ArrowLeft" });
|
||||
|
||||
expect(onMonthChange).toHaveBeenCalledWith(2025, 11);
|
||||
});
|
||||
|
||||
it("wraps focus at row boundaries for Home and End keys", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Focus on Jan 15
|
||||
const jan15 = screen.getByRole("button", {
|
||||
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
|
||||
});
|
||||
jan15.focus();
|
||||
|
||||
// Press Home to move to first day of month
|
||||
fireEvent.keyDown(jan15, { key: "Home" });
|
||||
|
||||
const jan1 = screen.getByRole("button", {
|
||||
name: /January 1, 2026 - Cycle day 1 - Menstrual phase$/i,
|
||||
});
|
||||
expect(document.activeElement).toBe(jan1);
|
||||
});
|
||||
|
||||
it("moves focus to last day when pressing End key", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
// Focus on Jan 15
|
||||
const jan15 = screen.getByRole("button", {
|
||||
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
|
||||
});
|
||||
jan15.focus();
|
||||
|
||||
// Press End to move to last day of month
|
||||
fireEvent.keyDown(jan15, { key: "End" });
|
||||
|
||||
// With lastPeriod Jan 1, cycleLength 28: Jan 31 = cycle day 3 (MENSTRUAL)
|
||||
const jan31 = screen.getByRole("button", {
|
||||
name: /January 31, 2026 - Cycle day 3 - Menstrual phase$/i,
|
||||
});
|
||||
expect(document.activeElement).toBe(jan31);
|
||||
});
|
||||
|
||||
it("calendar grid has proper role for accessibility", () => {
|
||||
render(<MonthView {...baseProps} />);
|
||||
|
||||
expect(screen.getByRole("grid")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user