mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(coach): open the deep-linked sheet only once the viewport is known
/coach?settings=data opened the conversations sheet from the first render. The server and hydration renders do not know the viewport and take the narrow layout, so on a desktop the sheet was open until the docked panel replaced it; only the batching of the hydration re-render kept it off the screen. The deep link now opens the sheet only once hydrated on a viewport that does not dock the panel; on a docked one the panel is revealed as before. The desktop deep-link spec records every mount of the sheet from the first byte and expects none; a phone deep-link spec covers the sheet path and serves as the watcher's positive control.
This commit is contained in:
1 parent
4bb03600b3
commit
4db7fc5113
3 files changed
+139
-2
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { sheetShown } from "../conversations-panel";
|
||||
|
||||
/**
|
||||
* `/coach?settings=data` opens the phone sheet, and only the phone sheet.
|
||||
* The server and hydration renders take the narrow layout (the viewport is
|
||||
* not known yet), so a deep link that opened the sheet from the first
|
||||
* render flashed it on a desktop before the docked panel replaced it.
|
||||
*/
|
||||
describe("sheetShown", () => {
|
||||
const deepLink = { chosen: false, deepLinkPending: true };
|
||||
|
||||
it("keeps the sheet shut while the viewport is not known", () => {
|
||||
expect(sheetShown({ ...deepLink, hydrated: false, docked: false })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("never opens the sheet for a deep link on a docked viewport", () => {
|
||||
expect(sheetShown({ ...deepLink, hydrated: true, docked: true })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("opens the sheet for a deep link on a phone once hydrated", () => {
|
||||
expect(sheetShown({ ...deepLink, hydrated: true, docked: false })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("follows the person's own choice otherwise", () => {
|
||||
const base = { deepLinkPending: false, hydrated: true, docked: false };
|
||||
expect(sheetShown({ ...base, chosen: true })).toBe(true);
|
||||
expect(sheetShown({ ...base, chosen: false })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef, useState, useSyncExternalStore } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
PanelRightClose,
|
||||
@@ -48,6 +48,25 @@ import { useDeleteCoachConversationWithUndo } from "./use-coach";
|
||||
*/
|
||||
export const COACH_PANEL_ID = "coach-conversations-panel";
|
||||
|
||||
const noSubscription = () => () => {};
|
||||
|
||||
/**
|
||||
* Whether the phone sheet is open. A settings deep link opens it only once
|
||||
* the viewport is known: the server and hydration renders take the narrow
|
||||
* (sheet) layout, so opening it from the first render flashed the sheet on
|
||||
* a desktop before the docked panel replaced it.
|
||||
*/
|
||||
export function sheetShown(state: {
|
||||
chosen: boolean;
|
||||
deepLinkPending: boolean;
|
||||
hydrated: boolean;
|
||||
docked: boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
state.chosen || (state.deepLinkPending && state.hydrated && !state.docked)
|
||||
);
|
||||
}
|
||||
|
||||
export interface ConversationsPanelProps {
|
||||
activeId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
@@ -75,7 +94,26 @@ export function ConversationsPanel({
|
||||
// A settings deep-link shows the panel without rewriting the remembered
|
||||
// choice; the next toggle settles it.
|
||||
const [revealed, setRevealed] = useState(openSettingsOnData);
|
||||
const [sheetOpen, setSheetOpen] = useState(openSettingsOnData);
|
||||
const hydrated = useSyncExternalStore(
|
||||
noSubscription,
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const [deepLinkPending, setDeepLinkPending] = useState(openSettingsOnData);
|
||||
const [sheetChosen, setSheetChosen] = useState(false);
|
||||
const sheetOpen = sheetShown({
|
||||
chosen: sheetChosen,
|
||||
deepLinkPending,
|
||||
hydrated,
|
||||
docked,
|
||||
});
|
||||
// On a docked viewport the deep link is served by `revealed`; it must not
|
||||
// open the sheet later if the window narrows.
|
||||
if (deepLinkPending && hydrated && docked) setDeepLinkPending(false);
|
||||
const setSheetOpen = (next: boolean) => {
|
||||
setDeepLinkPending(false);
|
||||
setSheetChosen(next);
|
||||
};
|
||||
const [settingsOpen, setSettingsOpen] = useState(openSettingsOnData);
|
||||
const [settingsOnData, setSettingsOnData] = useState(openSettingsOnData);
|
||||
|
||||
|
||||
Reference in new issue
Block a user