fix(cycle): hold the calendar's height while the cycle read loads

The cycle page's calendar card showed one line of loading text until
its read arrived, then grew to the month grid, so the predictions and
everything below moved down once the page had drawn. The card now
draws the month header and this month's weeks in the grid's own cell
size while it loads, with the loading text kept for screen readers.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:14:45 +01:00
1 parent 848e88d58e
commit 2284040c6e
3 files changed
+58 -5

No files matched your search

@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { CycleCalendar } from "../cycle-calendar";
import { CycleCalendar, CycleCalendarSkeleton } from "../cycle-calendar";
import { I18nProvider } from "@/lib/i18n/context";
import type { CalendarDay } from "../types";
@@ -242,3 +242,20 @@ describe("<CycleCalendar>", () => {
});
});
});
describe("CycleCalendarSkeleton", () => {
// November 2026 starts on a Sunday and spans six weeks; February 2027
// starts on a Monday and spans four.
for (const today of ["2026-11-11", "2027-02-10"]) {
it(`holds as many weeks as the month it stands in for (${today})`, () => {
const calendar = render(<CycleCalendar days={[]} today={today} />);
const skeleton = render(<CycleCalendarSkeleton today={today} />);
// Every row but the weekday header is one week.
const weeks = (calendar.match(/role="row"/g)?.length ?? 0) - 1;
const cells = skeleton.match(/aspect-square/g)?.length ?? 0;
expect(weeks).toBeGreaterThan(3);
expect(cells).toBe(weeks * 7);
});
}
});
+38
View File
@@ -5,6 +5,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useTranslations } from "@/lib/i18n/context";
import { resolveIntlLocale } from "@/lib/format-locale";
import type { Locale } from "@/lib/i18n/config";
@@ -65,6 +66,43 @@ function mondayIndex(d: Date): number {
return (d.getDay() + 6) % 7;
}
/**
* The calendar's frame while the page's read is in flight: the month header
* and the weeks of `today`'s month in the grid's own cell size, so the card
* keeps the height the month takes instead of growing from a line of text.
* The loading text stays for screen readers.
*/
export function CycleCalendarSkeleton({ today }: { today: string }) {
const { t } = useTranslations();
const [y, m] = today.split("-").map(Number);
const daysInMonth = new Date(y, m, 0).getDate();
const weeks = Math.ceil(
(mondayIndex(new Date(y, m - 1, 1)) + daysInMonth) / 7,
);
return (
<div data-slot="cycle-calendar-skeleton">
<span className="sr-only">{t("cycle.calendar.loading")}</span>
<div aria-hidden="true" className="space-y-3">
<div className="flex items-center justify-between">
<Skeleton className="size-10 rounded-md pointer-coarse:min-h-11 pointer-coarse:min-w-11" />
<Skeleton className="h-5 w-32" />
<Skeleton className="size-10 rounded-md pointer-coarse:min-h-11 pointer-coarse:min-w-11" />
</div>
<div className="grid grid-cols-7 gap-1">
{Array.from({ length: 7 }, (_, i) => (
<div key={`weekday-${i}`} className="text-2xs pb-1">
&nbsp;
</div>
))}
{Array.from({ length: weeks * 7 }, (_, i) => (
<Skeleton key={i} className="aspect-square min-h-11 rounded-lg" />
))}
</div>
</div>
</div>
);
}
type Translate = ReturnType<typeof useTranslations>["t"];
const CELL_DATE_FORMATS = new Map<Locale, Intl.DateTimeFormat>();
+2 -4
View File
@@ -16,7 +16,7 @@ import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import { CycleRing } from "./cycle-ring";
import { CycleCalendar } from "./cycle-calendar";
import { CycleCalendar, CycleCalendarSkeleton } from "./cycle-calendar";
import { CycleHistoryChart } from "./cycle-history-chart";
import { LogDaySheet, sheetDayContext } from "./log-day-sheet";
import { PhaseEducationCard } from "./phase-education-card";
@@ -347,9 +347,7 @@ export function CycleView() {
<Card>
<CardContent className="py-4">
{loading ? (
<p className="text-muted-foreground text-sm">
{t("cycle.calendar.loading")}
</p>
<CycleCalendarSkeleton today={today} />
) : calendarError ? (
// Inside the calendar card already — a borderless row keeps
// it in line with the plain loading line above it (§3/§6).