@okyrychenko-dev/react-action-guard-router 0.2.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from '../types-C3CPuZip.cjs';
1
+ import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from '../types-xxRwsJ1N.cjs';
2
2
 
3
3
  /**
4
4
  * Options for React Router navigation blocker
@@ -26,97 +26,11 @@ type UsePromptOptions = Omit<BaseNavigationBlockerOptions, "message" | "when" |
26
26
 
27
27
  /**
28
28
  * Blocks navigation in React Router v6+ applications based on conditions or scope state.
29
- *
30
- * This hook integrates React Router's `useBlocker` with react-action-guard's scope system,
31
- * allowing you to prevent navigation when certain conditions are met or when specific
32
- * scopes are blocked.
33
- *
34
- * **Features:**
35
- * - Block navigation based on boolean condition or function
36
- * - Automatically block when react-action-guard scopes are active
37
- * - Support for browser `beforeunload` event (tab close/refresh)
38
- * - Customizable confirmation dialog configuration
39
- * - Callbacks for block/allow events
40
- *
41
- * @param options - Configuration options
42
- * @returns Object with `isBlocking` state
43
- *
44
- * @example
45
- * ```tsx
46
- * // Block based on scope
47
- * function EditForm() {
48
- * useNavigationBlocker({
49
- * scope: 'form',
50
- * message: 'You have unsaved changes. Leave anyway?',
51
- * });
52
- *
53
- * return <form>...</form>;
54
- * }
55
- * ```
56
- *
57
- * @example
58
- * ```tsx
59
- * // Block based on condition
60
- * function UnsavedChanges() {
61
- * const [hasChanges, setHasChanges] = useState(false);
62
- *
63
- * useNavigationBlocker({
64
- * when: hasChanges,
65
- * message: 'Discard changes?',
66
- * onBlock: () => console.log('Navigation blocked'),
67
- * });
68
- * }
69
- * ```
70
- *
71
- * @example
72
- * ```tsx
73
- * // Block with custom dialog
74
- * function CheckoutFlow() {
75
- * useNavigationBlocker({
76
- * scope: 'checkout',
77
- * message: 'Your items will still be in your cart.',
78
- * blockBrowserUnload: true,
79
- * });
80
- * }
81
- * ```
82
29
  */
83
30
  declare function useNavigationBlocker(options: UseNavigationBlockerOptions): NavigationBlockerReturn;
84
31
 
85
32
  /**
86
33
  * Simple prompt API similar to React Router v5's `usePrompt`.
87
- *
88
- * Shows a confirmation dialog when the user tries to navigate away
89
- * while the condition is true.
90
- *
91
- * This is a simpler API compared to `useNavigationBlocker` for cases
92
- * where you just want to show a prompt without advanced configuration.
93
- *
94
- * @param message - Message to show in confirmation dialog
95
- * @param when - Condition to determine when to show the prompt
96
- *
97
- * @example
98
- * ```tsx
99
- * function MyForm() {
100
- * const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
101
- *
102
- * usePrompt(
103
- * 'You have unsaved changes. Are you sure you want to leave?',
104
- * hasUnsavedChanges
105
- * );
106
- *
107
- * return <form>...</form>;
108
- * }
109
- * ```
110
- *
111
- * @example
112
- * ```tsx
113
- * // With function condition
114
- * function EditPage() {
115
- * const isDirty = useFormDirty();
116
- *
117
- * usePrompt('Discard unsaved changes?', () => isDirty);
118
- * }
119
- * ```
120
34
  */
121
35
  declare function usePrompt(message: string, when: boolean | (() => boolean)): void;
122
36
  /**
@@ -1,4 +1,4 @@
1
- import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from '../types-C3CPuZip.js';
1
+ import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from '../types-xxRwsJ1N.js';
2
2
 
3
3
  /**
4
4
  * Options for React Router navigation blocker
@@ -26,97 +26,11 @@ type UsePromptOptions = Omit<BaseNavigationBlockerOptions, "message" | "when" |
26
26
 
27
27
  /**
28
28
  * Blocks navigation in React Router v6+ applications based on conditions or scope state.
29
- *
30
- * This hook integrates React Router's `useBlocker` with react-action-guard's scope system,
31
- * allowing you to prevent navigation when certain conditions are met or when specific
32
- * scopes are blocked.
33
- *
34
- * **Features:**
35
- * - Block navigation based on boolean condition or function
36
- * - Automatically block when react-action-guard scopes are active
37
- * - Support for browser `beforeunload` event (tab close/refresh)
38
- * - Customizable confirmation dialog configuration
39
- * - Callbacks for block/allow events
40
- *
41
- * @param options - Configuration options
42
- * @returns Object with `isBlocking` state
43
- *
44
- * @example
45
- * ```tsx
46
- * // Block based on scope
47
- * function EditForm() {
48
- * useNavigationBlocker({
49
- * scope: 'form',
50
- * message: 'You have unsaved changes. Leave anyway?',
51
- * });
52
- *
53
- * return <form>...</form>;
54
- * }
55
- * ```
56
- *
57
- * @example
58
- * ```tsx
59
- * // Block based on condition
60
- * function UnsavedChanges() {
61
- * const [hasChanges, setHasChanges] = useState(false);
62
- *
63
- * useNavigationBlocker({
64
- * when: hasChanges,
65
- * message: 'Discard changes?',
66
- * onBlock: () => console.log('Navigation blocked'),
67
- * });
68
- * }
69
- * ```
70
- *
71
- * @example
72
- * ```tsx
73
- * // Block with custom dialog
74
- * function CheckoutFlow() {
75
- * useNavigationBlocker({
76
- * scope: 'checkout',
77
- * message: 'Your items will still be in your cart.',
78
- * blockBrowserUnload: true,
79
- * });
80
- * }
81
- * ```
82
29
  */
83
30
  declare function useNavigationBlocker(options: UseNavigationBlockerOptions): NavigationBlockerReturn;
84
31
 
85
32
  /**
86
33
  * Simple prompt API similar to React Router v5's `usePrompt`.
87
- *
88
- * Shows a confirmation dialog when the user tries to navigate away
89
- * while the condition is true.
90
- *
91
- * This is a simpler API compared to `useNavigationBlocker` for cases
92
- * where you just want to show a prompt without advanced configuration.
93
- *
94
- * @param message - Message to show in confirmation dialog
95
- * @param when - Condition to determine when to show the prompt
96
- *
97
- * @example
98
- * ```tsx
99
- * function MyForm() {
100
- * const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
101
- *
102
- * usePrompt(
103
- * 'You have unsaved changes. Are you sure you want to leave?',
104
- * hasUnsavedChanges
105
- * );
106
- *
107
- * return <form>...</form>;
108
- * }
109
- * ```
110
- *
111
- * @example
112
- * ```tsx
113
- * // With function condition
114
- * function EditPage() {
115
- * const isDirty = useFormDirty();
116
- *
117
- * usePrompt('Discard unsaved changes?', () => isDirty);
118
- * }
119
- * ```
120
34
  */
121
35
  declare function usePrompt(message: string, when: boolean | (() => boolean)): void;
122
36
  /**
@@ -1,19 +1,18 @@
1
- import { useRef, useState, useCallback, useEffect, useMemo } from 'react';
1
+ import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2
2
  import { useBlocker } from 'react-router-dom';
3
+ import { isDefined, isArray, isFunction, isObject } from '@okyrychenko-dev/type-utils';
3
4
  import { useIsBlocked } from '@okyrychenko-dev/react-action-guard';
4
5
 
5
6
  // src/react-router/useNavigationBlocker.ts
6
-
7
- // src/core/utils.ts
8
7
  function resolveCondition(condition) {
9
- return typeof condition === "function" ? condition() : condition;
8
+ return isFunction(condition) ? condition() : condition;
10
9
  }
11
- var hasThen = (value) => typeof value === "object" && value !== null && "then" in value;
10
+ var hasThen = (value) => isObject(value) && "then" in value;
12
11
  function isThenable(value) {
13
12
  if (!hasThen(value)) {
14
13
  return false;
15
14
  }
16
- return typeof value.then === "function";
15
+ return isFunction(value.then);
17
16
  }
18
17
  function resolveConfirmResult(message, onConfirm, fallbackConfirm) {
19
18
  if (!onConfirm) {
@@ -25,14 +24,14 @@ function resolveConfirmResult(message, onConfirm, fallbackConfirm) {
25
24
  }
26
25
  return { kind: "sync", confirmed: result };
27
26
  }
28
- function isDefined(value) {
29
- return value !== void 0 && value !== null;
30
- }
31
27
  function normalizeScope(scope) {
32
28
  if (!scope) {
33
29
  return [];
34
30
  }
35
- return Array.isArray(scope) ? scope : [scope];
31
+ return isArray(scope) ? scope : [scope];
32
+ }
33
+ function createScopeKey(scope) {
34
+ return JSON.stringify([...new Set(normalizeScope(scope))].sort());
36
35
  }
37
36
 
38
37
  // src/core/useBeforeUnload.ts
@@ -59,7 +58,6 @@ function useShouldBlock(when, scope) {
59
58
  const scopes = useMemo(() => normalizeScope(scope), [scope]);
60
59
  const isScopeBlocked = useIsBlocked(scopes.length === 1 ? scopes[0] : scopes);
61
60
  const warnedMissingConditionRef = useRef(false);
62
- const warnedFunctionRef = useRef(false);
63
61
  useEffect(() => {
64
62
  if (process.env.NODE_ENV !== "development") {
65
63
  return;
@@ -70,24 +68,29 @@ function useShouldBlock(when, scope) {
70
68
  '[react-action-guard-router] useShouldBlock: Neither "when" nor "scope" provided. This hook will always return false. Please provide at least one condition.'
71
69
  );
72
70
  }
73
- if (!warnedFunctionRef.current && typeof when === "function") {
74
- warnedFunctionRef.current = true;
75
- console.warn(
76
- '[react-action-guard-router] useShouldBlock: "when" is a function. Make sure to wrap it with useCallback to avoid unnecessary re-computations.'
77
- );
78
- }
79
71
  }, [when, scope]);
80
- const shouldBlock = useMemo(() => {
81
- let blockCondition = false;
82
- if (isDefined(when)) {
83
- blockCondition = resolveCondition(when);
84
- }
85
- if (scope && isScopeBlocked) {
86
- blockCondition = true;
72
+ const blockCondition = isDefined(when) && resolveCondition(when);
73
+ return blockCondition || Boolean(scope && isScopeBlocked);
74
+ }
75
+
76
+ // src/core/confirmationOwner.utils.ts
77
+ function createConfirmationOwner() {
78
+ let currentAttempt = 0;
79
+ function invalidate() {
80
+ currentAttempt += 1;
81
+ }
82
+ function begin() {
83
+ const attempt = ++currentAttempt;
84
+ function settle() {
85
+ if (attempt !== currentAttempt) {
86
+ return false;
87
+ }
88
+ invalidate();
89
+ return true;
87
90
  }
88
- return blockCondition;
89
- }, [when, scope, isScopeBlocked]);
90
- return shouldBlock;
91
+ return settle;
92
+ }
93
+ return { begin, invalidate };
91
94
  }
92
95
 
93
96
  // src/react-router/useNavigationBlocker.ts
@@ -104,16 +107,24 @@ function useNavigationBlocker(options) {
104
107
  onAllow,
105
108
  onConfirm
106
109
  } = options;
107
- const confirmSeqRef = useRef(0);
108
- const [pendingConfirm, setPendingConfirm] = useState(null);
110
+ const [confirmationOwner] = useState(createConfirmationOwner);
111
+ const { begin, invalidate } = confirmationOwner;
112
+ const [blockedAttempt, setBlockedAttempt] = useState(null);
109
113
  const shouldBlock = useShouldBlock(when ?? block, scope);
114
+ const scopeKey = createScopeKey(scope);
115
+ useEffect(() => {
116
+ return invalidate;
117
+ }, [invalidate]);
110
118
  const blocker = useBlocker(
111
119
  useCallback(() => {
120
+ const settle = begin();
112
121
  if (!shouldBlock) {
122
+ setBlockedAttempt(null);
113
123
  return false;
114
124
  }
115
125
  onBlock?.();
116
126
  if (!message) {
127
+ setBlockedAttempt({ kind: "denied", settle, scopeKey, message });
117
128
  return true;
118
129
  }
119
130
  const confirmation = resolveConfirmResult(
@@ -122,46 +133,58 @@ function useNavigationBlocker(options) {
122
133
  (value) => window.confirm(value)
123
134
  );
124
135
  if (confirmation.kind === "async") {
125
- const id = ++confirmSeqRef.current;
126
- setPendingConfirm({ id, promise: confirmation.promise });
136
+ setBlockedAttempt({
137
+ kind: "confirming",
138
+ settle,
139
+ promise: confirmation.promise.catch(() => false),
140
+ scopeKey,
141
+ message
142
+ });
127
143
  return true;
128
144
  }
129
- if (confirmation.confirmed) {
145
+ if (confirmation.confirmed && settle()) {
146
+ setBlockedAttempt(null);
130
147
  onAllow?.();
131
148
  return false;
132
149
  }
150
+ setBlockedAttempt({ kind: "denied", settle, scopeKey, message });
133
151
  return true;
134
- }, [shouldBlock, message, onBlock, onConfirm, onAllow])
152
+ }, [shouldBlock, scopeKey, message, onBlock, onConfirm, onAllow, begin])
135
153
  );
136
154
  useEffect(() => {
137
- if (!pendingConfirm) {
155
+ if (!blockedAttempt) {
156
+ return;
157
+ }
158
+ if (!shouldBlock || blockedAttempt.scopeKey !== scopeKey || blockedAttempt.message !== message) {
159
+ queueMicrotask(() => {
160
+ setBlockedAttempt((current) => current === blockedAttempt ? null : current);
161
+ });
162
+ if (blockedAttempt.settle()) {
163
+ blocker.reset?.();
164
+ }
165
+ return;
166
+ }
167
+ if (blockedAttempt.kind === "denied") {
138
168
  return;
139
169
  }
140
170
  let active = true;
141
- const { id, promise } = pendingConfirm;
142
- promise.then((confirmed) => {
143
- if (!active || id !== confirmSeqRef.current) {
171
+ const { settle, promise } = blockedAttempt;
172
+ void promise.then((confirmed) => {
173
+ if (!active || !settle()) {
144
174
  return;
145
175
  }
176
+ setBlockedAttempt(null);
146
177
  if (confirmed) {
147
178
  onAllow?.();
148
179
  blocker.proceed?.();
149
180
  } else {
150
181
  blocker.reset?.();
151
182
  }
152
- }).catch(() => {
153
- if (active && id === confirmSeqRef.current) {
154
- blocker.reset?.();
155
- }
156
- }).finally(() => {
157
- if (active && id === confirmSeqRef.current) {
158
- setPendingConfirm(null);
159
- }
160
183
  });
161
184
  return () => {
162
185
  active = false;
163
186
  };
164
- }, [pendingConfirm, blocker, onAllow]);
187
+ }, [blockedAttempt, blocker, onAllow, shouldBlock, scopeKey, message]);
165
188
  useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);
166
189
  return {
167
190
  isBlocking: shouldBlock,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/react-router/useNavigationBlocker.ts","../../src/react-router/usePrompt.ts"],"names":["useEffect","useRef"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Resolves a condition value (boolean or function returning boolean)\n *\n * @param condition - Boolean value or function\n * @returns Resolved boolean value\n */\nexport function resolveCondition(condition: boolean | (() => boolean)): boolean {\n return typeof condition === \"function\" ? condition() : condition;\n}\n\ninterface Thenable {\n then: (...args: Array<unknown>) => unknown;\n}\n\nconst hasThen = (value: unknown): value is Thenable =>\n typeof value === \"object\" && value !== null && \"then\" in value;\n\n/**\n * Checks if a value behaves like a Promise (thenable)\n */\nexport function isThenable<T>(value: unknown): value is PromiseLike<T> {\n if (!hasThen(value)) {\n return false;\n }\n return typeof value.then === \"function\";\n}\n\nexport interface ConfirmResultSync {\n kind: \"sync\";\n confirmed: boolean;\n}\n\nexport interface ConfirmResultAsync {\n kind: \"async\";\n promise: Promise<boolean>;\n}\n\nexport type ConfirmResult = ConfirmResultSync | ConfirmResultAsync;\n\n/**\n * Normalizes custom confirm result into sync or async branch.\n */\nexport function resolveConfirmResult(\n message: string,\n onConfirm: ((message: string) => boolean | PromiseLike<boolean>) | undefined,\n fallbackConfirm: (message: string) => boolean\n): ConfirmResult {\n if (!onConfirm) {\n return { kind: \"sync\", confirmed: fallbackConfirm(message) };\n }\n\n const result = onConfirm(message);\n if (isThenable<boolean>(result)) {\n return { kind: \"async\", promise: Promise.resolve(result) };\n }\n\n return { kind: \"sync\", confirmed: result };\n}\n\n/**\n * Generates a unique blocker ID based on scope and timestamp\n *\n * @param prefix - Prefix for the ID\n * @param scope - Optional scope\n * @returns Unique blocker ID\n */\nexport function createBlockerId(prefix: string, scope?: string | Array<string>): string {\n const scopePart = scope ? `-${Array.isArray(scope) ? scope.join(\"-\") : scope}` : \"\";\n const timestamp = String(Date.now());\n return `${prefix}${scopePart}-${timestamp}`;\n}\n\n/**\n * Checks if a value is defined and not empty\n *\n * @param value - Value to check\n * @returns True if value is defined and not empty\n */\nexport function isDefined<T>(value: T | undefined | null): value is T {\n return value !== undefined && value !== null;\n}\n\n/**\n * Normalizes scope to an array\n *\n * @param scope - String or array of strings\n * @returns Array of scope strings\n */\nexport function normalizeScope(scope: string | Array<string> | undefined): Array<string> {\n if (!scope) {\n return [];\n }\n return Array.isArray(scope) ? scope : [scope];\n}\n","import { useEffect } from \"react\";\nimport { resolveCondition } from \"./utils\";\n\n/**\n * Default message shown when preventing browser unload\n */\nexport const DEFAULT_UNLOAD_MESSAGE = \"Changes you made may not be saved.\";\n\n/**\n * Blocks browser tab close/refresh when condition is met.\n *\n * This hook adds a `beforeunload` event listener to warn users\n * before they close or refresh the browser tab. Modern browsers\n * may ignore custom messages and show their own generic warning.\n *\n * @param when - Boolean or function returning boolean to determine if blocking should be active\n * @param message - Optional custom message (may be ignored by browsers)\n *\n * @example\n * ```tsx\n * function MyForm() {\n * const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n *\n * useBeforeUnload(hasUnsavedChanges, 'You have unsaved changes');\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With function condition\n * function EditPage() {\n * const isFormDirty = useIsBlocked('form');\n *\n * useBeforeUnload(() => isFormDirty);\n *\n * return <div>...</div>;\n * }\n * ```\n */\nexport function useBeforeUnload(\n when: boolean | (() => boolean),\n message = DEFAULT_UNLOAD_MESSAGE\n): void {\n useEffect(() => {\n const shouldBlock = resolveCondition(when);\n\n if (!shouldBlock) {\n return;\n }\n\n const handleBeforeUnload = (event: BeforeUnloadEvent): void => {\n // Prevent default to trigger the browser's confirmation dialog\n event.preventDefault();\n\n // Fallback for legacy browsers/WebViews that still rely on returnValue.\n if (\"returnValue\" in event) {\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n event.returnValue = message;\n }\n };\n\n window.addEventListener(\"beforeunload\", handleBeforeUnload);\n\n return () => {\n window.removeEventListener(\"beforeunload\", handleBeforeUnload);\n };\n }, [when, message]);\n}\n","import { useIsBlocked } from \"@okyrychenko-dev/react-action-guard\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { isDefined, normalizeScope, resolveCondition } from \"./utils\";\n\n/**\n * Shared hook to determine if blocking should be active based on\n * condition and/or scope state.\n *\n * This hook uses `useMemo` for optimal performance, ensuring blocking state\n * is computed synchronously without additional re-renders.\n *\n * @param when - Optional condition (boolean or function)\n * @param scope - Optional scope(s) to check\n * @returns Whether blocking should be active\n *\n * @internal\n */\nexport function useShouldBlock(\n when?: boolean | (() => boolean),\n scope?: string | Array<string>\n): boolean {\n // Memoize scope normalization to avoid recalculation on every render\n const scopes = useMemo(() => normalizeScope(scope), [scope]);\n\n // Check if any specified scopes are blocked\n const isScopeBlocked = useIsBlocked(scopes.length === 1 ? scopes[0] : scopes);\n\n // Development-only validation\n const warnedMissingConditionRef = useRef(false);\n const warnedFunctionRef = useRef(false);\n\n useEffect(() => {\n if (process.env.NODE_ENV !== \"development\") {\n return;\n }\n\n if (!warnedMissingConditionRef.current && !isDefined(when) && !scope) {\n warnedMissingConditionRef.current = true;\n console.warn(\n '[react-action-guard-router] useShouldBlock: Neither \"when\" nor \"scope\" provided. ' +\n \"This hook will always return false. Please provide at least one condition.\"\n );\n }\n\n if (!warnedFunctionRef.current && typeof when === \"function\") {\n warnedFunctionRef.current = true;\n console.warn(\n '[react-action-guard-router] useShouldBlock: \"when\" is a function. ' +\n \"Make sure to wrap it with useCallback to avoid unnecessary re-computations.\"\n );\n }\n }, [when, scope]);\n\n // Compute blocking state synchronously using useMemo\n // This avoids the useState + useEffect pattern which causes an extra render\n const shouldBlock = useMemo(() => {\n let blockCondition = false;\n\n // Check manual condition\n if (isDefined(when)) {\n blockCondition = resolveCondition(when);\n }\n\n // Check scope-based blocking (OR logic - block if either condition is true)\n if (scope && isScopeBlocked) {\n blockCondition = true;\n }\n\n return blockCondition;\n }, [when, scope, isScopeBlocked]);\n\n return shouldBlock;\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useBlocker } from \"react-router-dom\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\nimport type { UseNavigationBlockerOptions } from \"./types\";\n\n/**\n * Blocks navigation in React Router v6+ applications based on conditions or scope state.\n *\n * This hook integrates React Router's `useBlocker` with react-action-guard's scope system,\n * allowing you to prevent navigation when certain conditions are met or when specific\n * scopes are blocked.\n *\n * **Features:**\n * - Block navigation based on boolean condition or function\n * - Automatically block when react-action-guard scopes are active\n * - Support for browser `beforeunload` event (tab close/refresh)\n * - Customizable confirmation dialog configuration\n * - Callbacks for block/allow events\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // Block based on scope\n * function EditForm() {\n * useNavigationBlocker({\n * scope: 'form',\n * message: 'You have unsaved changes. Leave anyway?',\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Block based on condition\n * function UnsavedChanges() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\n * message: 'Discard changes?',\n * onBlock: () => console.log('Navigation blocked'),\n * });\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Block with custom dialog\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your items will still be in your cart.',\n * blockBrowserUnload: true,\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const {\n when,\n scope,\n message,\n blockBrowserUnload = true,\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n block, // deprecated, for backwards compatibility\n onBlock,\n onAllow,\n onConfirm,\n } = options;\n\n const confirmSeqRef = useRef(0);\n const [pendingConfirm, setPendingConfirm] = useState<{\n id: number;\n promise: Promise<boolean>;\n } | null>(null);\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when ?? block, scope);\n\n // Use React Router's blocker\n const blocker = useBlocker(\n useCallback(() => {\n // Early return if not blocking\n if (!shouldBlock) {\n return false;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n // If no message, just block\n if (!message) {\n return true;\n }\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"async\") {\n const id = ++confirmSeqRef.current;\n setPendingConfirm({ id, promise: confirmation.promise });\n return true;\n }\n\n if (confirmation.confirmed) {\n onAllow?.();\n return false;\n }\n\n return true;\n }, [shouldBlock, message, onBlock, onConfirm, onAllow])\n );\n\n useEffect(() => {\n if (!pendingConfirm) {\n return;\n }\n\n let active = true;\n const { id, promise } = pendingConfirm;\n\n promise\n .then((confirmed) => {\n if (!active || id !== confirmSeqRef.current) {\n return;\n }\n if (confirmed) {\n onAllow?.();\n blocker.proceed?.();\n } else {\n blocker.reset?.();\n }\n })\n .catch(() => {\n if (active && id === confirmSeqRef.current) {\n blocker.reset?.();\n }\n })\n .finally(() => {\n if (active && id === confirmSeqRef.current) {\n setPendingConfirm(null);\n }\n });\n\n return () => {\n active = false;\n };\n }, [pendingConfirm, blocker, onAllow]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n isIntercepting: blocker.state === \"blocked\",\n };\n}\n","import { useNavigationBlocker } from \"./useNavigationBlocker\";\nimport type { UsePromptOptions } from \"./types\";\n\n/**\n * Simple prompt API similar to React Router v5's `usePrompt`.\n *\n * Shows a confirmation dialog when the user tries to navigate away\n * while the condition is true.\n *\n * This is a simpler API compared to `useNavigationBlocker` for cases\n * where you just want to show a prompt without advanced configuration.\n *\n * @param message - Message to show in confirmation dialog\n * @param when - Condition to determine when to show the prompt\n *\n * @example\n * ```tsx\n * function MyForm() {\n * const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n *\n * usePrompt(\n * 'You have unsaved changes. Are you sure you want to leave?',\n * hasUnsavedChanges\n * );\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With function condition\n * function EditPage() {\n * const isDirty = useFormDirty();\n *\n * usePrompt('Discard unsaved changes?', () => isDirty);\n * }\n * ```\n */\nexport function usePrompt(message: string, when: boolean | (() => boolean)): void {\n useNavigationBlocker({\n when,\n message,\n blockBrowserUnload: true,\n });\n}\n\n/**\n * Hook variant that accepts options object\n *\n * @param options - Prompt configuration\n *\n * @example\n * ```tsx\n * usePromptWithOptions({\n * message: 'Leave without saving?',\n * when: hasChanges\n * });\n * ```\n */\nexport function usePromptWithOptions(options: UsePromptOptions): void {\n useNavigationBlocker({\n ...options,\n blockBrowserUnload: options.blockBrowserUnload ?? true,\n });\n}\n"]}
1
+ {"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/core/confirmationOwner.utils.ts","../../src/react-router/useNavigationBlocker.ts","../../src/react-router/usePrompt.ts"],"names":["useEffect","isDefined"],"mappings":"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","file":"index.js","sourcesContent":["import { isArray, isFunction, isObject } from \"@okyrychenko-dev/type-utils\";\nimport type { Optional } from \"@okyrychenko-dev/type-utils\";\n\n/**\n * Resolves a condition value (boolean or function returning boolean)\n *\n * @param condition - Boolean value or function\n * @returns Resolved boolean value\n */\nexport function resolveCondition(condition: boolean | (() => boolean)): boolean {\n return isFunction(condition) ? condition() : condition;\n}\n\ninterface Thenable {\n then: (...args: Array<unknown>) => unknown;\n}\n\nconst hasThen = (value: unknown): value is Thenable => isObject(value) && \"then\" in value;\n\n/**\n * Checks if a value behaves like a Promise (thenable)\n */\nexport function isThenable<T>(value: unknown): value is PromiseLike<T> {\n if (!hasThen(value)) {\n return false;\n }\n\n return isFunction(value.then);\n}\n\nexport interface ConfirmResultSync {\n kind: \"sync\";\n confirmed: boolean;\n}\n\nexport interface ConfirmResultAsync {\n kind: \"async\";\n promise: Promise<boolean>;\n}\n\nexport type ConfirmResult = ConfirmResultSync | ConfirmResultAsync;\n\n/**\n * Normalizes custom confirm result into sync or async branch.\n */\nexport function resolveConfirmResult(\n message: string,\n onConfirm: Optional<(message: string) => boolean | PromiseLike<boolean>>,\n fallbackConfirm: (message: string) => boolean\n): ConfirmResult {\n if (!onConfirm) {\n return { kind: \"sync\", confirmed: fallbackConfirm(message) };\n }\n\n const result = onConfirm(message);\n\n if (isThenable<boolean>(result)) {\n return { kind: \"async\", promise: Promise.resolve(result) };\n }\n\n return { kind: \"sync\", confirmed: result };\n}\n\n/**\n * Generates a unique blocker ID based on scope and timestamp\n *\n * @param prefix - Prefix for the ID\n * @param scope - Optional scope\n * @returns Unique blocker ID\n */\nexport function createBlockerId(prefix: string, scope?: string | Array<string>): string {\n const scopePart = scope ? `-${isArray(scope) ? scope.join(\"-\") : scope}` : \"\";\n const timestamp = String(Date.now());\n\n return `${prefix}${scopePart}-${timestamp}`;\n}\n\n/**\n * Checks if a value is defined and not empty\n *\n * @param value - Value to check\n * @returns True if value is defined and not empty\n */\nexport { isDefined } from \"@okyrychenko-dev/type-utils\";\n\n/**\n * Normalizes scope to an array\n *\n * @param scope - String or array of strings\n * @returns Array of scope strings\n */\nexport function normalizeScope(scope: Optional<string | Array<string>>): Array<string> {\n if (!scope) {\n return [];\n }\n\n return isArray(scope) ? scope : [scope];\n}\n\n/** Identifies protection independently of scope order and duplicate entries. */\nexport function createScopeKey(scope: Optional<string | Array<string>>): string {\n return JSON.stringify([...new Set(normalizeScope(scope))].sort());\n}\n","import { useEffect } from \"react\";\nimport { resolveCondition } from \"./utils\";\n\n/**\n * Default message shown when preventing browser unload\n */\nexport const DEFAULT_UNLOAD_MESSAGE = \"Changes you made may not be saved.\";\n\n/**\n * Blocks browser tab close/refresh when condition is met.\n */\nexport function useBeforeUnload(\n when: boolean | (() => boolean),\n message = DEFAULT_UNLOAD_MESSAGE\n): void {\n useEffect(() => {\n const shouldBlock = resolveCondition(when);\n\n if (!shouldBlock) {\n return;\n }\n\n const handleBeforeUnload = (event: BeforeUnloadEvent): void => {\n // Prevent default to trigger the browser's confirmation dialog\n event.preventDefault();\n\n // Fallback for legacy browsers/WebViews that still rely on returnValue.\n if (\"returnValue\" in event) {\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n event.returnValue = message;\n }\n };\n\n window.addEventListener(\"beforeunload\", handleBeforeUnload);\n\n return () => {\n window.removeEventListener(\"beforeunload\", handleBeforeUnload);\n };\n }, [when, message]);\n}\n","import { useIsBlocked } from \"@okyrychenko-dev/react-action-guard\";\nimport { isDefined } from \"@okyrychenko-dev/type-utils\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { normalizeScope, resolveCondition } from \"./utils\";\n\n/**\n * Shared hook to determine if blocking should be active based on\n * condition and/or scope state.\n *\n * Pure function conditions are evaluated synchronously on every render.\n * Mutating a ref alone does not trigger a render.\n *\n * @param when - Optional condition (boolean or function)\n * @param scope - Optional scope(s) to check\n * @returns Whether blocking should be active\n *\n * @internal\n */\nexport function useShouldBlock(\n when?: boolean | (() => boolean),\n scope?: string | Array<string>\n): boolean {\n // Memoize scope normalization to avoid recalculation on every render\n const scopes = useMemo(() => normalizeScope(scope), [scope]);\n\n // Check if any specified scopes are blocked\n const isScopeBlocked = useIsBlocked(scopes.length === 1 ? scopes[0] : scopes);\n\n // Development-only validation\n const warnedMissingConditionRef = useRef(false);\n\n useEffect(() => {\n if (process.env.NODE_ENV !== \"development\") {\n return;\n }\n\n if (!warnedMissingConditionRef.current && !isDefined(when) && !scope) {\n warnedMissingConditionRef.current = true;\n console.warn(\n '[react-action-guard-router] useShouldBlock: Neither \"when\" nor \"scope\" provided. ' +\n \"This hook will always return false. Please provide at least one condition.\"\n );\n }\n }, [when, scope]);\n\n const blockCondition = isDefined(when) && resolveCondition(when);\n\n return blockCondition || Boolean(scope && isScopeBlocked);\n}\n","import type { ConfirmationOwner } from \"./confirmationOwner.types\";\n\nexport function createConfirmationOwner(): ConfirmationOwner {\n let currentAttempt = 0;\n\n function invalidate(): void {\n currentAttempt += 1;\n }\n\n function begin(): () => boolean {\n const attempt = ++currentAttempt;\n\n function settle(): boolean {\n if (attempt !== currentAttempt) {\n return false;\n }\n invalidate();\n\n return true;\n }\n\n return settle;\n }\n\n return { begin, invalidate };\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport { useBlocker } from \"react-router-dom\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n createConfirmationOwner,\n createScopeKey,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { Nullable } from \"@okyrychenko-dev/type-utils\";\nimport type { NavigationBlockerReturn } from \"../core\";\nimport type { BlockedNavigationAttempt, UseNavigationBlockerOptions } from \"./types\";\n\n/**\n * Blocks navigation in React Router v6+ applications based on conditions or scope state.\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const {\n when,\n scope,\n message,\n blockBrowserUnload = true,\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n block, // deprecated, for backwards compatibility\n onBlock,\n onAllow,\n onConfirm,\n } = options;\n\n const [confirmationOwner] = useState(createConfirmationOwner);\n const { begin, invalidate } = confirmationOwner;\n const [blockedAttempt, setBlockedAttempt] = useState<Nullable<BlockedNavigationAttempt>>(null);\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when ?? block, scope);\n\n const scopeKey = createScopeKey(scope);\n\n useEffect(() => {\n return invalidate;\n }, [invalidate]);\n\n // Use React Router's blocker\n const blocker = useBlocker(\n useCallback(() => {\n const settle = begin();\n\n // Early return if not blocking\n if (!shouldBlock) {\n setBlockedAttempt(null);\n\n return false;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n // If no message, just block\n if (!message) {\n setBlockedAttempt({ kind: \"denied\", settle, scopeKey, message });\n\n return true;\n }\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"async\") {\n setBlockedAttempt({\n kind: \"confirming\",\n settle,\n promise: confirmation.promise.catch(() => false),\n scopeKey,\n message,\n });\n\n return true;\n }\n\n if (confirmation.confirmed && settle()) {\n setBlockedAttempt(null);\n onAllow?.();\n\n return false;\n }\n\n setBlockedAttempt({ kind: \"denied\", settle, scopeKey, message });\n\n return true;\n }, [shouldBlock, scopeKey, message, onBlock, onConfirm, onAllow, begin])\n );\n\n useEffect(() => {\n if (!blockedAttempt) {\n return;\n }\n\n // Compare protection values, not inline callback identities, before attaching completion.\n if (\n !shouldBlock ||\n blockedAttempt.scopeKey !== scopeKey ||\n blockedAttempt.message !== message\n ) {\n queueMicrotask(() => {\n setBlockedAttempt((current) => (current === blockedAttempt ? null : current));\n });\n\n if (blockedAttempt.settle()) {\n blocker.reset?.();\n }\n\n return;\n }\n\n if (blockedAttempt.kind === \"denied\") {\n return;\n }\n\n let active = true;\n const { settle, promise } = blockedAttempt;\n\n void promise.then((confirmed) => {\n if (!active || !settle()) {\n return;\n }\n\n setBlockedAttempt(null);\n\n if (confirmed) {\n onAllow?.();\n blocker.proceed?.();\n } else {\n blocker.reset?.();\n }\n });\n\n return () => {\n active = false;\n };\n }, [blockedAttempt, blocker, onAllow, shouldBlock, scopeKey, message]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n isIntercepting: blocker.state === \"blocked\",\n };\n}\n","import { useNavigationBlocker } from \"./useNavigationBlocker\";\nimport type { UsePromptOptions } from \"./types\";\n\n/**\n * Simple prompt API similar to React Router v5's `usePrompt`.\n */\nexport function usePrompt(message: string, when: boolean | (() => boolean)): void {\n useNavigationBlocker({\n when,\n message,\n blockBrowserUnload: true,\n });\n}\n\n/**\n * Hook variant that accepts options object\n *\n * @param options - Prompt configuration\n *\n * @example\n * ```tsx\n * usePromptWithOptions({\n * message: 'Leave without saving?',\n * when: hasChanges\n * });\n * ```\n */\nexport function usePromptWithOptions(options: UsePromptOptions): void {\n useNavigationBlocker({\n ...options,\n blockBrowserUnload: options.blockBrowserUnload ?? true,\n });\n}\n"]}