mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Hold the coach read strip's place while its read loads
The strip sits above the chart on every metric sub-page and painted nothing until its read landed, so the chart and everything under it jumped down a card's height after first paint. It now paints a card of the same anatomy while loading.
This commit is contained in:
1 parent
554f36aeab
commit
0d574f2f2d
2 files changed
+70
-5
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* The strip sits above the chart on every metric sub-page. While its read is
|
||||
* on the way it holds its place with a card of the same anatomy, so the chart
|
||||
* and everything below it do not jump down when the read lands.
|
||||
*/
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({
|
||||
user: { timezone: "UTC", dateOfBirth: null, gender: null },
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true }));
|
||||
|
||||
import { CoachReadStrip } from "../coach-read-strip";
|
||||
|
||||
describe("CoachReadStrip — placeholder while the read loads", () => {
|
||||
it("paints the card frame and title before the read lands", () => {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, enabled: false } },
|
||||
});
|
||||
const markup = renderToStaticMarkup(
|
||||
<QueryClientProvider client={client}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<CoachReadStrip metricType="WEIGHT" unit="kg" />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(markup).toContain('data-slot="coach-read-strip"');
|
||||
expect(markup).toContain('data-state="loading"');
|
||||
expect(markup).toContain('aria-busy="true"');
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { queryKeys } from "@/lib/query-keys";
|
||||
import { metricFractionDigits } from "@/lib/measurements/value-domain";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import type { CoachReadStripData } from "@/lib/insights/derived/coach-read-shape";
|
||||
|
||||
@@ -78,7 +79,7 @@ export function CoachReadStrip({
|
||||
const transformed = unitDisplay.isTransformed(metricType);
|
||||
const resolvedUnit = transformed ? unitDisplay.unitFor(metricType) : unit;
|
||||
|
||||
const { data } = useQuery({
|
||||
const { data, isError } = useQuery({
|
||||
queryKey: queryKeys.insightsCoachRead(metricType, locale),
|
||||
queryFn: () =>
|
||||
apiGet<CoachReadStripData>(
|
||||
@@ -88,10 +89,34 @@ export function CoachReadStrip({
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Match the rest of the query-dependent insights chrome: don't paint a
|
||||
// branch during SSR / hydration (React #418) and don't paint until the
|
||||
// read lands.
|
||||
if (!mounted || !data) return null;
|
||||
// Don't paint a data branch during SSR / hydration (React #418). While the
|
||||
// read is on its way the strip holds its place with a card of the same
|
||||
// shape: it lands above the chart, and popping in later pushed the chart
|
||||
// and everything under it down the page. A landed read always paints (the
|
||||
// baseline line degrades to the "learning" copy), so the placeholder only
|
||||
// ever gives way to a card of its own height; a failed read drops it.
|
||||
if (!mounted || !data) {
|
||||
if (!isAuthenticated || isError) return null;
|
||||
return (
|
||||
<Card
|
||||
data-slot="coach-read-strip"
|
||||
data-state="loading"
|
||||
aria-busy="true"
|
||||
className="gap-2 py-3 md:py-4"
|
||||
>
|
||||
<CardHeader>
|
||||
<TileHeader
|
||||
icon={Sparkles}
|
||||
title={t("insights.coach.readStrip.label")}
|
||||
titleAs="h2"
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-5.5 w-3/4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const fmt = (value: number): string =>
|
||||
new Intl.NumberFormat(locale, {
|
||||
|
||||
Reference in new issue
Block a user