Skip to content

Commit e7a9244

Browse files
committed
feat: Open prevent page leave modal on browser back button navigation
Implemented a workaround to show the "prevent leave" modal when users click the browser back button with unsaved changes. This solution addresses a limitation in Next.js App Router which doesn't provide built-in hooks for intercepting browser history navigation events. Technical details: - Creates a duplicate history entry as a buffer to prevent immediate back navigation, allowing the prevent leave modal to be displayed. - Uses history.go(-2) to properly navigate when user confirms leaving - Re-establishes buffer state when user cancels navigation Note: This implementation specifically targets the browser back button use case, which covers the vast majority of user navigation patterns. Forward button navigation is not supported by this workaround, but I think this is an acceptable trade-off considering the complexity of fully emulating the browser's history behavior within a Next.js application.
1 parent e6aad5a commit e7a9244

File tree

1 file changed

+71
-1
lines changed
  • packages/ui/src/elements/LeaveWithoutSaving

1 file changed

+71
-1
lines changed

Diff for: packages/ui/src/elements/LeaveWithoutSaving/index.tsx

+71-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import React, { useCallback } from 'react'
2+
import React, { useCallback, useEffect } from 'react'
33

44
import type { OnCancel } from '../ConfirmationModal/index.js'
55

@@ -12,34 +12,104 @@ import { usePreventLeave } from './usePreventLeave.js'
1212

1313
const modalSlug = 'leave-without-saving'
1414

15+
// Workaround to show the prevent leave page modal
16+
// when clicking the browser navigation button.
17+
// Note that this hook assumes only back button being used
18+
// and not forward button. Which is the case for most of the time.
19+
const useOnBrowserHistoryChange = ({
20+
hasAcceptedLeave,
21+
hasCancelledLeave,
22+
onLeave,
23+
onPrevent,
24+
onStay,
25+
shouldPreventLeave,
26+
}: {
27+
hasAcceptedLeave: boolean
28+
hasCancelledLeave: boolean
29+
onLeave: () => void
30+
onPrevent: () => void
31+
onStay: () => void
32+
shouldPreventLeave: boolean
33+
}) => {
34+
// Push a duplicated history entry to the stack
35+
// so that when the popstate event is triggered
36+
// it doesn't immediately navigate away.
37+
// This creates a fake visual and we can show the
38+
// prevent leave page modal in the meantime
39+
useEffect(() => {
40+
window.history.pushState(null, '', window.location.href)
41+
}, [])
42+
43+
const handlePopState = useCallback(() => {
44+
if (shouldPreventLeave) {
45+
onPrevent()
46+
} else {
47+
window.history.go(-2) // -2 because we pushed a duplicate entry
48+
}
49+
}, [onPrevent, shouldPreventLeave])
50+
51+
useEffect(() => {
52+
window.addEventListener('popstate', handlePopState)
53+
54+
return () => {
55+
window.removeEventListener('popstate', handlePopState)
56+
}
57+
}, [handlePopState])
58+
59+
useEffect(() => {
60+
if (hasAcceptedLeave) {
61+
onLeave()
62+
window.history.go(-2)
63+
} else if (hasCancelledLeave) {
64+
onStay()
65+
window.history.pushState(null, '', window.location.href) // Push a duplicate entry to the stack
66+
}
67+
}, [hasAcceptedLeave, hasCancelledLeave, onLeave, onStay])
68+
}
69+
1570
export const LeaveWithoutSaving: React.FC = () => {
1671
const { closeModal, openModal } = useModal()
1772
const modified = useFormModified()
1873
const { isValid } = useForm()
1974
const { user } = useAuth()
2075
const [hasAccepted, setHasAccepted] = React.useState(false)
76+
const [hasCancelled, setHasCancelled] = React.useState(false)
2177
const { t } = useTranslation()
2278

2379
const prevent = Boolean((modified || !isValid) && user)
2480

2581
const onPrevent = useCallback(() => {
2682
openModal(modalSlug)
83+
// Reset the modal confirm and cancel state
84+
setHasAccepted(false)
85+
setHasCancelled(false)
2786
}, [openModal])
2887

2988
const handleAccept = useCallback(() => {
3089
closeModal(modalSlug)
90+
setHasAccepted(true)
3191
}, [closeModal])
3292

3393
usePreventLeave({ hasAccepted, onAccept: handleAccept, onPrevent, prevent })
3494

3595
const onCancel: OnCancel = useCallback(() => {
3696
closeModal(modalSlug)
97+
setHasCancelled(true)
3798
}, [closeModal])
3899

39100
const onConfirm = useCallback(() => {
40101
setHasAccepted(true)
41102
}, [])
42103

104+
useOnBrowserHistoryChange({
105+
hasAcceptedLeave: hasAccepted,
106+
hasCancelledLeave: hasCancelled,
107+
onLeave: handleAccept,
108+
onPrevent,
109+
onStay: onCancel,
110+
shouldPreventLeave: prevent,
111+
})
112+
43113
return (
44114
<ConfirmationModal
45115
body={t('general:changesNotSaved')}

0 commit comments

Comments
 (0)