Skip to content

Commit 366f592

Browse files
authored
WEB-147 Fix forward/backward navigation guard (#130)
2 parents 8d76a9a + eda4038 commit 366f592

3 files changed

Lines changed: 35 additions & 40 deletions

File tree

src/app/editor/[id]/[slug]/useUnsavedChangesGuard.ts

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
11
"use client";
22

3-
import { useCallback, useEffect, useRef } from "react";
3+
import { useCallback, useEffect } from "react";
44
import { useDialogs } from "@/components/ui/dialog-provider";
55

66
const LEAVE_MESSAGE = "You have unsaved changes. Leave this page without saving?";
77

8-
// Next.js internally sets an auto-incrementing `idx` on history.state to track
9-
// navigation position. This is an undocumented internal — not a public API — so
10-
// it may disappear or change in any Next.js release. We use it to determine
11-
// whether the user navigated forward or back so we can revert the navigation
12-
// when they cancel. If `idx` is absent we degrade gracefully (warn, but can't
13-
// revert). See: https://github.com/vercel/next.js/discussions/34980
14-
function getCurrentHistoryIndex() {
15-
const historyState = window.history.state as { idx?: number } | null;
16-
return typeof historyState?.idx === "number" ? historyState.idx : null;
17-
}
18-
198
export function useUnsavedChangesGuard(isDirty: boolean) {
209
const { confirm } = useDialogs();
21-
const historyIndexRef = useRef<number | null>(null);
22-
const isRevertingNavigationRef = useRef(false);
2310

2411
const confirmDiscardChanges = useCallback(async () => {
2512
if (!isDirty) {
@@ -38,47 +25,34 @@ export function useUnsavedChangesGuard(isDirty: boolean) {
3825
return;
3926
}
4027

41-
historyIndexRef.current = getCurrentHistoryIndex();
42-
isRevertingNavigationRef.current = false;
43-
4428
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
4529
event.preventDefault();
4630
// Needed for Chrome version <=119 to show the confirmation dialog
4731
event.returnValue = true;
4832
};
4933

50-
const handlePopState = () => {
51-
const nextHistoryIndex = getCurrentHistoryIndex();
52-
53-
if (!isRevertingNavigationRef.current) {
54-
const shouldLeave = window.confirm(LEAVE_MESSAGE);
34+
const handleHistoryNavigate = (event: NavigateEvent) => {
35+
if (event.navigationType !== "traverse" || !event.cancelable || event.hashChange) {
36+
return;
37+
}
5538

56-
if (!shouldLeave) {
57-
if (
58-
historyIndexRef.current !== null &&
59-
nextHistoryIndex !== null &&
60-
historyIndexRef.current !== nextHistoryIndex
61-
) {
62-
isRevertingNavigationRef.current = true;
63-
window.history.go(historyIndexRef.current > nextHistoryIndex ? 1 : -1);
64-
return;
65-
}
39+
const destination = new URL(event.destination.url);
6640

67-
console.warn("Unable to determine history direction for unsaved-changes guard");
68-
}
41+
if (destination.origin !== window.location.origin) {
42+
return;
6943
}
7044

71-
isRevertingNavigationRef.current = false;
72-
historyIndexRef.current = nextHistoryIndex;
45+
if (!window.confirm(LEAVE_MESSAGE)) {
46+
event.preventDefault();
47+
}
7348
};
7449

7550
window.addEventListener("beforeunload", handleBeforeUnload);
76-
window.addEventListener("popstate", handlePopState);
51+
window.navigation?.addEventListener("navigate", handleHistoryNavigate);
7752

7853
return () => {
79-
isRevertingNavigationRef.current = false;
8054
window.removeEventListener("beforeunload", handleBeforeUnload);
81-
window.removeEventListener("popstate", handlePopState);
55+
window.navigation?.removeEventListener("navigate", handleHistoryNavigate);
8256
};
8357
}, [isDirty]);
8458

src/types/navigation.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
declare global {
2+
interface Window {
3+
navigation?: Navigation;
4+
}
5+
6+
interface Navigation extends EventTarget {
7+
addEventListener(type: "navigate", listener: (event: NavigateEvent) => void): void;
8+
removeEventListener(type: "navigate", listener: (event: NavigateEvent) => void): void;
9+
}
10+
11+
interface NavigateEvent extends Event {
12+
readonly cancelable: boolean;
13+
readonly destination: {
14+
readonly url: string;
15+
};
16+
readonly hashChange: boolean;
17+
readonly navigationType: string;
18+
}
19+
}
20+
21+
export {};

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
"paths": { "@/*": ["./src/*"] },
66
"plugins": [{ "name": "next" }]
77
},
8-
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", ".next/types/**/*.ts"],
8+
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", ".next/types/**/*.ts"],
99
"exclude": ["node_modules"]
1010
}

0 commit comments

Comments
 (0)