@okyrychenko-dev/react-action-guard-router 0.2.2 → 0.2.4
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 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/core/constants.ts","../../src/nextjs/usePagesRouterBlocker.ts","../../src/nextjs/useAppRouterBlocker.ts"],"names":["useEffect","useMemo","useIsBlocked","useRef","router","useRouter","useNavigationBlocker"],"mappings":"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","file":"index.cjs","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","/**\n * Error messages for route blocking\n */\nexport const ROUTE_ERRORS = {\n /** Route change was aborted by the user */\n ABORTED: \"Route change aborted by user\",\n /** Route change was blocked */\n BLOCKED: \"Route change blocked\",\n} as const;\n","import { useRouter } from \"next/router\";\nimport { useEffect, useRef } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n ROUTE_ERRORS,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { UseNavigationBlockerOptions } from \"./types\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\n\n/**\n * Blocks navigation in Next.js Pages Router applications.\n *\n * This hook integrates with Next.js Pages Router's route change events and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific scopes are blocked.\n *\n * **Note:** This hook ONLY works with Next.js Pages Router (pages/ directory).\n * For App Router, use the App Router adapter instead.\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\n * - Callbacks for block/allow events\n *\n * **Custom confirm note:**\n * Async `onConfirm` is supported, but Pages Router requires a re-attempted\n * navigation after confirmation (via `router.push(url)` with default options).\n *\n * ⚠️ **Limitation:** When using async `onConfirm`, the re-attempted navigation\n * uses default options. Original `as`, `shallow`, `locale`, and `scroll` params\n * may be lost because Next.js Pages Router does not expose these in\n * the `routeChangeStart` event. For precise control, use synchronous confirmations.\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // pages/edit.tsx\n * function EditPage() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\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 scope\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your order is being processed. Please wait.',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n const allowNextUrlRef = useRef<string | null>(null);\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using Next.js router events\n useEffect(() => {\n if (!shouldBlock) {\n return;\n }\n\n const handleRouteChangeStart = (url: string): void => {\n // Early return for allowed URL\n if (allowNextUrlRef.current === url) {\n allowNextUrlRef.current = null;\n onAllow?.();\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n // If no message, block silently\n if (!message) {\n router.events.emit(\"routeChangeError\");\n throw new Error(ROUTE_ERRORS.BLOCKED);\n }\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"sync\" && !confirmation.confirmed) {\n router.events.emit(\"routeChangeError\");\n throw new Error(ROUTE_ERRORS.ABORTED);\n }\n\n if (confirmation.kind === \"sync\") {\n onAllow?.();\n return;\n }\n\n router.events.emit(\"routeChangeError\");\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n allowNextUrlRef.current = url;\n onAllow?.();\n void router.push(url);\n }\n })\n .catch(() => {\n // On error, treat as cancelled\n });\n throw new Error(ROUTE_ERRORS.ABORTED);\n };\n\n router.events.on(\"routeChangeStart\", handleRouteChangeStart);\n\n return () => {\n router.events.off(\"routeChangeStart\", handleRouteChangeStart);\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n","import { useEffect } from \"react\";\nimport { DEFAULT_UNLOAD_MESSAGE, useBeforeUnload, useShouldBlock } from \"../core\";\nimport type { UseNavigationBlockerOptions } from \"./types\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\n\n/**\n * Blocks navigation in Next.js App Router applications.\n *\n * **⚠️ IMPORTANT LIMITATION:**\n * Next.js App Router (app/ directory) does not currently provide an official API\n * for blocking navigation. This implementation provides best-effort blocking using\n * available mechanisms, but has limitations:\n *\n * - ✅ Browser back/forward buttons are blocked via `beforeunload`\n * - ✅ Tab close/refresh is blocked via `beforeunload`\n * - ❌ Next.js `<Link>` components are NOT blocked\n * - ❌ Programmatic `router.push()` is NOT blocked\n * - ❌ Browser address bar navigation is NOT fully blocked\n *\n * For production use with full blocking capabilities, consider using Pages Router\n * or wait for official Next.js App Router blocking API.\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // app/edit/page.tsx\n * 'use client';\n *\n * function EditPage() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\n * message: 'You have unsaved changes.',\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Block based on scope\n * 'use client';\n *\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your order is being processed.',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, blockBrowserUnload = true } = options;\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Log warning in development\n useEffect(() => {\n if (process.env.NODE_ENV === \"development\" && shouldBlock) {\n console.warn(\n \"[react-action-guard-router] App Router has limited blocking support. \" +\n \"Link clicks cannot be blocked. Use Pages Router for full support.\"\n );\n }\n }, [shouldBlock]);\n\n // Best effort: Block browser navigation via beforeunload\n // This will prevent tab close, refresh, and some browser back/forward\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/core/constants.ts","../../src/nextjs/usePagesRouterBlocker.ts","../../src/nextjs/useAppRouterBlocker.ts"],"names":["useEffect","useMemo","useIsBlocked","useRef","router","useRouter","useNavigationBlocker"],"mappings":"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","file":"index.cjs","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","/**\n * Error messages for route blocking\n */\nexport const ROUTE_ERRORS = {\n /** Route change was aborted by the user */\n ABORTED: \"Route change aborted by user\",\n /** Route change was blocked */\n BLOCKED: \"Route change blocked\",\n} as const;\n","import { useRouter } from \"next/router\";\nimport { useEffect, useRef } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n ROUTE_ERRORS,\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 Next.js Pages Router applications.\n *\n * This hook integrates with Next.js Pages Router's route change events and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific scopes are blocked.\n *\n * **Note:** This hook ONLY works with Next.js Pages Router (pages/ directory).\n * For App Router, use the App Router adapter instead.\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\n * - Callbacks for block/allow events\n *\n * **Custom confirm note:**\n * Async `onConfirm` is supported, but Pages Router requires a re-attempted\n * navigation after confirmation (via `router.push(url)` with default options).\n *\n * ⚠️ **Limitation:** When using async `onConfirm`, the re-attempted navigation\n * uses default options. Original `as`, `shallow`, `locale`, and `scroll` params\n * may be lost because Next.js Pages Router does not expose these in\n * the `routeChangeStart` event. For precise control, use synchronous confirmations.\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // pages/edit.tsx\n * function EditPage() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\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 scope\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your order is being processed. Please wait.',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n const allowNextUrlRef = useRef<string | null>(null);\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using Next.js router events\n useEffect(() => {\n if (!shouldBlock) {\n return;\n }\n\n const handleRouteChangeStart = (url: string): void => {\n // Early return for allowed URL\n if (allowNextUrlRef.current === url) {\n allowNextUrlRef.current = null;\n onAllow?.();\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n // If no message, block silently\n if (!message) {\n router.events.emit(\"routeChangeError\");\n throw new Error(ROUTE_ERRORS.BLOCKED);\n }\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"sync\" && !confirmation.confirmed) {\n router.events.emit(\"routeChangeError\");\n throw new Error(ROUTE_ERRORS.ABORTED);\n }\n\n if (confirmation.kind === \"sync\") {\n onAllow?.();\n return;\n }\n\n router.events.emit(\"routeChangeError\");\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n allowNextUrlRef.current = url;\n onAllow?.();\n void router.push(url);\n }\n })\n .catch(() => {\n // On error, treat as cancelled\n });\n throw new Error(ROUTE_ERRORS.ABORTED);\n };\n\n router.events.on(\"routeChangeStart\", handleRouteChangeStart);\n\n return () => {\n router.events.off(\"routeChangeStart\", handleRouteChangeStart);\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n","import { useEffect } from \"react\";\nimport { DEFAULT_UNLOAD_MESSAGE, useBeforeUnload, useShouldBlock } from \"../core\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\nimport type { UseNavigationBlockerOptions } from \"./types\";\n\n/**\n * Blocks navigation in Next.js App Router applications.\n *\n * **⚠️ IMPORTANT LIMITATION:**\n * Next.js App Router (app/ directory) does not currently provide an official API\n * for blocking navigation. This implementation provides best-effort blocking using\n * available mechanisms, but has limitations:\n *\n * - ✅ Browser back/forward buttons are blocked via `beforeunload`\n * - ✅ Tab close/refresh is blocked via `beforeunload`\n * - ❌ Next.js `<Link>` components are NOT blocked\n * - ❌ Programmatic `router.push()` is NOT blocked\n * - ❌ Browser address bar navigation is NOT fully blocked\n *\n * For production use with full blocking capabilities, consider using Pages Router\n * or wait for official Next.js App Router blocking API.\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // app/edit/page.tsx\n * 'use client';\n *\n * function EditPage() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\n * message: 'You have unsaved changes.',\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Block based on scope\n * 'use client';\n *\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your order is being processed.',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, blockBrowserUnload = true } = options;\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Log warning in development\n useEffect(() => {\n if (process.env.NODE_ENV === \"development\" && shouldBlock) {\n console.warn(\n \"[react-action-guard-router] App Router has limited blocking support. \" +\n \"Link clicks cannot be blocked. Use Pages Router for full support.\"\n );\n }\n }, [shouldBlock]);\n\n // Best effort: Block browser navigation via beforeunload\n // This will prevent tab close, refresh, and some browser back/forward\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
package/dist/nextjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/core/constants.ts","../../src/nextjs/usePagesRouterBlocker.ts","../../src/nextjs/useAppRouterBlocker.ts"],"names":["useEffect","useRef","useNavigationBlocker"],"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","/**\n * Error messages for route blocking\n */\nexport const ROUTE_ERRORS = {\n /** Route change was aborted by the user */\n ABORTED: \"Route change aborted by user\",\n /** Route change was blocked */\n BLOCKED: \"Route change blocked\",\n} as const;\n","import { useRouter } from \"next/router\";\nimport { useEffect, useRef } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n ROUTE_ERRORS,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { UseNavigationBlockerOptions } from \"./types\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\n\n/**\n * Blocks navigation in Next.js Pages Router applications.\n *\n * This hook integrates with Next.js Pages Router's route change events and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific scopes are blocked.\n *\n * **Note:** This hook ONLY works with Next.js Pages Router (pages/ directory).\n * For App Router, use the App Router adapter instead.\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\n * - Callbacks for block/allow events\n *\n * **Custom confirm note:**\n * Async `onConfirm` is supported, but Pages Router requires a re-attempted\n * navigation after confirmation (via `router.push(url)` with default options).\n *\n * ⚠️ **Limitation:** When using async `onConfirm`, the re-attempted navigation\n * uses default options. Original `as`, `shallow`, `locale`, and `scroll` params\n * may be lost because Next.js Pages Router does not expose these in\n * the `routeChangeStart` event. For precise control, use synchronous confirmations.\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // pages/edit.tsx\n * function EditPage() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\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 scope\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your order is being processed. Please wait.',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n const allowNextUrlRef = useRef<string | null>(null);\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using Next.js router events\n useEffect(() => {\n if (!shouldBlock) {\n return;\n }\n\n const handleRouteChangeStart = (url: string): void => {\n // Early return for allowed URL\n if (allowNextUrlRef.current === url) {\n allowNextUrlRef.current = null;\n onAllow?.();\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n // If no message, block silently\n if (!message) {\n router.events.emit(\"routeChangeError\");\n throw new Error(ROUTE_ERRORS.BLOCKED);\n }\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"sync\" && !confirmation.confirmed) {\n router.events.emit(\"routeChangeError\");\n throw new Error(ROUTE_ERRORS.ABORTED);\n }\n\n if (confirmation.kind === \"sync\") {\n onAllow?.();\n return;\n }\n\n router.events.emit(\"routeChangeError\");\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n allowNextUrlRef.current = url;\n onAllow?.();\n void router.push(url);\n }\n })\n .catch(() => {\n // On error, treat as cancelled\n });\n throw new Error(ROUTE_ERRORS.ABORTED);\n };\n\n router.events.on(\"routeChangeStart\", handleRouteChangeStart);\n\n return () => {\n router.events.off(\"routeChangeStart\", handleRouteChangeStart);\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n","import { useEffect } from \"react\";\nimport { DEFAULT_UNLOAD_MESSAGE, useBeforeUnload, useShouldBlock } from \"../core\";\nimport type { UseNavigationBlockerOptions } from \"./types\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\n\n/**\n * Blocks navigation in Next.js App Router applications.\n *\n * **⚠️ IMPORTANT LIMITATION:**\n * Next.js App Router (app/ directory) does not currently provide an official API\n * for blocking navigation. This implementation provides best-effort blocking using\n * available mechanisms, but has limitations:\n *\n * - ✅ Browser back/forward buttons are blocked via `beforeunload`\n * - ✅ Tab close/refresh is blocked via `beforeunload`\n * - ❌ Next.js `<Link>` components are NOT blocked\n * - ❌ Programmatic `router.push()` is NOT blocked\n * - ❌ Browser address bar navigation is NOT fully blocked\n *\n * For production use with full blocking capabilities, consider using Pages Router\n * or wait for official Next.js App Router blocking API.\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // app/edit/page.tsx\n * 'use client';\n *\n * function EditPage() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\n * message: 'You have unsaved changes.',\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Block based on scope\n * 'use client';\n *\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your order is being processed.',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, blockBrowserUnload = true } = options;\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Log warning in development\n useEffect(() => {\n if (process.env.NODE_ENV === \"development\" && shouldBlock) {\n console.warn(\n \"[react-action-guard-router] App Router has limited blocking support. \" +\n \"Link clicks cannot be blocked. Use Pages Router for full support.\"\n );\n }\n }, [shouldBlock]);\n\n // Best effort: Block browser navigation via beforeunload\n // This will prevent tab close, refresh, and some browser back/forward\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/core/constants.ts","../../src/nextjs/usePagesRouterBlocker.ts","../../src/nextjs/useAppRouterBlocker.ts"],"names":["useEffect","useRef","useNavigationBlocker"],"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","/**\n * Error messages for route blocking\n */\nexport const ROUTE_ERRORS = {\n /** Route change was aborted by the user */\n ABORTED: \"Route change aborted by user\",\n /** Route change was blocked */\n BLOCKED: \"Route change blocked\",\n} as const;\n","import { useRouter } from \"next/router\";\nimport { useEffect, useRef } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n ROUTE_ERRORS,\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 Next.js Pages Router applications.\n *\n * This hook integrates with Next.js Pages Router's route change events and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific scopes are blocked.\n *\n * **Note:** This hook ONLY works with Next.js Pages Router (pages/ directory).\n * For App Router, use the App Router adapter instead.\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\n * - Callbacks for block/allow events\n *\n * **Custom confirm note:**\n * Async `onConfirm` is supported, but Pages Router requires a re-attempted\n * navigation after confirmation (via `router.push(url)` with default options).\n *\n * ⚠️ **Limitation:** When using async `onConfirm`, the re-attempted navigation\n * uses default options. Original `as`, `shallow`, `locale`, and `scroll` params\n * may be lost because Next.js Pages Router does not expose these in\n * the `routeChangeStart` event. For precise control, use synchronous confirmations.\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // pages/edit.tsx\n * function EditPage() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\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 scope\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your order is being processed. Please wait.',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n const allowNextUrlRef = useRef<string | null>(null);\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using Next.js router events\n useEffect(() => {\n if (!shouldBlock) {\n return;\n }\n\n const handleRouteChangeStart = (url: string): void => {\n // Early return for allowed URL\n if (allowNextUrlRef.current === url) {\n allowNextUrlRef.current = null;\n onAllow?.();\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n // If no message, block silently\n if (!message) {\n router.events.emit(\"routeChangeError\");\n throw new Error(ROUTE_ERRORS.BLOCKED);\n }\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"sync\" && !confirmation.confirmed) {\n router.events.emit(\"routeChangeError\");\n throw new Error(ROUTE_ERRORS.ABORTED);\n }\n\n if (confirmation.kind === \"sync\") {\n onAllow?.();\n return;\n }\n\n router.events.emit(\"routeChangeError\");\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n allowNextUrlRef.current = url;\n onAllow?.();\n void router.push(url);\n }\n })\n .catch(() => {\n // On error, treat as cancelled\n });\n throw new Error(ROUTE_ERRORS.ABORTED);\n };\n\n router.events.on(\"routeChangeStart\", handleRouteChangeStart);\n\n return () => {\n router.events.off(\"routeChangeStart\", handleRouteChangeStart);\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n","import { useEffect } from \"react\";\nimport { DEFAULT_UNLOAD_MESSAGE, useBeforeUnload, useShouldBlock } from \"../core\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\nimport type { UseNavigationBlockerOptions } from \"./types\";\n\n/**\n * Blocks navigation in Next.js App Router applications.\n *\n * **⚠️ IMPORTANT LIMITATION:**\n * Next.js App Router (app/ directory) does not currently provide an official API\n * for blocking navigation. This implementation provides best-effort blocking using\n * available mechanisms, but has limitations:\n *\n * - ✅ Browser back/forward buttons are blocked via `beforeunload`\n * - ✅ Tab close/refresh is blocked via `beforeunload`\n * - ❌ Next.js `<Link>` components are NOT blocked\n * - ❌ Programmatic `router.push()` is NOT blocked\n * - ❌ Browser address bar navigation is NOT fully blocked\n *\n * For production use with full blocking capabilities, consider using Pages Router\n * or wait for official Next.js App Router blocking API.\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // app/edit/page.tsx\n * 'use client';\n *\n * function EditPage() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\n * message: 'You have unsaved changes.',\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Block based on scope\n * 'use client';\n *\n * function CheckoutFlow() {\n * useNavigationBlocker({\n * scope: 'checkout',\n * message: 'Your order is being processed.',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, blockBrowserUnload = true } = options;\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Log warning in development\n useEffect(() => {\n if (process.env.NODE_ENV === \"development\" && shouldBlock) {\n console.warn(\n \"[react-action-guard-router] App Router has limited blocking support. \" +\n \"Link clicks cannot be blocked. Use Pages Router for full support.\"\n );\n }\n }, [shouldBlock]);\n\n // Best effort: Block browser navigation via beforeunload\n // This will prevent tab close, refresh, and some browser back/forward\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
|
@@ -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","useMemo","useIsBlocked","useRef","useState","useBlocker","useCallback"],"mappings":"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","file":"index.cjs","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 { UseNavigationBlockerOptions } from \"./types\";\nimport type { NavigationBlockerReturn } from \"../core/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/react-router/useNavigationBlocker.ts","../../src/react-router/usePrompt.ts"],"names":["useEffect","useMemo","useIsBlocked","useRef","useState","useBlocker","useCallback"],"mappings":"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","file":"index.cjs","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 +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 { UseNavigationBlockerOptions } from \"./types\";\nimport type { NavigationBlockerReturn } from \"../core/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/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 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/tanstack-router/useNavigationBlocker.ts"],"names":["useEffect","useMemo","useIsBlocked","useRef","useRouter"],"mappings":"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","file":"index.cjs","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 { useRouter } from \"@tanstack/react-router\";\nimport { useEffect } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { SafeTanStackRouter, UseNavigationBlockerOptions } from \"./types\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\n\ninterface RetryableUpdate {\n [key: string]: unknown;\n retry: () => void;\n}\n\nconst hasBlockingHistory = (value: unknown): value is SafeTanStackRouter => {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n if (!(\"history\" in value)) {\n return false;\n }\n const history = value.history;\n if (!history || typeof history !== \"object\") {\n return false;\n }\n if (!(\"block\" in history)) {\n return false;\n }\n return typeof history.block === \"function\";\n};\n\nconst hasRetry = (value: Record<string, unknown>): value is RetryableUpdate =>\n typeof value.retry === \"function\";\n\nconst attemptRetry = (update: Record<string, unknown>): void => {\n if (hasRetry(update)) {\n try {\n update.retry();\n } catch {\n // Swallow retry errors to avoid breaking blocking flow.\n }\n }\n};\n\n/**\n * Blocks navigation in TanStack Router applications based on conditions or scope state.\n *\n * This hook integrates with TanStack Router's history blocking mechanism and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific 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\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 * // Type-safe with scopes\n * import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';\n *\n * type AppScopes = 'global' | 'form' | 'checkout';\n * const { useBlocker } = createTypedHooks<AppScopes>();\n *\n * function MyComponent() {\n * useBlocker('blocker-id', { scope: 'form' });\n *\n * useNavigationBlocker({\n * scope: 'form', // Type-safe!\n * message: 'Leave?',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using TanStack Router's history\n useEffect(() => {\n if (!shouldBlock || !hasBlockingHistory(router)) {\n return;\n }\n\n // Block navigation with properly typed update parameter\n type NavigationUpdate = Record<string, unknown> & { retry?: () => void };\n const unblock = router.history.block((update: NavigationUpdate) => {\n // Early return if no message\n if (!message) {\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"async\") {\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n })\n .catch((error: unknown) => {\n // On error, unblock but don't retry\n unblock();\n if (process.env.NODE_ENV !== \"production\") {\n console.error(\"[react-action-guard-router] Confirmation error:\", error);\n }\n });\n return;\n }\n\n if (confirmation.confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n });\n\n return () => {\n unblock();\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/tanstack-router/useNavigationBlocker.ts"],"names":["useEffect","useMemo","useIsBlocked","useRef","useRouter"],"mappings":"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","file":"index.cjs","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 { useRouter } from \"@tanstack/react-router\";\nimport { useEffect } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\nimport type { SafeTanStackRouter, UseNavigationBlockerOptions } from \"./types\";\n\ninterface RetryableUpdate {\n [key: string]: unknown;\n retry: () => void;\n}\n\nconst hasBlockingHistory = (value: unknown): value is SafeTanStackRouter => {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n if (!(\"history\" in value)) {\n return false;\n }\n const history = value.history;\n if (!history || typeof history !== \"object\") {\n return false;\n }\n if (!(\"block\" in history)) {\n return false;\n }\n return typeof history.block === \"function\";\n};\n\nconst hasRetry = (value: Record<string, unknown>): value is RetryableUpdate =>\n typeof value.retry === \"function\";\n\nconst attemptRetry = (update: Record<string, unknown>): void => {\n if (hasRetry(update)) {\n try {\n update.retry();\n } catch {\n // Swallow retry errors to avoid breaking blocking flow.\n }\n }\n};\n\n/**\n * Blocks navigation in TanStack Router applications based on conditions or scope state.\n *\n * This hook integrates with TanStack Router's history blocking mechanism and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific 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\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 * // Type-safe with scopes\n * import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';\n *\n * type AppScopes = 'global' | 'form' | 'checkout';\n * const { useBlocker } = createTypedHooks<AppScopes>();\n *\n * function MyComponent() {\n * useBlocker('blocker-id', { scope: 'form' });\n *\n * useNavigationBlocker({\n * scope: 'form', // Type-safe!\n * message: 'Leave?',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using TanStack Router's history\n useEffect(() => {\n if (!shouldBlock || !hasBlockingHistory(router)) {\n return;\n }\n\n // Block navigation with properly typed update parameter\n type NavigationUpdate = Record<string, unknown> & { retry?: () => void };\n const unblock = router.history.block((update: NavigationUpdate) => {\n // Early return if no message\n if (!message) {\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"async\") {\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n })\n .catch((error: unknown) => {\n // On error, unblock but don't retry\n unblock();\n if (process.env.NODE_ENV !== \"production\") {\n console.error(\"[react-action-guard-router] Confirmation error:\", error);\n }\n });\n return;\n }\n\n if (confirmation.confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n });\n\n return () => {\n unblock();\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/tanstack-router/useNavigationBlocker.ts"],"names":["useEffect"],"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 { useRouter } from \"@tanstack/react-router\";\nimport { useEffect } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { SafeTanStackRouter, UseNavigationBlockerOptions } from \"./types\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\n\ninterface RetryableUpdate {\n [key: string]: unknown;\n retry: () => void;\n}\n\nconst hasBlockingHistory = (value: unknown): value is SafeTanStackRouter => {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n if (!(\"history\" in value)) {\n return false;\n }\n const history = value.history;\n if (!history || typeof history !== \"object\") {\n return false;\n }\n if (!(\"block\" in history)) {\n return false;\n }\n return typeof history.block === \"function\";\n};\n\nconst hasRetry = (value: Record<string, unknown>): value is RetryableUpdate =>\n typeof value.retry === \"function\";\n\nconst attemptRetry = (update: Record<string, unknown>): void => {\n if (hasRetry(update)) {\n try {\n update.retry();\n } catch {\n // Swallow retry errors to avoid breaking blocking flow.\n }\n }\n};\n\n/**\n * Blocks navigation in TanStack Router applications based on conditions or scope state.\n *\n * This hook integrates with TanStack Router's history blocking mechanism and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific 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\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 * // Type-safe with scopes\n * import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';\n *\n * type AppScopes = 'global' | 'form' | 'checkout';\n * const { useBlocker } = createTypedHooks<AppScopes>();\n *\n * function MyComponent() {\n * useBlocker('blocker-id', { scope: 'form' });\n *\n * useNavigationBlocker({\n * scope: 'form', // Type-safe!\n * message: 'Leave?',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using TanStack Router's history\n useEffect(() => {\n if (!shouldBlock || !hasBlockingHistory(router)) {\n return;\n }\n\n // Block navigation with properly typed update parameter\n type NavigationUpdate = Record<string, unknown> & { retry?: () => void };\n const unblock = router.history.block((update: NavigationUpdate) => {\n // Early return if no message\n if (!message) {\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"async\") {\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n })\n .catch((error: unknown) => {\n // On error, unblock but don't retry\n unblock();\n if (process.env.NODE_ENV !== \"production\") {\n console.error(\"[react-action-guard-router] Confirmation error:\", error);\n }\n });\n return;\n }\n\n if (confirmation.confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n });\n\n return () => {\n unblock();\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/tanstack-router/useNavigationBlocker.ts"],"names":["useEffect"],"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 { useRouter } from \"@tanstack/react-router\";\nimport { useEffect } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\nimport type { SafeTanStackRouter, UseNavigationBlockerOptions } from \"./types\";\n\ninterface RetryableUpdate {\n [key: string]: unknown;\n retry: () => void;\n}\n\nconst hasBlockingHistory = (value: unknown): value is SafeTanStackRouter => {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n if (!(\"history\" in value)) {\n return false;\n }\n const history = value.history;\n if (!history || typeof history !== \"object\") {\n return false;\n }\n if (!(\"block\" in history)) {\n return false;\n }\n return typeof history.block === \"function\";\n};\n\nconst hasRetry = (value: Record<string, unknown>): value is RetryableUpdate =>\n typeof value.retry === \"function\";\n\nconst attemptRetry = (update: Record<string, unknown>): void => {\n if (hasRetry(update)) {\n try {\n update.retry();\n } catch {\n // Swallow retry errors to avoid breaking blocking flow.\n }\n }\n};\n\n/**\n * Blocks navigation in TanStack Router applications based on conditions or scope state.\n *\n * This hook integrates with TanStack Router's history blocking mechanism and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific 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\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 * // Type-safe with scopes\n * import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';\n *\n * type AppScopes = 'global' | 'form' | 'checkout';\n * const { useBlocker } = createTypedHooks<AppScopes>();\n *\n * function MyComponent() {\n * useBlocker('blocker-id', { scope: 'form' });\n *\n * useNavigationBlocker({\n * scope: 'form', // Type-safe!\n * message: 'Leave?',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using TanStack Router's history\n useEffect(() => {\n if (!shouldBlock || !hasBlockingHistory(router)) {\n return;\n }\n\n // Block navigation with properly typed update parameter\n type NavigationUpdate = Record<string, unknown> & { retry?: () => void };\n const unblock = router.history.block((update: NavigationUpdate) => {\n // Early return if no message\n if (!message) {\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"async\") {\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n })\n .catch((error: unknown) => {\n // On error, unblock but don't retry\n unblock();\n if (process.env.NODE_ENV !== \"production\") {\n console.error(\"[react-action-guard-router] Confirmation error:\", error);\n }\n });\n return;\n }\n\n if (confirmation.confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n });\n\n return () => {\n unblock();\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okyrychenko-dev/react-action-guard-router",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Navigation blocking adapters for React Action Guard across React Router, TanStack Router, and Next.js",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"access": "public"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@okyrychenko-dev/react-action-guard": "^1.0.
|
|
81
|
+
"@okyrychenko-dev/react-action-guard": "^1.0.4",
|
|
82
82
|
"@tanstack/react-router": "^1.0.0",
|
|
83
83
|
"next": "^13.4.0 || ^14.0.0 || ^15.0.0",
|
|
84
84
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -96,11 +96,12 @@
|
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"overrides": {
|
|
99
|
-
"@okyrychenko-dev/react-zustand-toolkit": "^0.4.
|
|
99
|
+
"@okyrychenko-dev/react-zustand-toolkit": "^0.4.1",
|
|
100
|
+
"postcss": ">=8.5.10"
|
|
100
101
|
},
|
|
101
102
|
"devDependencies": {
|
|
102
103
|
"@eslint/js": "^9.39.1",
|
|
103
|
-
"@okyrychenko-dev/react-action-guard": "^1.0.
|
|
104
|
+
"@okyrychenko-dev/react-action-guard": "^1.0.4",
|
|
104
105
|
"@storybook/addon-a11y": "10.0.8",
|
|
105
106
|
"@storybook/addon-docs": "10.0.8",
|
|
106
107
|
"@storybook/react-vite": "10.0.8",
|
|
@@ -129,7 +130,7 @@
|
|
|
129
130
|
"react-dom": "^19.2.0",
|
|
130
131
|
"react-router-dom": "^7.1.3",
|
|
131
132
|
"remark-gfm": "^4.0.1",
|
|
132
|
-
"storybook": "10.
|
|
133
|
+
"storybook": "^10.3.6",
|
|
133
134
|
"tsup": "^8.5.1",
|
|
134
135
|
"typescript": "^5.3.3",
|
|
135
136
|
"typescript-eslint": "^8.46.4",
|