All files / src/features/items useLeaveGuard.ts

97.61% Statements 41/42
96% Branches 24/25
100% Functions 9/9
100% Lines 35/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 595x     5x 5x 5x       5x           46x 29x 12x   12x 12x 12x 1x 1x   12x 7x 7x 7x 2x 1x 1x     12x 13x 13x 13x     4x 1x 1x   12x 12x 12x 12x 12x 12x 12x       12x        
"use client";
import { useEffect } from "react";
 
export const LEAVE_PROMPT = "You have changes that aren't saved yet. Leave anyway?";
const GUARD = "__adoredLeaveGuard";
let activeGuards = 0;
 
/** For in-app exits the guard can't see (a router.push after an action): true when
 *  nothing is unsaved, or the curator chooses to leave anyway. */
export const confirmLeave = () => activeGuards === 0 || window.confirm(LEAVE_PROMPT);
 
// App Router has no route-change event, so guard the ways out directly: a full unload,
// a same-origin link click (capture phase, before Next's <Link> handler), and — best
// effort — Back, by parking a duplicate of this history entry for Back to land on first.
export function useLeaveGuard(active: boolean): void {
  useEffect(() => {
    if (!active) return;
    activeGuards++;
    // Spread the existing state so Next's router keys survive on the duplicate entry.
    const arm = () =>
      window.history.pushState({ ...window.history.state, [GUARD]: true }, "", window.location.href);
    const onUnload = (e: BeforeUnloadEvent) => {
      e.preventDefault();
      e.returnValue = "";
    };
    const onClick = (e: MouseEvent) => {
      Iif (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
      const a = (e.target as Element | null)?.closest?.("a[href]");
      if (!(a instanceof HTMLAnchorElement) || a.target === "_blank" || a.origin !== window.location.origin) return;
      if (!window.confirm(LEAVE_PROMPT)) {
        e.preventDefault();
        e.stopPropagation();
      }
    };
    const stop = () => {
      window.removeEventListener("beforeunload", onUnload);
      window.removeEventListener("popstate", onPop);
      document.removeEventListener("click", onClick, true);
    };
    function onPop() {
      if (!window.confirm(LEAVE_PROMPT)) return arm();
      stop(); // our own back() must not ask twice
      window.history.back();
    }
    if (!window.history.state?.[GUARD]) arm();
    window.addEventListener("beforeunload", onUnload);
    window.addEventListener("popstate", onPop);
    document.addEventListener("click", onClick, true);
    return () => {
      activeGuards--;
      stop();
      // Saved (or unmounted) while still on the guard entry: drop it, so the next Back
      // isn't spent on a duplicate of this page. After a link navigation the current
      // entry is the new page's, so this never fires there.
      if (window.history.state?.[GUARD]) window.history.back();
    };
  }, [active]);
}