All files / src/utils/hooks useUnsavedChangesWarning.js

76.92% Statements 10/13
100% Branches 4/4
80% Functions 4/5
72.72% Lines 8/11

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                                                        5x   3x       5x 5x 3x           3x 3x     5x        
import { useEffect } from "react";
import { useBlocker } from "react-router-dom";
 
/**
 * Warn the user before they navigate away from a form with unsaved changes.
 *
 * Covers both navigation kinds:
 *  - In-app navigation incl. the browser BACK button — via React Router's
 *    `useBlocker` (requires a data router / RouterProvider, which this app uses).
 *    A blocked navigation surfaces via the returned blocker so the caller can
 *    render a confirm dialog (Stay / Leave).
 *  - Hard navigation (refresh, tab close, address-bar) — via the native
 *    `beforeunload` prompt.
 *
 * Implements EI-3115 / EI-TC-4562: "creating an entry and clicking Back" must
 * prompt before discarding unsaved data.
 *
 * LIMITATION: React Router supports only ONE active blocker at a time. Don't
 * mount two guarded components simultaneously (e.g. a guarded modal inside a
 * guarded page) — the later blocker silently wins. Today's guarded forms live
 * on separate routes, so only one is ever mounted.
 *
 * @param {boolean} when  True when the form is dirty (has unsaved changes).
 * @returns the React Router blocker (state: "blocked" | "unblocked" | "proceeding",
 *          with .proceed() and .reset()) — pass it to <UnsavedChangesDialog/>.
 */
export function useUnsavedChangesWarning(when) {
    // Block in-app + back-button navigations to a DIFFERENT path while dirty.
    const blocker = useBlocker(
        ({ currentLocation, nextLocation }) =>
            Boolean(when) && currentLocation.pathname !== nextLocation.pathname
    );
 
    // Native guard for refresh / tab-close / external navigation.
    useEffect(() => {
        if (!when) return undefined;
        const handleBeforeUnload = (event) => {
            event.preventDefault();
            // Chrome requires returnValue to be set to trigger the prompt.
            event.returnValue = "";
            return "";
        };
        window.addEventListener("beforeunload", handleBeforeUnload);
        return () => window.removeEventListener("beforeunload", handleBeforeUnload);
    }, [when]);
 
    return blocker;
}
 
export default useUnsavedChangesWarning;