"use client"; import { useCallback, useEffect, useRef } from "react"; /** * Lets the system Back gesture/button close the topmost layer first. * * While `open` is true a same-URL history entry is pushed; a popstate (the * Android back gesture, the browser back button, an iOS edge swipe in * standalone) closes the layer instead of leaving the page. Layers form a * stack, so only the topmost one reacts. Closing a layer through its own UI * pops the pushed entry again — unless the layer closed because of a * navigation (a link inside the layer), in which case the entry is left for * the permanent listener to skip on the next Back press. */ type State = { freezmoLayer?: boolean }; const stack: symbol[] = []; export function useBackClose(open: boolean, onClose: () => void) { const closeRef = useRef(onClose); closeRef.current = onClose; const openRef = useRef(open); openRef.current = open; const idRef = useRef(Symbol("layer")); const pushedRef = useRef(false); const closingRef = useRef(false); const skipRef = useRef(false); const poppedRef = useRef(false); const urlRef = useRef(""); /** Call before closing for a navigation: the entry is left for the skipper. */ const skipRewind = useCallback(() => { skipRef.current = true; }, []); // Push the entry when the layer opens; rewind it when it closes on its own. useEffect(() => { const id = idRef.current; if (open) { if (pushedRef.current) return; pushedRef.current = true; urlRef.current = window.location.href; stack.push(id); try { window.history.pushState({ freezmoLayer: true }, ""); } catch { pushedRef.current = false; stack.splice(stack.indexOf(id), 1); } return; } if (!pushedRef.current) return; pushedRef.current = false; const index = stack.indexOf(id); if (index >= 0) stack.splice(index, 1); if (skipRef.current) { // Closed because of a navigation: leave the entry alone, the permanent // listener skips it on the next Back press. skipRef.current = false; return; } if (poppedRef.current) { // The Back press already consumed the entry — nothing to rewind. poppedRef.current = false; return; } if (window.location.href === urlRef.current) { closingRef.current = true; try { window.history.back(); } catch { /* ignore */ } } }, [open]); // One listener per mounted component: close this layer if it is the top of // the stack; otherwise skip stale entries left behind by navigations. useEffect(() => { const id = idRef.current; const onPop = () => { if (closingRef.current) { closingRef.current = false; return; } if (openRef.current) { if (stack[stack.length - 1] !== id) return; poppedRef.current = true; closeRef.current(); return; } const state = window.history.state as State | null; if (state?.freezmoLayer) { try { window.history.back(); } catch { /* ignore */ } } }; window.addEventListener("popstate", onPop); return () => window.removeEventListener("popstate", onPop); }, []); return { skipRewind }; }