mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(day): step the open day from the URL, not from the last rendered day
A step read the day the layer had last published, which reaches the shortcut and the header arrows through useSearchParams, a render and an effect. Next 16.4 lets that chain trail a replaceState by more than a key repeat, so two quick presses of ] both stepped from the same day and one was lost. stepDay now reads ?day= from window.location, which the step itself writes, and refuses a step past today on that value too.
This commit is contained in:
1 parent
fdd0f50130
commit
76af868064
4 files changed
+64
-16
No files matched your search
@@ -6,7 +6,8 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
*
|
||||
* - opening adds exactly one entry, so Back closes the layer;
|
||||
* - opening another day while one is open swaps it in place;
|
||||
* - stepping to the neighbouring day never adds an entry, however often;
|
||||
* - stepping to the neighbouring day never adds an entry, however often,
|
||||
* starts from the day in the URL and never passes today;
|
||||
* - closing a day the layer opened goes Back (the next Back leaves the
|
||||
* page); closing a deep link only drops the parameter and stays.
|
||||
*/
|
||||
@@ -57,6 +58,8 @@ function installFakeBrowser(initialUrl: string) {
|
||||
|
||||
let browser: ReturnType<typeof installFakeBrowser>;
|
||||
|
||||
const TODAY = "2026-01-10";
|
||||
|
||||
beforeEach(() => {
|
||||
browser = installFakeBrowser("/insights/blood-pressure?range=90");
|
||||
});
|
||||
@@ -88,18 +91,44 @@ describe("day layer history", () => {
|
||||
it("steps without adding history entries", async () => {
|
||||
const { openDay, stepDay } = await import("../day-layer-controller");
|
||||
openDay("2026-01-03");
|
||||
stepDay("2026-01-03", -1);
|
||||
stepDay("2026-01-02", -1);
|
||||
stepDay("2026-01-01", 1);
|
||||
stepDay(-1, TODAY);
|
||||
stepDay(-1, TODAY);
|
||||
stepDay(1, TODAY);
|
||||
expect(browser.length()).toBe(2);
|
||||
expect(browser.current()).toContain("day=2026-01-02");
|
||||
});
|
||||
|
||||
it("steps from the day in the URL, so quick steps in a row all count", async () => {
|
||||
// Nothing republishes the open day between these calls, which is what a
|
||||
// fast key repeat looks like while the layer is still rendering the
|
||||
// previous step. Each step must still move from where the last one went.
|
||||
const { openDay, stepDay } = await import("../day-layer-controller");
|
||||
openDay("2026-01-03");
|
||||
expect(stepDay(1, TODAY)).toBe("2026-01-04");
|
||||
expect(stepDay(1, TODAY)).toBe("2026-01-05");
|
||||
expect(browser.current()).toContain("day=2026-01-05");
|
||||
});
|
||||
|
||||
it("never steps past today, however often it is asked", async () => {
|
||||
const { openDay, stepDay } = await import("../day-layer-controller");
|
||||
openDay("2026-01-09");
|
||||
expect(stepDay(1, TODAY)).toBe(TODAY);
|
||||
expect(stepDay(1, TODAY)).toBeNull();
|
||||
expect(stepDay(1, TODAY)).toBeNull();
|
||||
expect(browser.current()).toContain(`day=${TODAY}`);
|
||||
});
|
||||
|
||||
it("does nothing while no day is open", async () => {
|
||||
const { stepDay } = await import("../day-layer-controller");
|
||||
expect(stepDay(-1, TODAY)).toBeNull();
|
||||
expect(browser.current()).toBe("/insights/blood-pressure?range=90");
|
||||
});
|
||||
|
||||
it("closes a pushed day with Back, landing on the page without it", async () => {
|
||||
const { closeDay, openDay, stepDay } =
|
||||
await import("../day-layer-controller");
|
||||
openDay("2026-01-03");
|
||||
stepDay("2026-01-03", 1);
|
||||
stepDay(1, TODAY);
|
||||
closeDay();
|
||||
expect(browser.current()).toBe("/insights/blood-pressure?range=90");
|
||||
});
|
||||
@@ -107,7 +136,7 @@ describe("day layer history", () => {
|
||||
it("closes a deep link in place: the parameter goes, the page stays", async () => {
|
||||
browser = installFakeBrowser("/?day=2026-01-03");
|
||||
const { closeDay, stepDay } = await import("../day-layer-controller");
|
||||
stepDay("2026-01-03", -1);
|
||||
stepDay(-1, TODAY);
|
||||
expect(browser.current()).toBe("/?day=2026-01-02");
|
||||
closeDay();
|
||||
expect(browser.current()).toBe("/");
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
import type { DateKey } from "@/lib/day/contract";
|
||||
import { DAY_QUERY_PARAM, type DateKey } from "@/lib/day/contract";
|
||||
|
||||
import {
|
||||
dayHistoryState,
|
||||
historyEntryOwnedByLayer,
|
||||
parseDayParam,
|
||||
shiftDateKey,
|
||||
withDayHref,
|
||||
} from "./day-url";
|
||||
@@ -155,8 +156,27 @@ export function openDay(date: DateKey, options: OpenDayOptions = {}): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Move the open day by `delta` calendar days, without a new history entry. */
|
||||
export function stepDay(from: DateKey, delta: number): DateKey {
|
||||
/**
|
||||
* Move the open day by `delta` calendar days, without a new history entry.
|
||||
* Null, and nothing written, when no day is open or the step would pass
|
||||
* `today`.
|
||||
*
|
||||
* The step starts from the day in the URL at the moment of the step, not
|
||||
* from the day a caller last rendered. The rendered day reaches a caller
|
||||
* through Next's `useSearchParams`, a render of the layer and its
|
||||
* `publishOpenDay`, and Next 16.4 lets that chain trail a `replaceState` by
|
||||
* more than a key repeat: two quick `]` presses then both stepped from the
|
||||
* same day and one of them was lost. `window.location` is written by the
|
||||
* step itself, so it is never behind.
|
||||
*/
|
||||
export function stepDay(delta: number, today: DateKey): DateKey | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const from = parseDayParam(
|
||||
new URLSearchParams(window.location.search).get(DAY_QUERY_PARAM),
|
||||
today,
|
||||
);
|
||||
if (from === null) return null;
|
||||
if (delta > 0 && from >= today) return null;
|
||||
const next = shiftDateKey(from, delta);
|
||||
jumpDay(next);
|
||||
return next;
|
||||
|
||||
@@ -348,8 +348,7 @@ function DayLayer() {
|
||||
const onStep = useCallback(
|
||||
(delta: number) => {
|
||||
if (date === null) return;
|
||||
if (delta > 0 && date >= today) return;
|
||||
stepDay(date, delta);
|
||||
stepDay(delta, today);
|
||||
},
|
||||
[date, today],
|
||||
);
|
||||
|
||||
@@ -192,12 +192,12 @@ export function GlobalShortcuts() {
|
||||
setShortcutsHelpOpen(true);
|
||||
return;
|
||||
case "day": {
|
||||
const date = state.openDay;
|
||||
if (date === null) return;
|
||||
if (state.openDay === null) return;
|
||||
event.preventDefault();
|
||||
// No day after today: the layer refuses it, so the step does too.
|
||||
if (action.delta > 0 && date >= state.today) return;
|
||||
stepDay(date, action.delta);
|
||||
// The step reads the day from the URL, which is ahead of
|
||||
// `openDay` while a previous step is still rendering, and refuses
|
||||
// a day after today itself.
|
||||
stepDay(action.delta, state.today);
|
||||
return;
|
||||
}
|
||||
case "day-toggle":
|
||||
|
||||
Reference in new issue
Block a user