import { useCallback, useEffect, useRef } from "react"; // While a sheet is open, browser Back asks the sheet instead of leaving the page: a same-URL entry is // pushed on open and Back pops it. Leaving the page outright gets the browser prompt while edits are unsaved, // and an in-app link asks the sheet first while edits are unsaved. export const useSheetBackGuard = (open: boolean, isDirty: () => boolean, onLeave: (to?: string) => void) => { const onLeaveRef = useRef(onLeave); onLeaveRef.current = onLeave; const isDirtyRef = useRef(isDirty); isDirtyRef.current = isDirty; const detachRef = useRef<(() => void) | null>(null); const leftRef = useRef(false); useEffect(() => { if (!open) return; const stamp = `sheet-${Date.now()}`; leftRef.current = false; // The router keeps its own index in history state, so the marker is merged in rather than replacing it. const mark = () => window.history.pushState({ ...window.history.state, akashicSheet: stamp }, "", window.location.href); mark(); const onPop = () => { mark(); onLeaveRef.current(); }; const onUnload = (event: BeforeUnloadEvent) => { if (!isDirtyRef.current()) return; event.preventDefault(); event.returnValue = ""; }; // Capture phase, so an unsaved sheet is asked before the router handles the link. const onClick = (event: MouseEvent) => { if (event.defaultPrevented || event.button !== 0) return; if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; const link = event.target instanceof Element ? event.target.closest("a[href]") : null; if (!link || (link.target !== "" && link.target !== "_self")) return; const to = new URL(link.href, window.location.href); const here = new URL(window.location.href); if (to.origin !== here.origin || to.pathname !== here.pathname || to.search !== here.search) return; if (to.hash === here.hash || !isDirtyRef.current()) return; event.preventDefault(); event.stopPropagation(); onLeaveRef.current(to.hash); }; window.addEventListener("popstate", onPop); window.addEventListener("beforeunload", onUnload); document.addEventListener("click", onClick, true); const detach = () => { window.removeEventListener("popstate", onPop); window.removeEventListener("beforeunload", onUnload); document.removeEventListener("click", onClick, true); }; detachRef.current = detach; return () => { detach(); detachRef.current = null; if (leftRef.current) return; // Closed from inside the sheet: take back the entry it added, unless navigation already moved on. if ((window.history.state as { akashicSheet?: string } | null)?.akashicSheet === stamp) window.history.back(); }; }, [open]); // The confirmed navigation takes over the sheet entry rather than stacking on it, then tells the router. const leaveTo = useCallback((hash: string) => { detachRef.current?.(); detachRef.current = null; leftRef.current = true; const state: Record = { ...(window.history.state as Record | null) }; delete state.akashicSheet; const target = new URL(window.location.href); target.hash = hash; window.history.replaceState(state, "", target.href); window.dispatchEvent(new PopStateEvent("popstate", { state })); }, []); return { leaveTo }; };