import { useContext, useEffect, useState } from 'react'; import { UNSAFE_NavigationContext } from 'react-router-dom'; import ConfirmDialog from '../ui/confirm-dialog'; type Go = (...args: unknown[]) => void; interface GuardedHistory { push: Go; replace: Go; } /** The page part of the address the app shows now: "/seo/general". */ const currentPath = (): string => window.location.hash.replace(/^#/, '').split(/[?#]/)[0] || '/'; /** The page part of where a navigation goes, as react-router hands it over. */ const targetPath = (to: unknown): string => { if (typeof to === 'string') { return to.split(/[?#]/)[0]; } return (to as { pathname?: string } | null)?.pathname ?? ''; }; /** * Asks before unsaved changes are left behind: the browser's own prompt * when the tab is closed or reloaded, and a dialog when a link of the app * leads to another page. Moving between the sections of the same page * keeps the changes, so it passes. The app runs on a HashRouter, which has * no navigation blocker, so the router's history is wrapped while there * is something to lose. * * @param {boolean} when - Whether there are unsaved changes. * @returns {JSX.Element} The dialog, to render with the page. */ export const useLeaveGuard = (when: boolean): JSX.Element => { const { navigator } = useContext(UNSAFE_NavigationContext); const [leave, setLeave] = useState<(() => void) | null>(null); useEffect(() => { if (!when) { return undefined; } const warn = (event: BeforeUnloadEvent): void => { event.preventDefault(); // Older browsers show the prompt only when a value is set. event.returnValue = ''; }; window.addEventListener('beforeunload', warn); return () => window.removeEventListener('beforeunload', warn); }, [when]); useEffect(() => { if (!when) { return undefined; } const history = navigator as unknown as GuardedHistory; const { push, replace } = history; const guard = (go: Go): Go => (...args) => { const target = targetPath(args[0]); if (target === '' || target === currentPath()) { go.apply(history, args); return; } setLeave(() => () => go.apply(history, args)); }; history.push = guard(push); history.replace = guard(replace); return () => { history.push = push; history.replace = replace; }; }, [when, navigator]); return ( !next && setLeave(null)} title="Leave without saving?" description="The changes on this page are not saved yet. Leaving drops them." confirmLabel="Leave" cancelLabel="Stay" tone="destructive" onConfirm={() => { const go = leave; setLeave(null); go?.(); }} /> ); };