Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { useCallback, useEffect, useRef } from "react";
import { useHistory } from "react-router";
 
/**
 * @param continueCallback - callback to call when the user wants to leave the page
 * @param cancelCallback - callback to call when the user wants to stay on the page
 */
export type LeaveBlockerCallbacks = {
  continueCallback?: () => void;
  cancelCallback?: () => void;
};
 
/**
 * @param active - should the blocker be active or not. Set false to disable the blocker
 * @param onBeforeBlock - callback to check if we should block the page. If there is a need for a predicate to block the page
 * @param onBlock - callback to call when we should block the page. It Allows using custom modals to ask the user if they want to leave the page
 */
export type LeaveBlockerProps = {
  active: boolean;
  onBeforeBlock?: () => boolean;
  onBlock?: (callbacks: LeaveBlockerCallbacks) => void;
};
 
// Use `data-leave` attribute to mark the button that should be used to leave the current view (without changing url) to be able to block this action
const LEAVE_BUTTON_SELECTOR = "[data-leave]";
export const LEAVE_BLOCKER_KEY: string = "LEAVE_BLOCKER";
 
type LeaveBlockerCallback = {
  current?: (shouldLeave: boolean) => void;
};
// This is used to avoid problems with blocking the page API in react-router v5
// Callback is stored in a ref and called when the user decides to leave the page (this will unblock history.block for the current transition)
export const leaveBlockerCallback: LeaveBlockerCallback = {
  current: undefined,
};
/**
 * Block leaving the page if there is a reason to do so.
 * It includes
 * - blocking the action of a tab/window closing,
 * - blocking going through the browser history,
 * - blocking clicking on the button with `data-leave` attribute, which is supposed to lead to leave the current view
 */
export const LeaveBlocker = ({ active = true, onBeforeBlock, onBlock }: LeaveBlockerProps) => {
  // This will make active value available in the callbacks without the need to update the callback every time the active value changes
  const isActive = useRef(active);
  isActive.current = active;
  const history = useHistory();
  // This is a way to block the page on a tab/window closing
  // It will be done with browser standard API and confirm dialog
  const beforeUnloadHandler = useCallback(
    (e: BeforeUnloadEvent) => {
      if (!isActive.current) return;
      const shouldBlock = onBeforeBlock ? onBeforeBlock() : true;
      if (!shouldBlock) return true;
      e.preventDefault();
      e.returnValue = false;
      return false;
    },
    [onBeforeBlock],
  );
  const shouldSkipClickChecks = useRef(false);
  // This is a way to block the view (but not a page) change by clicking on the button
  // It obligates us to use `data-leave` attribute on the button that should be used to leave the current view
  const beforeLeaveClickHandler = useCallback(
    (e: MouseEvent) => {
      if (!isActive.current) return;
      // It allows to skip the check if the user chooses to leave the page
      if (shouldSkipClickChecks.current) return;
      const eventTarget = e.target as HTMLElement;
      const target = eventTarget?.matches?.(LEAVE_BUTTON_SELECTOR)
        ? e.target
        : eventTarget?.closest(LEAVE_BUTTON_SELECTOR);
 
      if (target) {
        const shouldBlock = onBeforeBlock ? onBeforeBlock() : true;
        if (!shouldBlock) return;
        e.preventDefault();
        e.stopPropagation();
        if (onBlock) {
          onBlock({
            continueCallback() {
              shouldSkipClickChecks.current = true;
              eventTarget.click();
              shouldSkipClickChecks.current = false;
            },
          });
        }
        return false;
      }
    },
    [onBeforeBlock, onBlock],
  );
 
  useEffect(() => {
    let unsubcribe: Function | null = null;
 
    window.addEventListener("beforeunload", beforeUnloadHandler);
    window.addEventListener("click", beforeLeaveClickHandler, { capture: true });
    unsubcribe = history.block(() => {
      if (!isActive.current) return;
      const shouldBlock = onBeforeBlock ? onBeforeBlock() : true;
      if (!shouldBlock) {
        return;
      }
 
      onBlock?.({
        continueCallback: () => {
          leaveBlockerCallback.current?.(true);
          leaveBlockerCallback.current = undefined;
          unsubcribe?.();
        },
        cancelCallback: () => {
          leaveBlockerCallback.current?.(false);
          leaveBlockerCallback.current = undefined;
        },
      });
      // workaround for react-router v5
      // see `getUserConfirmation` on the history object
      return LEAVE_BLOCKER_KEY;
    });
    return () => {
      window.removeEventListener("beforeunload", beforeUnloadHandler);
      window.removeEventListener("click", beforeLeaveClickHandler, { capture: true });
      if (unsubcribe) unsubcribe();
    };
  }, [onBeforeBlock, onBlock, beforeUnloadHandler, beforeLeaveClickHandler]);
  return null;
};