@real-router/preact 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/dist/cjs/index.d.ts +216 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.mts +216 -2
- package/dist/esm/index.d.mts.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/RouterProvider.tsx +20 -1
- package/src/hooks/useRoute.tsx +15 -4
- package/src/hooks/useRouteEnter.tsx +147 -0
- package/src/hooks/useRouteExit.tsx +159 -0
- package/src/index.ts +16 -0
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/components/RouteView/components.tsx","../../src/components/RouteView/helpers.tsx","../../src/useSyncExternalStore.ts","../../src/context.ts","../../src/hooks/useRouter.tsx","../../src/hooks/useRouteNode.tsx","../../src/components/RouteView/RouteView.tsx","../../src/constants.ts","../../../../shared/dom-utils/route-announcer.ts","../../../../shared/dom-utils/scroll-restore.ts","../../../../shared/dom-utils/link-utils.ts","../../src/hooks/useIsActiveRoute.tsx","../../src/components/Link.tsx","../../src/components/RouterErrorBoundary.tsx","../../src/hooks/useNavigator.tsx","../../src/hooks/useRouteUtils.tsx","../../src/hooks/useRoute.tsx","../../src/hooks/useRouterTransition.tsx","../../src/RouterProvider.tsx"],"sourcesContent":["import type { MatchProps, NotFoundProps, SelfProps } from \"./types\";\n\nexport function Match(_props: MatchProps): null {\n return null;\n}\n\nMatch.displayName = \"RouteView.Match\";\n\nexport function Self(_props: SelfProps): null {\n return null;\n}\n\nSelf.displayName = \"RouteView.Self\";\n\nexport function NotFound(_props: NotFoundProps): null {\n return null;\n}\n\nNotFound.displayName = \"RouteView.NotFound\";\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\nimport { startsWithSegment } from \"@real-router/route-utils\";\nimport { Fragment, isValidElement, toChildArray } from \"preact\";\nimport { Suspense } from \"preact/compat\";\n\nimport { Match, NotFound, Self } from \"./components\";\n\nimport type { MatchProps, NotFoundProps, SelfProps } from \"./types\";\nimport type { VNode, ComponentChildren } from \"preact\";\n\ninterface FallbackSlots {\n selfChildren: ComponentChildren;\n selfFallback: ComponentChildren | undefined;\n selfFound: boolean;\n notFoundChildren: ComponentChildren;\n}\n\nfunction isSegmentMatch(\n routeName: string,\n fullSegmentName: string,\n exact: boolean,\n): boolean {\n if (fullSegmentName === \"\") {\n return false;\n }\n\n if (exact) {\n return routeName === fullSegmentName;\n }\n\n return startsWithSegment(routeName, fullSegmentName);\n}\n\nexport function collectElements(\n children: ComponentChildren,\n result: VNode[],\n): void {\n for (const child of toChildArray(children)) {\n if (!isValidElement(child)) {\n continue;\n }\n\n if (\n child.type === Match ||\n child.type === Self ||\n child.type === NotFound\n ) {\n result.push(child);\n } else {\n collectElements(\n (child.props as { readonly children: ComponentChildren }).children,\n result,\n );\n }\n }\n}\n\nfunction renderSlot(\n slotChildren: ComponentChildren,\n key: string,\n fallback?: ComponentChildren,\n): VNode {\n const content =\n fallback === undefined ? (\n slotChildren\n ) : (\n <Suspense fallback={fallback}>{slotChildren}</Suspense>\n );\n\n return <Fragment key={key}>{content}</Fragment>;\n}\n\nfunction recordFallback(child: VNode, slots: FallbackSlots): boolean {\n if (child.type === NotFound) {\n slots.notFoundChildren = (child.props as NotFoundProps).children;\n\n return true;\n }\n\n if (child.type === Self) {\n if (!slots.selfFound) {\n slots.selfChildren = (child.props as SelfProps).children;\n slots.selfFallback = (child.props as SelfProps).fallback;\n slots.selfFound = true;\n }\n\n return true;\n }\n\n return false;\n}\n\nfunction processMatch(\n child: VNode,\n routeName: string,\n nodeName: string,\n alreadyActive: boolean,\n): VNode | null {\n const { segment, exact = false, fallback } = child.props as MatchProps;\n const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;\n const isActive =\n !alreadyActive && isSegmentMatch(routeName, fullSegmentName, exact);\n\n if (!isActive) {\n return null;\n }\n\n return renderSlot(\n (child.props as MatchProps).children,\n fullSegmentName,\n fallback,\n );\n}\n\nfunction appendFallback(\n rendered: VNode[],\n routeName: string,\n nodeName: string,\n slots: FallbackSlots,\n): void {\n if (slots.selfFound && routeName === nodeName) {\n rendered.push(\n renderSlot(slots.selfChildren, \"__route-view-self__\", slots.selfFallback),\n );\n\n return;\n }\n\n if (routeName === UNKNOWN_ROUTE && slots.notFoundChildren !== null) {\n rendered.push(\n <Fragment key=\"__route-view-not-found__\">\n {slots.notFoundChildren}\n </Fragment>,\n );\n }\n}\n\nexport function buildRenderList(\n elements: VNode[],\n routeName: string,\n nodeName: string,\n): { rendered: VNode[]; activeMatchFound: boolean } {\n const slots: FallbackSlots = {\n selfChildren: null,\n selfFallback: undefined,\n selfFound: false,\n notFoundChildren: null,\n };\n let activeMatchFound = false;\n const rendered: VNode[] = [];\n\n for (const child of elements) {\n if (recordFallback(child, slots)) {\n continue;\n }\n\n const matchRendered = processMatch(\n child,\n routeName,\n nodeName,\n activeMatchFound,\n );\n\n if (matchRendered !== null) {\n activeMatchFound = true;\n rendered.push(matchRendered);\n }\n }\n\n if (!activeMatchFound) {\n appendFallback(rendered, routeName, nodeName, slots);\n }\n\n return { rendered, activeMatchFound };\n}\n","import { useEffect, useState } from \"preact/hooks\";\n\n/**\n * Polyfill for React's useSyncExternalStore.\n *\n * Preact does not provide a native useSyncExternalStore.\n * This implementation uses useState + useEffect to subscribe\n * to external stores.\n *\n * Race condition handling: the value may change between\n * `useState(getSnapshot)` (render) and `useEffect` (commit).\n * We synchronize by calling `setValue(getSnapshot())` before\n * subscribing in the effect.\n *\n * The updater uses `Object.is` to bail out when the snapshot\n * is referentially stable, preventing redundant re-renders.\n */\nexport function useSyncExternalStore<T>(\n subscribe: (onStoreChange: () => void) => () => void,\n getSnapshot: () => T,\n _getServerSnapshot?: () => T,\n): T {\n const [value, setValue] = useState(getSnapshot);\n\n useEffect(() => {\n const sync = (): void => {\n setValue((prev) => {\n const next = getSnapshot();\n\n return Object.is(prev, next) ? prev : next;\n });\n };\n\n sync();\n\n return subscribe(sync);\n }, [subscribe, getSnapshot]);\n\n return value;\n}\n","import { createContext } from \"preact\";\n\nimport type { RouteContext as RouteContextType } from \"./types\";\nimport type { Router, Navigator } from \"@real-router/core\";\n\nexport const RouteContext = createContext<RouteContextType | null>(null);\n\nexport const RouterContext = createContext<Router | null>(null);\n\nexport const NavigatorContext = createContext<Navigator | null>(null);\n","import { useContext } from \"preact/hooks\";\n\nimport { RouterContext } from \"../context\";\n\nimport type { Router } from \"@real-router/core\";\n\nexport const useRouter = (): Router => {\n const router = useContext(RouterContext);\n\n if (!router) {\n throw new Error(\"useRouter must be used within a RouterProvider\");\n }\n\n return router;\n};\n","import { getNavigator } from \"@real-router/core\";\nimport { createRouteNodeSource } from \"@real-router/sources\";\nimport { useMemo } from \"preact/hooks\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouteContext } from \"../types\";\n\nexport function useRouteNode(nodeName: string): RouteContext {\n const router = useRouter();\n\n const store = useMemo(\n () => createRouteNodeSource(router, nodeName),\n [router, nodeName],\n );\n\n const { route, previousRoute } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n\n const navigator = useMemo(() => getNavigator(router), [router]);\n\n return useMemo(\n (): RouteContext => ({ navigator, route, previousRoute }),\n [navigator, route, previousRoute],\n );\n}\n","import { useMemo } from \"preact/hooks\";\n\nimport { Match, NotFound, Self } from \"./components\";\nimport { buildRenderList, collectElements } from \"./helpers\";\nimport { useRouteNode } from \"../../hooks/useRouteNode\";\n\nimport type { RouteViewProps } from \"./types\";\nimport type { VNode } from \"preact\";\n\nfunction RouteViewRoot({\n nodeName,\n children,\n}: Readonly<RouteViewProps>): VNode | null {\n const { route } = useRouteNode(nodeName);\n\n // Cache the flattened Match/Self/NotFound list across renders with unchanged\n // children. children only differs when the parent re-renders with a new\n // node, so this memoises the steady-state traversal.\n const elements = useMemo(() => {\n const collected: VNode[] = [];\n\n collectElements(children, collected);\n\n return collected;\n }, [children]);\n\n if (!route) {\n return null;\n }\n\n const { rendered } = buildRenderList(elements, route.name, nodeName);\n\n if (rendered.length > 0) {\n return <>{rendered}</>;\n }\n\n return null;\n}\n\nRouteViewRoot.displayName = \"RouteView\";\n\nexport const RouteView = Object.assign(RouteViewRoot, {\n Match,\n Self,\n NotFound,\n});\n\nexport type {\n RouteViewProps,\n MatchProps as RouteViewMatchProps,\n SelfProps as RouteViewSelfProps,\n NotFoundProps as RouteViewNotFoundProps,\n} from \"./types\";\n","/**\n * Stable empty object for default params\n */\nexport const EMPTY_PARAMS = Object.freeze({});\n\n/**\n * Stable empty options object\n */\nexport const EMPTY_OPTIONS = Object.freeze({});\n","import type { Router, State } from \"@real-router/core\";\n\nconst CLEAR_DELAY = 7000;\nconst SAFARI_READY_DELAY = 100;\nconst ANNOUNCER_ATTR = \"data-real-router-announcer\";\nconst INTERNAL_ROUTE_PREFIX = \"@@\";\nconst VISUALLY_HIDDEN =\n \"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0\";\n\nexport interface RouteAnnouncerOptions {\n prefix?: string;\n getAnnouncementText?: (route: State) => string;\n}\n\nexport function createRouteAnnouncer(\n router: Router,\n options?: RouteAnnouncerOptions,\n): { destroy: () => void } {\n const prefix = options?.prefix ?? \"Navigated to \";\n const getCustomText = options?.getAnnouncementText;\n\n let isInitialNavigation = true;\n let isReady = false;\n let isDestroyed = false;\n let lastAnnouncedText = \"\";\n let pendingText: string | null = null;\n let clearTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const announcer = getOrCreateAnnouncer();\n\n const doAnnounce = (text: string, h1: HTMLElement | null): void => {\n lastAnnouncedText = text;\n clearTimeout(clearTimeoutId);\n announcer.textContent = text;\n clearTimeoutId = setTimeout(() => {\n announcer.textContent = \"\";\n lastAnnouncedText = \"\";\n }, CLEAR_DELAY);\n\n manageFocus(h1);\n };\n\n // Safari-ready delay: announcing before VoiceOver wires up the aria-live region\n // causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms\n // before marking the announcer \"ready\" — any navigation during that window is\n // buffered in pendingText and flushed once the delay expires.\n const safariTimeoutId = setTimeout(() => {\n isReady = true;\n\n if (pendingText !== null && !isDestroyed) {\n const text = pendingText;\n\n pendingText = null;\n doAnnounce(text, document.querySelector<HTMLElement>(\"h1\"));\n }\n }, SAFARI_READY_DELAY);\n\n const unsubscribe = router.subscribe(({ route }) => {\n if (isInitialNavigation) {\n isInitialNavigation = false;\n\n return;\n }\n\n // Double rAF: waits for two paint frames so the incoming route's DOM\n // (including the new <h1>) is fully rendered before resolveText reads it.\n // Single rAF fires before the new route's template has been attached,\n // which would cause resolveText to pick up the OLD h1 or fall back to\n // document.title / route.name prematurely.\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (isDestroyed) {\n return;\n }\n\n const h1 = document.querySelector<HTMLElement>(\"h1\");\n const text = resolveText(route, prefix, getCustomText, h1);\n\n if (!text || text === lastAnnouncedText) {\n return;\n }\n\n if (!isReady) {\n // Defer announcement until Safari-ready window elapses (see safariTimeoutId).\n pendingText = text;\n\n return;\n }\n\n doAnnounce(text, h1);\n });\n });\n });\n\n return {\n destroy() {\n isDestroyed = true;\n unsubscribe();\n clearTimeout(clearTimeoutId);\n clearTimeout(safariTimeoutId);\n removeAnnouncer();\n },\n };\n}\n\nfunction getOrCreateAnnouncer(): HTMLElement {\n const existing = document.querySelector<HTMLElement>(`[${ANNOUNCER_ATTR}]`);\n\n if (existing) {\n return existing;\n }\n\n const element = document.createElement(\"div\");\n\n element.setAttribute(\"style\", VISUALLY_HIDDEN);\n element.setAttribute(\"aria-live\", \"assertive\");\n element.setAttribute(\"aria-atomic\", \"true\");\n element.setAttribute(ANNOUNCER_ATTR, \"\");\n\n document.body.prepend(element);\n\n return element;\n}\n\nfunction removeAnnouncer(): void {\n document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();\n}\n\nfunction resolveText(\n route: State,\n prefix: string,\n getCustomText: ((route: State) => string) | undefined,\n h1: HTMLElement | null,\n): string {\n if (getCustomText) {\n return getCustomText(route);\n }\n\n const h1Text = h1?.textContent.trim() ?? \"\";\n const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)\n ? \"\"\n : route.name;\n const rawText =\n h1Text || document.title || routeName || globalThis.location.pathname;\n\n return `${prefix}${rawText}`;\n}\n\nfunction manageFocus(h1: HTMLElement | null): void {\n if (!h1) {\n return;\n }\n\n if (!h1.hasAttribute(\"tabindex\")) {\n h1.setAttribute(\"tabindex\", \"-1\");\n }\n\n h1.focus({ preventScroll: true });\n}\n","import type { Router, State } from \"@real-router/core\";\n\nconst STORAGE_KEY = \"real-router:scroll\";\n\nconst NOOP_INSTANCE: { destroy: () => void } = Object.freeze({\n destroy: () => {\n /* no-op */\n },\n});\n\nexport type ScrollRestorationMode = \"restore\" | \"top\" | \"manual\";\n\nexport interface ScrollRestorationOptions {\n mode?: ScrollRestorationMode | undefined;\n anchorScrolling?: boolean | undefined;\n scrollContainer?: (() => HTMLElement | null) | undefined;\n}\n\ninterface NavigationContext {\n direction?: \"forward\" | \"back\" | \"unknown\";\n navigationType?: \"push\" | \"replace\" | \"traverse\" | \"reload\";\n}\n\nexport function createScrollRestoration(\n router: Router,\n options?: ScrollRestorationOptions,\n): { destroy: () => void } {\n if (typeof globalThis.window === \"undefined\") {\n return NOOP_INSTANCE;\n }\n\n const mode = options?.mode ?? \"restore\";\n\n // mode \"manual\" = utility does nothing. Don't flip history.scrollRestoration,\n // don't subscribe, don't register pagehide — leave the browser's native\n // auto-restore intact for the app to override if it wants to.\n if (mode === \"manual\") {\n return NOOP_INSTANCE;\n }\n\n const anchorEnabled = options?.anchorScrolling ?? true;\n const getContainer = options?.scrollContainer;\n\n const prevScrollRestoration = history.scrollRestoration;\n\n try {\n history.scrollRestoration = \"manual\";\n } catch {\n // Ignore — some embedded contexts may reject the assignment.\n }\n\n // Resolve the container lazily on every event so containers mounted AFTER\n // the provider still get correct scroll handling. Falls back to window when\n // the getter is absent or returns null (pre-mount).\n const readPos = (): number => {\n const element = getContainer?.();\n\n return element ? element.scrollTop : globalThis.scrollY;\n };\n\n const writePos = (top: number): void => {\n const element = getContainer?.();\n\n if (element) {\n element.scrollTop = top;\n } else {\n globalThis.scrollTo(0, top);\n }\n };\n\n const scrollToHashOrTop = (): void => {\n const hash = globalThis.location.hash;\n\n if (anchorEnabled && hash.length > 1) {\n // location.hash is percent-encoded; ids in the DOM are the raw string.\n // Decode for the match. Fall back to the raw slice if the hash contains\n // a malformed escape sequence (decodeURIComponent throws on those).\n let id: string;\n\n try {\n id = decodeURIComponent(hash.slice(1));\n } catch {\n id = hash.slice(1);\n }\n\n // eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars\n const element = document.getElementById(id);\n\n if (element) {\n element.scrollIntoView();\n\n return;\n }\n }\n\n writePos(0);\n };\n\n let destroyed = false;\n\n const unsubscribe = router.subscribe(({ route, previousRoute }) => {\n const nav = (route.context as { navigation?: NavigationContext })\n .navigation;\n\n // Browsers dispatch reload as the initial navigation after refresh, so\n // previousRoute is undefined and capture is naturally skipped. The\n // pre-refresh position was already persisted via pagehide.\n if (previousRoute) {\n putPos(keyOf(previousRoute), readPos());\n }\n\n // Single rAF so DOM is committed before we read anchors / write scroll.\n // Guard against destroy() racing with the callback.\n requestAnimationFrame(() => {\n if (destroyed) {\n return;\n }\n\n if (mode === \"top\" || !nav) {\n scrollToHashOrTop();\n\n return;\n }\n\n if (nav.navigationType === \"replace\") {\n return;\n }\n\n if (\n nav.direction === \"back\" ||\n nav.navigationType === \"traverse\" ||\n nav.navigationType === \"reload\"\n ) {\n writePos(loadStore()[keyOf(route)] ?? 0);\n\n return;\n }\n\n scrollToHashOrTop();\n });\n });\n\n const onPageHide = (): void => {\n const current = router.getState();\n\n if (current) {\n putPos(keyOf(current), readPos());\n }\n };\n\n globalThis.addEventListener(\"pagehide\", onPageHide);\n\n return {\n destroy: () => {\n if (destroyed) {\n return;\n }\n\n destroyed = true;\n unsubscribe();\n globalThis.removeEventListener(\"pagehide\", onPageHide);\n\n try {\n history.scrollRestoration = prevScrollRestoration;\n } catch {\n // Ignore.\n }\n },\n };\n}\n\nfunction keyOf(state: State): string {\n return `${state.name}:${canonicalJson(state.params)}`;\n}\n\nfunction loadStore(): Record<string, number> {\n try {\n const raw = sessionStorage.getItem(STORAGE_KEY);\n\n return raw ? (JSON.parse(raw) as Record<string, number>) : {};\n } catch {\n return {};\n }\n}\n\nfunction putPos(key: string, pos: number): void {\n try {\n const store = loadStore();\n\n store[key] = pos;\n sessionStorage.setItem(STORAGE_KEY, JSON.stringify(store));\n } catch {\n // Ignore quota / security errors.\n }\n}\n\nfunction canonicalJson(value: unknown): string {\n return JSON.stringify(value, canonicalReplacer);\n}\n\nfunction canonicalReplacer(_key: string, val: unknown): unknown {\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n const sorted: Record<string, unknown> = {};\n // eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable\n const keys = Object.keys(val as Record<string, unknown>).sort(\n (left: string, right: string) => left.localeCompare(right),\n );\n\n for (const key of keys) {\n sorted[key] = (val as Record<string, unknown>)[key];\n }\n\n return sorted;\n }\n\n return val;\n}\n","import type { Router, Params } from \"@real-router/core\";\n\nexport function shouldNavigate(evt: MouseEvent): boolean {\n return (\n evt.button === 0 &&\n !evt.metaKey &&\n !evt.altKey &&\n !evt.ctrlKey &&\n !evt.shiftKey\n );\n}\n\ntype BuildUrlFn = (name: string, params: Params) => string | undefined;\n\nexport function buildHref(\n router: Router,\n routeName: string,\n routeParams: Params,\n): string | undefined {\n try {\n const buildUrl = router.buildUrl as BuildUrlFn | undefined;\n\n if (buildUrl) {\n const url = buildUrl(routeName, routeParams);\n\n if (url !== undefined) {\n return url;\n }\n }\n\n return router.buildPath(routeName, routeParams);\n } catch {\n console.error(\n `[real-router] Route \"${routeName}\" is not defined. The element will render without an href attribute.`,\n );\n\n return undefined;\n }\n}\n\nfunction parseTokens(value: string | undefined): string[] {\n return value ? (value.match(/\\S+/g) ?? []) : [];\n}\n\nexport function buildActiveClassName(\n isActive: boolean,\n activeClassName: string | undefined,\n baseClassName: string | undefined,\n): string | undefined {\n if (isActive && activeClassName) {\n const activeTokens = parseTokens(activeClassName);\n\n if (activeTokens.length === 0) {\n return baseClassName ?? undefined;\n }\n if (!baseClassName) {\n return activeTokens.join(\" \");\n }\n\n const baseTokens = parseTokens(baseClassName);\n const seen = new Set(baseTokens);\n\n for (const token of activeTokens) {\n if (!seen.has(token)) {\n seen.add(token);\n baseTokens.push(token);\n }\n }\n\n return baseTokens.join(\" \");\n }\n\n return baseClassName ?? undefined;\n}\n\nexport function shallowEqual(\n prev: object | undefined,\n next: object | undefined,\n): boolean {\n if (Object.is(prev, next)) {\n return true;\n }\n if (!prev || !next) {\n return false;\n }\n\n const prevKeys = Object.keys(prev);\n\n if (prevKeys.length !== Object.keys(next).length) {\n return false;\n }\n\n const prevRecord = prev as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n\n for (const key of prevKeys) {\n if (!Object.is(prevRecord[key], nextRecord[key])) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function applyLinkA11y(element: HTMLElement | null | undefined): void {\n if (!element) {\n return;\n }\n if (\n element instanceof HTMLAnchorElement ||\n element instanceof HTMLButtonElement\n ) {\n return;\n }\n if (!element.hasAttribute(\"role\")) {\n element.setAttribute(\"role\", \"link\");\n }\n if (!element.hasAttribute(\"tabindex\")) {\n element.setAttribute(\"tabindex\", \"0\");\n }\n}\n","import { createActiveRouteSource } from \"@real-router/sources\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { Params } from \"@real-router/core\";\n\nexport function useIsActiveRoute(\n routeName: string,\n params?: Params,\n strict = false,\n ignoreQueryParams = true,\n): boolean {\n const router = useRouter();\n\n // createActiveRouteSource is per-router + canonical-args cached in\n // @real-router/sources, so passing params by reference is safe.\n const store = createActiveRouteSource(router, routeName, params, {\n strict,\n ignoreQueryParams,\n });\n\n return useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n}\n","import { memo } from \"preact/compat\";\nimport { useCallback, useMemo } from \"preact/hooks\";\n\nimport { EMPTY_PARAMS, EMPTY_OPTIONS } from \"../constants\";\nimport {\n shouldNavigate,\n buildHref,\n buildActiveClassName,\n shallowEqual,\n} from \"../dom-utils\";\nimport { useIsActiveRoute } from \"../hooks/useIsActiveRoute\";\nimport { useRouter } from \"../hooks/useRouter\";\n\nimport type { LinkProps } from \"../types\";\nimport type { FunctionComponent, JSX } from \"preact\";\n\nfunction areLinkPropsEqual(\n prev: Readonly<LinkProps>,\n next: Readonly<LinkProps>,\n): boolean {\n return (\n prev.routeName === next.routeName &&\n prev.className === next.className &&\n prev.activeClassName === next.activeClassName &&\n prev.activeStrict === next.activeStrict &&\n prev.ignoreQueryParams === next.ignoreQueryParams &&\n prev.onClick === next.onClick &&\n prev.target === next.target &&\n prev.style === next.style &&\n prev.children === next.children &&\n shallowEqual(prev.routeParams, next.routeParams) &&\n shallowEqual(prev.routeOptions, next.routeOptions)\n );\n}\n\nexport const Link: FunctionComponent<LinkProps> = memo(\n ({\n routeName,\n routeParams = EMPTY_PARAMS,\n routeOptions = EMPTY_OPTIONS,\n className,\n activeClassName = \"active\",\n activeStrict = false,\n ignoreQueryParams = true,\n onClick,\n target,\n children,\n ...props\n }) => {\n const router = useRouter();\n\n // memo + areLinkPropsEqual guarantees that on bail-out the component does\n // not render; on render, routeParams/routeOptions changed reference (true\n // change caught by shallowEqual), so they're safe to use directly in hook\n // deps without useStableValue.\n\n const isActive = useIsActiveRoute(\n routeName,\n routeParams,\n activeStrict,\n ignoreQueryParams,\n );\n\n const href = useMemo(\n () => buildHref(router, routeName, routeParams),\n [router, routeName, routeParams],\n );\n\n const handleClick = useCallback(\n (evt: JSX.TargetedMouseEvent<HTMLAnchorElement>) => {\n if (onClick) {\n onClick(evt);\n\n if (evt.defaultPrevented) {\n return;\n }\n }\n\n if (!shouldNavigate(evt) || target === \"_blank\") {\n return;\n }\n\n evt.preventDefault();\n router.navigate(routeName, routeParams, routeOptions).catch(() => {});\n },\n [onClick, target, router, routeName, routeParams, routeOptions],\n );\n\n const finalClassName = useMemo(\n () => buildActiveClassName(isActive, activeClassName, className),\n [isActive, activeClassName, className],\n );\n\n return (\n <a\n {...props}\n href={href}\n className={finalClassName}\n onClick={handleClick}\n >\n {children}\n </a>\n );\n },\n areLinkPropsEqual,\n);\n\nLink.displayName = \"Link\";\n","import { createDismissableError } from \"@real-router/sources\";\nimport { Fragment } from \"preact\";\nimport { useEffect, useRef } from \"preact/hooks\";\n\nimport { useRouter } from \"../hooks/useRouter\";\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\n\nimport type { RouterError, State } from \"@real-router/core\";\nimport type { ComponentChildren, VNode } from \"preact\";\n\nexport interface RouterErrorBoundaryProps {\n readonly children: ComponentChildren;\n readonly fallback: (\n error: RouterError,\n resetError: () => void,\n ) => ComponentChildren;\n readonly onError?: (\n error: RouterError,\n toRoute: State | null,\n fromRoute: State | null,\n ) => void;\n}\n\nexport function RouterErrorBoundary({\n children,\n fallback,\n onError,\n}: RouterErrorBoundaryProps): VNode {\n const router = useRouter();\n const store = createDismissableError(router);\n const snapshot = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n\n const onErrorRef = useRef(onError);\n\n // eslint-disable-next-line @eslint-react/refs -- \"latest ref\" pattern: sync callback to ref to avoid effect re-runs\n onErrorRef.current = onError;\n\n useEffect(() => {\n if (snapshot.error) {\n onErrorRef.current?.(\n snapshot.error,\n snapshot.toRoute,\n snapshot.fromRoute,\n );\n }\n // eslint-disable-next-line @eslint-react/exhaustive-deps -- onError tracked via ref, snapshot fields accessed inside callback\n }, [snapshot.version]);\n\n return (\n <Fragment>\n {children}\n {snapshot.error ? fallback(snapshot.error, snapshot.resetError) : null}\n </Fragment>\n );\n}\n","import { useContext } from \"preact/hooks\";\n\nimport { NavigatorContext } from \"../context\";\n\nimport type { Navigator } from \"@real-router/core\";\n\nexport const useNavigator = (): Navigator => {\n const navigator = useContext(NavigatorContext);\n\n if (!navigator) {\n throw new Error(\"useNavigator must be used within a RouterProvider\");\n }\n\n return navigator;\n};\n","import { getPluginApi } from \"@real-router/core/api\";\nimport { getRouteUtils } from \"@real-router/route-utils\";\n\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouteUtils } from \"@real-router/route-utils\";\n\nexport const useRouteUtils = (): RouteUtils => {\n const router = useRouter();\n\n return getRouteUtils(getPluginApi(router).getTree());\n};\n","import { useContext } from \"preact/hooks\";\n\nimport { RouteContext } from \"../context\";\n\nimport type { RouteContext as RouteContextType } from \"../types\";\nimport type { Params } from \"@real-router/core\";\n\nexport const useRoute = <P extends Params = Params>(): RouteContextType<P> => {\n const routeContext = useContext(RouteContext);\n\n if (!routeContext) {\n throw new Error(\"useRoute must be used within a RouteProvider\");\n }\n\n return routeContext as RouteContextType<P>;\n};\n","import { getTransitionSource } from \"@real-router/sources\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouterTransitionSnapshot } from \"@real-router/sources\";\n\nexport function useRouterTransition(): RouterTransitionSnapshot {\n const router = useRouter();\n const store = getTransitionSource(router);\n\n return useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n}\n","import { getNavigator } from \"@real-router/core\";\nimport { createRouteSource } from \"@real-router/sources\";\nimport { useEffect, useMemo } from \"preact/hooks\";\n\nimport { NavigatorContext, RouteContext, RouterContext } from \"./context\";\nimport { createRouteAnnouncer, createScrollRestoration } from \"./dom-utils\";\nimport { useSyncExternalStore } from \"./useSyncExternalStore\";\n\nimport type { ScrollRestorationOptions } from \"./dom-utils\";\nimport type { Router } from \"@real-router/core\";\nimport type { FunctionComponent, ComponentChildren } from \"preact\";\n\nexport interface RouteProviderProps {\n router: Router;\n children: ComponentChildren;\n announceNavigation?: boolean;\n scrollRestoration?: ScrollRestorationOptions;\n}\n\nexport const RouterProvider: FunctionComponent<RouteProviderProps> = ({\n router,\n children,\n announceNavigation,\n scrollRestoration,\n}) => {\n useEffect(() => {\n if (!announceNavigation) {\n return;\n }\n\n const announcer = createRouteAnnouncer(router);\n\n return () => {\n announcer.destroy();\n };\n }, [announceNavigation, router]);\n\n // Primitive deps so inline `{ mode: \"restore\" }` doesn't thrash on every\n // render. scrollContainer is a getter invoked lazily on every event inside\n // the utility — swapping its reference doesn't change the resolved element,\n // so we intentionally omit it from deps to keep inline getters stable.\n const srMode = scrollRestoration?.mode;\n const srAnchor = scrollRestoration?.anchorScrolling;\n const srEnabled = scrollRestoration !== undefined;\n\n useEffect(() => {\n if (!srEnabled) {\n return;\n }\n\n const sr = createScrollRestoration(router, {\n mode: srMode,\n anchorScrolling: srAnchor,\n // srEnabled check above guarantees scrollRestoration is defined.\n scrollContainer: scrollRestoration.scrollContainer,\n });\n\n return () => {\n sr.destroy();\n };\n // scrollRestoration (for scrollContainer) omitted — see comment above.\n // eslint-disable-next-line @eslint-react/exhaustive-deps\n }, [router, srEnabled, srMode, srAnchor]);\n const navigator = useMemo(() => getNavigator(router), [router]);\n\n // useSyncExternalStore manages the router subscription lifecycle:\n // subscribe connects to router on first listener, unsubscribes on last.\n const store = useMemo(() => createRouteSource(router), [router]);\n const { route, previousRoute } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot, // SSR: router returns same state on server and client\n );\n\n const routeContextValue = useMemo(\n () => ({ navigator, route, previousRoute }),\n [navigator, route, previousRoute],\n );\n\n return (\n <RouterContext.Provider value={router}>\n <NavigatorContext.Provider value={navigator}>\n <RouteContext.Provider value={routeContextValue}>\n {children}\n </RouteContext.Provider>\n </NavigatorContext.Provider>\n </RouterContext.Provider>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["NOOP_INSTANCE"],"sources":["../../src/components/RouteView/components.tsx","../../src/components/RouteView/helpers.tsx","../../src/useSyncExternalStore.ts","../../src/context.ts","../../src/hooks/useRouter.tsx","../../src/hooks/useRouteNode.tsx","../../src/components/RouteView/RouteView.tsx","../../src/constants.ts","../../../../shared/dom-utils/route-announcer.ts","../../../../shared/dom-utils/scroll-restore.ts","../../../../shared/dom-utils/view-transitions.ts","../../../../shared/dom-utils/link-utils.ts","../../src/hooks/useIsActiveRoute.tsx","../../src/components/Link.tsx","../../src/components/RouterErrorBoundary.tsx","../../src/hooks/useNavigator.tsx","../../src/hooks/useRouteUtils.tsx","../../src/hooks/useRoute.tsx","../../src/hooks/useRouterTransition.tsx","../../src/hooks/useRouteExit.tsx","../../src/hooks/useRouteEnter.tsx","../../src/RouterProvider.tsx"],"sourcesContent":["import type { MatchProps, NotFoundProps, SelfProps } from \"./types\";\n\nexport function Match(_props: MatchProps): null {\n return null;\n}\n\nMatch.displayName = \"RouteView.Match\";\n\nexport function Self(_props: SelfProps): null {\n return null;\n}\n\nSelf.displayName = \"RouteView.Self\";\n\nexport function NotFound(_props: NotFoundProps): null {\n return null;\n}\n\nNotFound.displayName = \"RouteView.NotFound\";\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\nimport { startsWithSegment } from \"@real-router/route-utils\";\nimport { Fragment, isValidElement, toChildArray } from \"preact\";\nimport { Suspense } from \"preact/compat\";\n\nimport { Match, NotFound, Self } from \"./components\";\n\nimport type { MatchProps, NotFoundProps, SelfProps } from \"./types\";\nimport type { VNode, ComponentChildren } from \"preact\";\n\ninterface FallbackSlots {\n selfChildren: ComponentChildren;\n selfFallback: ComponentChildren | undefined;\n selfFound: boolean;\n notFoundChildren: ComponentChildren;\n}\n\nfunction isSegmentMatch(\n routeName: string,\n fullSegmentName: string,\n exact: boolean,\n): boolean {\n if (fullSegmentName === \"\") {\n return false;\n }\n\n if (exact) {\n return routeName === fullSegmentName;\n }\n\n return startsWithSegment(routeName, fullSegmentName);\n}\n\nexport function collectElements(\n children: ComponentChildren,\n result: VNode[],\n): void {\n for (const child of toChildArray(children)) {\n if (!isValidElement(child)) {\n continue;\n }\n\n if (\n child.type === Match ||\n child.type === Self ||\n child.type === NotFound\n ) {\n result.push(child);\n } else {\n collectElements(\n (child.props as { readonly children: ComponentChildren }).children,\n result,\n );\n }\n }\n}\n\nfunction renderSlot(\n slotChildren: ComponentChildren,\n key: string,\n fallback?: ComponentChildren,\n): VNode {\n const content =\n fallback === undefined ? (\n slotChildren\n ) : (\n <Suspense fallback={fallback}>{slotChildren}</Suspense>\n );\n\n return <Fragment key={key}>{content}</Fragment>;\n}\n\nfunction recordFallback(child: VNode, slots: FallbackSlots): boolean {\n if (child.type === NotFound) {\n slots.notFoundChildren = (child.props as NotFoundProps).children;\n\n return true;\n }\n\n if (child.type === Self) {\n if (!slots.selfFound) {\n slots.selfChildren = (child.props as SelfProps).children;\n slots.selfFallback = (child.props as SelfProps).fallback;\n slots.selfFound = true;\n }\n\n return true;\n }\n\n return false;\n}\n\nfunction processMatch(\n child: VNode,\n routeName: string,\n nodeName: string,\n alreadyActive: boolean,\n): VNode | null {\n const { segment, exact = false, fallback } = child.props as MatchProps;\n const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;\n const isActive =\n !alreadyActive && isSegmentMatch(routeName, fullSegmentName, exact);\n\n if (!isActive) {\n return null;\n }\n\n return renderSlot(\n (child.props as MatchProps).children,\n fullSegmentName,\n fallback,\n );\n}\n\nfunction appendFallback(\n rendered: VNode[],\n routeName: string,\n nodeName: string,\n slots: FallbackSlots,\n): void {\n if (slots.selfFound && routeName === nodeName) {\n rendered.push(\n renderSlot(slots.selfChildren, \"__route-view-self__\", slots.selfFallback),\n );\n\n return;\n }\n\n if (routeName === UNKNOWN_ROUTE && slots.notFoundChildren !== null) {\n rendered.push(\n <Fragment key=\"__route-view-not-found__\">\n {slots.notFoundChildren}\n </Fragment>,\n );\n }\n}\n\nexport function buildRenderList(\n elements: VNode[],\n routeName: string,\n nodeName: string,\n): { rendered: VNode[]; activeMatchFound: boolean } {\n const slots: FallbackSlots = {\n selfChildren: null,\n selfFallback: undefined,\n selfFound: false,\n notFoundChildren: null,\n };\n let activeMatchFound = false;\n const rendered: VNode[] = [];\n\n for (const child of elements) {\n if (recordFallback(child, slots)) {\n continue;\n }\n\n const matchRendered = processMatch(\n child,\n routeName,\n nodeName,\n activeMatchFound,\n );\n\n if (matchRendered !== null) {\n activeMatchFound = true;\n rendered.push(matchRendered);\n }\n }\n\n if (!activeMatchFound) {\n appendFallback(rendered, routeName, nodeName, slots);\n }\n\n return { rendered, activeMatchFound };\n}\n","import { useEffect, useState } from \"preact/hooks\";\n\n/**\n * Polyfill for React's useSyncExternalStore.\n *\n * Preact does not provide a native useSyncExternalStore.\n * This implementation uses useState + useEffect to subscribe\n * to external stores.\n *\n * Race condition handling: the value may change between\n * `useState(getSnapshot)` (render) and `useEffect` (commit).\n * We synchronize by calling `setValue(getSnapshot())` before\n * subscribing in the effect.\n *\n * The updater uses `Object.is` to bail out when the snapshot\n * is referentially stable, preventing redundant re-renders.\n */\nexport function useSyncExternalStore<T>(\n subscribe: (onStoreChange: () => void) => () => void,\n getSnapshot: () => T,\n _getServerSnapshot?: () => T,\n): T {\n const [value, setValue] = useState(getSnapshot);\n\n useEffect(() => {\n const sync = (): void => {\n setValue((prev) => {\n const next = getSnapshot();\n\n return Object.is(prev, next) ? prev : next;\n });\n };\n\n sync();\n\n return subscribe(sync);\n }, [subscribe, getSnapshot]);\n\n return value;\n}\n","import { createContext } from \"preact\";\n\nimport type { RouteContext as RouteContextType } from \"./types\";\nimport type { Router, Navigator } from \"@real-router/core\";\n\nexport const RouteContext = createContext<RouteContextType | null>(null);\n\nexport const RouterContext = createContext<Router | null>(null);\n\nexport const NavigatorContext = createContext<Navigator | null>(null);\n","import { useContext } from \"preact/hooks\";\n\nimport { RouterContext } from \"../context\";\n\nimport type { Router } from \"@real-router/core\";\n\nexport const useRouter = (): Router => {\n const router = useContext(RouterContext);\n\n if (!router) {\n throw new Error(\"useRouter must be used within a RouterProvider\");\n }\n\n return router;\n};\n","import { getNavigator } from \"@real-router/core\";\nimport { createRouteNodeSource } from \"@real-router/sources\";\nimport { useMemo } from \"preact/hooks\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouteContext } from \"../types\";\n\nexport function useRouteNode(nodeName: string): RouteContext {\n const router = useRouter();\n\n const store = useMemo(\n () => createRouteNodeSource(router, nodeName),\n [router, nodeName],\n );\n\n const { route, previousRoute } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n\n const navigator = useMemo(() => getNavigator(router), [router]);\n\n return useMemo(\n (): RouteContext => ({ navigator, route, previousRoute }),\n [navigator, route, previousRoute],\n );\n}\n","import { useMemo } from \"preact/hooks\";\n\nimport { Match, NotFound, Self } from \"./components\";\nimport { buildRenderList, collectElements } from \"./helpers\";\nimport { useRouteNode } from \"../../hooks/useRouteNode\";\n\nimport type { RouteViewProps } from \"./types\";\nimport type { VNode } from \"preact\";\n\nfunction RouteViewRoot({\n nodeName,\n children,\n}: Readonly<RouteViewProps>): VNode | null {\n const { route } = useRouteNode(nodeName);\n\n // Cache the flattened Match/Self/NotFound list across renders with unchanged\n // children. children only differs when the parent re-renders with a new\n // node, so this memoises the steady-state traversal.\n const elements = useMemo(() => {\n const collected: VNode[] = [];\n\n collectElements(children, collected);\n\n return collected;\n }, [children]);\n\n if (!route) {\n return null;\n }\n\n const { rendered } = buildRenderList(elements, route.name, nodeName);\n\n if (rendered.length > 0) {\n return <>{rendered}</>;\n }\n\n return null;\n}\n\nRouteViewRoot.displayName = \"RouteView\";\n\nexport const RouteView = Object.assign(RouteViewRoot, {\n Match,\n Self,\n NotFound,\n});\n\nexport type {\n RouteViewProps,\n MatchProps as RouteViewMatchProps,\n SelfProps as RouteViewSelfProps,\n NotFoundProps as RouteViewNotFoundProps,\n} from \"./types\";\n","/**\n * Stable empty object for default params\n */\nexport const EMPTY_PARAMS = Object.freeze({});\n\n/**\n * Stable empty options object\n */\nexport const EMPTY_OPTIONS = Object.freeze({});\n","import type { Router, State } from \"@real-router/core\";\n\nconst CLEAR_DELAY = 7000;\nconst SAFARI_READY_DELAY = 100;\nconst ANNOUNCER_ATTR = \"data-real-router-announcer\";\nconst INTERNAL_ROUTE_PREFIX = \"@@\";\nconst VISUALLY_HIDDEN =\n \"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0\";\n\nexport interface RouteAnnouncerOptions {\n prefix?: string;\n getAnnouncementText?: (route: State) => string;\n}\n\nexport function createRouteAnnouncer(\n router: Router,\n options?: RouteAnnouncerOptions,\n): { destroy: () => void } {\n const prefix = options?.prefix ?? \"Navigated to \";\n const getCustomText = options?.getAnnouncementText;\n\n let isInitialNavigation = true;\n let isReady = false;\n let isDestroyed = false;\n let lastAnnouncedText = \"\";\n let pendingText: string | null = null;\n let clearTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const announcer = getOrCreateAnnouncer();\n\n const doAnnounce = (text: string, h1: HTMLElement | null): void => {\n lastAnnouncedText = text;\n clearTimeout(clearTimeoutId);\n announcer.textContent = text;\n clearTimeoutId = setTimeout(() => {\n announcer.textContent = \"\";\n lastAnnouncedText = \"\";\n }, CLEAR_DELAY);\n\n manageFocus(h1);\n };\n\n // Safari-ready delay: announcing before VoiceOver wires up the aria-live region\n // causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms\n // before marking the announcer \"ready\" — any navigation during that window is\n // buffered in pendingText and flushed once the delay expires.\n const safariTimeoutId = setTimeout(() => {\n isReady = true;\n\n if (pendingText !== null && !isDestroyed) {\n const text = pendingText;\n\n pendingText = null;\n doAnnounce(text, document.querySelector<HTMLElement>(\"h1\"));\n }\n }, SAFARI_READY_DELAY);\n\n const unsubscribe = router.subscribe(({ route }) => {\n if (isInitialNavigation) {\n isInitialNavigation = false;\n\n return;\n }\n\n // Double rAF: waits for two paint frames so the incoming route's DOM\n // (including the new <h1>) is fully rendered before resolveText reads it.\n // Single rAF fires before the new route's template has been attached,\n // which would cause resolveText to pick up the OLD h1 or fall back to\n // document.title / route.name prematurely.\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (isDestroyed) {\n return;\n }\n\n const h1 = document.querySelector<HTMLElement>(\"h1\");\n const text = resolveText(route, prefix, getCustomText, h1);\n\n if (!text || text === lastAnnouncedText) {\n return;\n }\n\n if (!isReady) {\n // Defer announcement until Safari-ready window elapses (see safariTimeoutId).\n pendingText = text;\n\n return;\n }\n\n doAnnounce(text, h1);\n });\n });\n });\n\n return {\n destroy() {\n isDestroyed = true;\n unsubscribe();\n clearTimeout(clearTimeoutId);\n clearTimeout(safariTimeoutId);\n removeAnnouncer();\n },\n };\n}\n\nfunction getOrCreateAnnouncer(): HTMLElement {\n const existing = document.querySelector<HTMLElement>(`[${ANNOUNCER_ATTR}]`);\n\n if (existing) {\n return existing;\n }\n\n const element = document.createElement(\"div\");\n\n element.setAttribute(\"style\", VISUALLY_HIDDEN);\n element.setAttribute(\"aria-live\", \"assertive\");\n element.setAttribute(\"aria-atomic\", \"true\");\n element.setAttribute(ANNOUNCER_ATTR, \"\");\n\n document.body.prepend(element);\n\n return element;\n}\n\nfunction removeAnnouncer(): void {\n document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();\n}\n\nfunction resolveText(\n route: State,\n prefix: string,\n getCustomText: ((route: State) => string) | undefined,\n h1: HTMLElement | null,\n): string {\n if (getCustomText) {\n return getCustomText(route);\n }\n\n const h1Text = (h1?.textContent ?? \"\").trim();\n const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)\n ? \"\"\n : route.name;\n const rawText =\n h1Text || document.title || routeName || globalThis.location.pathname;\n\n return `${prefix}${rawText}`;\n}\n\nfunction manageFocus(h1: HTMLElement | null): void {\n if (!h1) {\n return;\n }\n\n if (!h1.hasAttribute(\"tabindex\")) {\n h1.setAttribute(\"tabindex\", \"-1\");\n }\n\n h1.focus({ preventScroll: true });\n}\n","import type { Router, State } from \"@real-router/core\";\n\nconst STORAGE_KEY = \"real-router:scroll\";\n\nconst NOOP_INSTANCE: { destroy: () => void } = Object.freeze({\n destroy: () => {\n /* no-op */\n },\n});\n\nexport type ScrollRestorationMode = \"restore\" | \"top\" | \"manual\";\n\nexport interface ScrollRestorationOptions {\n mode?: ScrollRestorationMode | undefined;\n anchorScrolling?: boolean | undefined;\n scrollContainer?: (() => HTMLElement | null) | undefined;\n}\n\ninterface NavigationContext {\n direction?: \"forward\" | \"back\" | \"unknown\";\n navigationType?: \"push\" | \"replace\" | \"traverse\" | \"reload\";\n}\n\nexport function createScrollRestoration(\n router: Router,\n options?: ScrollRestorationOptions,\n): { destroy: () => void } {\n if (typeof globalThis.window === \"undefined\") {\n return NOOP_INSTANCE;\n }\n\n const mode = options?.mode ?? \"restore\";\n\n // mode \"manual\" = utility does nothing. Don't flip history.scrollRestoration,\n // don't subscribe, don't register pagehide — leave the browser's native\n // auto-restore intact for the app to override if it wants to.\n if (mode === \"manual\") {\n return NOOP_INSTANCE;\n }\n\n const anchorEnabled = options?.anchorScrolling ?? true;\n const getContainer = options?.scrollContainer;\n\n const prevScrollRestoration = history.scrollRestoration;\n\n try {\n history.scrollRestoration = \"manual\";\n } catch {\n // Ignore — some embedded contexts may reject the assignment.\n }\n\n // Resolve the container lazily on every event so containers mounted AFTER\n // the provider still get correct scroll handling. Falls back to window when\n // the getter is absent or returns null (pre-mount).\n const readPos = (): number => {\n const element = getContainer?.();\n\n return element ? element.scrollTop : globalThis.scrollY;\n };\n\n const writePos = (top: number): void => {\n const element = getContainer?.();\n\n if (element) {\n element.scrollTop = top;\n } else {\n globalThis.scrollTo(0, top);\n }\n };\n\n const scrollToHashOrTop = (): void => {\n const hash = globalThis.location.hash;\n\n if (anchorEnabled && hash.length > 1) {\n // location.hash is percent-encoded; ids in the DOM are the raw string.\n // Decode for the match. Fall back to the raw slice if the hash contains\n // a malformed escape sequence (decodeURIComponent throws on those).\n let id: string;\n\n try {\n id = decodeURIComponent(hash.slice(1));\n } catch {\n id = hash.slice(1);\n }\n\n // eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars\n const element = document.getElementById(id);\n\n if (element) {\n element.scrollIntoView();\n\n return;\n }\n }\n\n writePos(0);\n };\n\n let destroyed = false;\n\n const unsubscribe = router.subscribe(({ route, previousRoute }) => {\n const nav = (route.context as { navigation?: NavigationContext })\n .navigation;\n\n // Browsers dispatch reload as the initial navigation after refresh, so\n // previousRoute is undefined and capture is naturally skipped. The\n // pre-refresh position was already persisted via pagehide.\n if (previousRoute) {\n putPos(keyOf(previousRoute), readPos());\n }\n\n // Single rAF so DOM is committed before we read anchors / write scroll.\n // Guard against destroy() racing with the callback.\n requestAnimationFrame(() => {\n if (destroyed) {\n return;\n }\n\n if (mode === \"top\" || !nav) {\n scrollToHashOrTop();\n\n return;\n }\n\n if (nav.navigationType === \"replace\") {\n return;\n }\n\n if (\n nav.direction === \"back\" ||\n nav.navigationType === \"traverse\" ||\n nav.navigationType === \"reload\"\n ) {\n writePos(loadStore()[keyOf(route)] ?? 0);\n\n return;\n }\n\n scrollToHashOrTop();\n });\n });\n\n const onPageHide = (): void => {\n const current = router.getState();\n\n if (current) {\n putPos(keyOf(current), readPos());\n }\n };\n\n globalThis.addEventListener(\"pagehide\", onPageHide);\n\n return {\n destroy: () => {\n if (destroyed) {\n return;\n }\n\n destroyed = true;\n unsubscribe();\n globalThis.removeEventListener(\"pagehide\", onPageHide);\n\n try {\n history.scrollRestoration = prevScrollRestoration;\n } catch {\n // Ignore.\n }\n },\n };\n}\n\nfunction keyOf(state: State): string {\n return `${state.name}:${canonicalJson(state.params)}`;\n}\n\nfunction loadStore(): Record<string, number> {\n try {\n const raw = sessionStorage.getItem(STORAGE_KEY);\n\n return raw ? (JSON.parse(raw) as Record<string, number>) : {};\n } catch {\n return {};\n }\n}\n\nfunction putPos(key: string, pos: number): void {\n try {\n const store = loadStore();\n\n store[key] = pos;\n sessionStorage.setItem(STORAGE_KEY, JSON.stringify(store));\n } catch {\n // Ignore quota / security errors.\n }\n}\n\nfunction canonicalJson(value: unknown): string {\n return JSON.stringify(value, canonicalReplacer);\n}\n\nfunction canonicalReplacer(_key: string, val: unknown): unknown {\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n const sorted: Record<string, unknown> = {};\n // eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable\n const keys = Object.keys(val as Record<string, unknown>).sort(\n (left: string, right: string) => left.localeCompare(right),\n );\n\n for (const key of keys) {\n sorted[key] = (val as Record<string, unknown>)[key];\n }\n\n return sorted;\n }\n\n return val;\n}\n","import type { Router } from \"@real-router/core\";\n\nexport interface ViewTransitions {\n destroy: () => void;\n}\n\nconst NOOP_INSTANCE: ViewTransitions = Object.freeze({\n destroy: () => {\n /* no-op */\n },\n});\n\nexport function createViewTransitions(router: Router): ViewTransitions {\n if (\n typeof document === \"undefined\" ||\n typeof document.startViewTransition !== \"function\"\n ) {\n return NOOP_INSTANCE;\n }\n\n let closeVT: (() => void) | null = null;\n let currentVT: { skipTransition?: () => void } | null = null;\n // Tracks whether TRANSITION_SUCCESS fired for the current leave. Used to\n // distinguish \"benign cleanup abort\" (router's async path aborts its own\n // controller in a finally block after successful navigation) from \"real\n // cancellation\" (concurrent navigate, guard rejection, dispose).\n let successFired = false;\n\n const resolveAndClear = (): void => {\n closeVT?.();\n closeVT = null;\n };\n\n const offLeave = router.subscribeLeave(({ signal }) => {\n // Reentrant abort: signal already aborted when we're called. Open no VT\n // — router will fall through to TRANSITION_CANCELLED via isCurrentNav()\n // after leave resolves. addEventListener(\"abort\", ...) does not re-fire\n // for past events, so skipping startViewTransition is the safe path.\n if (signal.aborted) {\n return;\n }\n\n successFired = false;\n resolveAndClear();\n\n // Return a Promise so the router awaits until the browser invokes\n // updateCallback. This ensures old DOM snapshot is captured BEFORE the\n // router commits the new state — giving correct exit→state→entry\n // ordering (vs fire-and-forget, where URL changes before VT captures).\n return new Promise<void>((resolveLeave) => {\n // Capture the resolver synchronously BEFORE startViewTransition() is\n // called. The browser invokes updateCallback in a later task, but\n // router.subscribe (TRANSITION_SUCCESS) can fire before that. If we\n // captured `resolve` inside the callback, subscribe would see closeVT\n // still null and skip resolving — the deferred would hang for 4s\n // until the VT API aborts with TimeoutError.\n const deferred = new Promise<void>((resolve) => {\n closeVT = resolve;\n });\n\n signal.addEventListener(\n \"abort\",\n () => {\n if (successFired) {\n // Router's async path (#finishAsyncNavigation) aborts its own\n // controller in a finally block AFTER completeTransition (and\n // thus AFTER subscribe fired). This is cleanup, not\n // cancellation — VT is progressing normally, do nothing.\n return;\n }\n\n // Real cancellation (concurrent navigate, dispose). Resolve the\n // deferred so updateCallback can complete, skip the VT so no\n // stale animation leaks, and unblock the router if the abort\n // fires before updateCallback was invoked.\n resolveAndClear();\n currentVT?.skipTransition?.();\n resolveLeave();\n },\n { once: true },\n );\n\n try {\n currentVT = document.startViewTransition(() => {\n // Resolving here unblocks the router at the moment the browser\n // enters updateCallback — by spec, old DOM snapshot is captured\n // before this callback runs. Router now proceeds through\n // activation guards and setState; the VT animation waits on\n // `deferred`, which is resolved from router.subscribe after a\n // task-queue tick (see NOTE on setTimeout below).\n resolveLeave();\n\n return deferred;\n });\n } catch {\n // Defensive: spec says startViewTransition doesn't throw under\n // normal conditions, but Chromium has had edge cases (detached\n // document, extension interference). Clean up and unblock router.\n resolveAndClear();\n resolveLeave();\n }\n });\n });\n\n const offSuccess = router.subscribe(() => {\n const resolver = closeVT;\n\n successFired = true;\n closeVT = null;\n\n if (resolver === null) {\n currentVT = null;\n } else {\n // CRITICAL: CANNOT use requestAnimationFrame here. When the router\n // takes the async path (leave returned a Promise), subscribe fires\n // AFTER the browser has already transitioned VT into the\n // \"update-callback-called\" phase. In that phase Chromium sets\n // rendering suppression to true, which ALSO blocks rAF callbacks.\n // rAF would never fire → deferred never resolves → browser aborts\n // vt.ready with TimeoutError after 4s (observed in Chromium).\n //\n // setTimeout runs on the task queue independent of the rendering\n // pipeline, so it fires regardless of suppression. React's scheduler\n // uses MessageChannel tasks, which are queued before our setTimeout,\n // so the new DOM is committed by the time our callback runs.\n setTimeout(() => {\n resolver();\n currentVT = null;\n }, 0);\n }\n });\n\n return {\n destroy: () => {\n offLeave();\n offSuccess();\n currentVT?.skipTransition?.();\n currentVT = null;\n resolveAndClear();\n },\n };\n}\n","import type { Router, Params } from \"@real-router/core\";\n\nexport function shouldNavigate(evt: MouseEvent): boolean {\n return (\n evt.button === 0 &&\n !evt.metaKey &&\n !evt.altKey &&\n !evt.ctrlKey &&\n !evt.shiftKey\n );\n}\n\ntype BuildUrlFn = (name: string, params: Params) => string | undefined;\n\nexport function buildHref(\n router: Router,\n routeName: string,\n routeParams: Params,\n): string | undefined {\n try {\n const buildUrl = router.buildUrl as BuildUrlFn | undefined;\n\n if (buildUrl) {\n const url = buildUrl(routeName, routeParams);\n\n if (url !== undefined) {\n return url;\n }\n }\n\n return router.buildPath(routeName, routeParams);\n } catch {\n console.error(\n `[real-router] Route \"${routeName}\" is not defined. The element will render without an href attribute.`,\n );\n\n return undefined;\n }\n}\n\nfunction parseTokens(value: string | undefined): string[] {\n return value ? (value.match(/\\S+/g) ?? []) : [];\n}\n\nexport function buildActiveClassName(\n isActive: boolean,\n activeClassName: string | undefined,\n baseClassName: string | undefined,\n): string | undefined {\n if (isActive && activeClassName) {\n const activeTokens = parseTokens(activeClassName);\n\n if (activeTokens.length === 0) {\n return baseClassName ?? undefined;\n }\n if (!baseClassName) {\n return activeTokens.join(\" \");\n }\n\n const baseTokens = parseTokens(baseClassName);\n const seen = new Set(baseTokens);\n\n for (const token of activeTokens) {\n if (!seen.has(token)) {\n seen.add(token);\n baseTokens.push(token);\n }\n }\n\n return baseTokens.join(\" \");\n }\n\n return baseClassName ?? undefined;\n}\n\nexport function shallowEqual(\n prev: object | undefined,\n next: object | undefined,\n): boolean {\n if (Object.is(prev, next)) {\n return true;\n }\n if (!prev || !next) {\n return false;\n }\n\n const prevKeys = Object.keys(prev);\n\n if (prevKeys.length !== Object.keys(next).length) {\n return false;\n }\n\n const prevRecord = prev as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n\n for (const key of prevKeys) {\n if (!Object.is(prevRecord[key], nextRecord[key])) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function applyLinkA11y(element: HTMLElement | null | undefined): void {\n if (!element) {\n return;\n }\n if (\n element instanceof HTMLAnchorElement ||\n element instanceof HTMLButtonElement\n ) {\n return;\n }\n if (!element.hasAttribute(\"role\")) {\n element.setAttribute(\"role\", \"link\");\n }\n if (!element.hasAttribute(\"tabindex\")) {\n element.setAttribute(\"tabindex\", \"0\");\n }\n}\n","import { createActiveRouteSource } from \"@real-router/sources\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { Params } from \"@real-router/core\";\n\nexport function useIsActiveRoute(\n routeName: string,\n params?: Params,\n strict = false,\n ignoreQueryParams = true,\n): boolean {\n const router = useRouter();\n\n // createActiveRouteSource is per-router + canonical-args cached in\n // @real-router/sources, so passing params by reference is safe.\n const store = createActiveRouteSource(router, routeName, params, {\n strict,\n ignoreQueryParams,\n });\n\n return useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n}\n","import { memo } from \"preact/compat\";\nimport { useCallback, useMemo } from \"preact/hooks\";\n\nimport { EMPTY_PARAMS, EMPTY_OPTIONS } from \"../constants\";\nimport {\n shouldNavigate,\n buildHref,\n buildActiveClassName,\n shallowEqual,\n} from \"../dom-utils\";\nimport { useIsActiveRoute } from \"../hooks/useIsActiveRoute\";\nimport { useRouter } from \"../hooks/useRouter\";\n\nimport type { LinkProps } from \"../types\";\nimport type { FunctionComponent, JSX } from \"preact\";\n\nfunction areLinkPropsEqual(\n prev: Readonly<LinkProps>,\n next: Readonly<LinkProps>,\n): boolean {\n return (\n prev.routeName === next.routeName &&\n prev.className === next.className &&\n prev.activeClassName === next.activeClassName &&\n prev.activeStrict === next.activeStrict &&\n prev.ignoreQueryParams === next.ignoreQueryParams &&\n prev.onClick === next.onClick &&\n prev.target === next.target &&\n prev.style === next.style &&\n prev.children === next.children &&\n shallowEqual(prev.routeParams, next.routeParams) &&\n shallowEqual(prev.routeOptions, next.routeOptions)\n );\n}\n\nexport const Link: FunctionComponent<LinkProps> = memo(\n ({\n routeName,\n routeParams = EMPTY_PARAMS,\n routeOptions = EMPTY_OPTIONS,\n className,\n activeClassName = \"active\",\n activeStrict = false,\n ignoreQueryParams = true,\n onClick,\n target,\n children,\n ...props\n }) => {\n const router = useRouter();\n\n // memo + areLinkPropsEqual guarantees that on bail-out the component does\n // not render; on render, routeParams/routeOptions changed reference (true\n // change caught by shallowEqual), so they're safe to use directly in hook\n // deps without useStableValue.\n\n const isActive = useIsActiveRoute(\n routeName,\n routeParams,\n activeStrict,\n ignoreQueryParams,\n );\n\n const href = useMemo(\n () => buildHref(router, routeName, routeParams),\n [router, routeName, routeParams],\n );\n\n const handleClick = useCallback(\n (evt: JSX.TargetedMouseEvent<HTMLAnchorElement>) => {\n if (onClick) {\n onClick(evt);\n\n if (evt.defaultPrevented) {\n return;\n }\n }\n\n if (!shouldNavigate(evt) || target === \"_blank\") {\n return;\n }\n\n evt.preventDefault();\n router.navigate(routeName, routeParams, routeOptions).catch(() => {});\n },\n [onClick, target, router, routeName, routeParams, routeOptions],\n );\n\n const finalClassName = useMemo(\n () => buildActiveClassName(isActive, activeClassName, className),\n [isActive, activeClassName, className],\n );\n\n return (\n <a\n {...props}\n href={href}\n className={finalClassName}\n onClick={handleClick}\n >\n {children}\n </a>\n );\n },\n areLinkPropsEqual,\n);\n\nLink.displayName = \"Link\";\n","import { createDismissableError } from \"@real-router/sources\";\nimport { Fragment } from \"preact\";\nimport { useEffect, useRef } from \"preact/hooks\";\n\nimport { useRouter } from \"../hooks/useRouter\";\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\n\nimport type { RouterError, State } from \"@real-router/core\";\nimport type { ComponentChildren, VNode } from \"preact\";\n\nexport interface RouterErrorBoundaryProps {\n readonly children: ComponentChildren;\n readonly fallback: (\n error: RouterError,\n resetError: () => void,\n ) => ComponentChildren;\n readonly onError?: (\n error: RouterError,\n toRoute: State | null,\n fromRoute: State | null,\n ) => void;\n}\n\nexport function RouterErrorBoundary({\n children,\n fallback,\n onError,\n}: RouterErrorBoundaryProps): VNode {\n const router = useRouter();\n const store = createDismissableError(router);\n const snapshot = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n\n const onErrorRef = useRef(onError);\n\n // eslint-disable-next-line @eslint-react/refs -- \"latest ref\" pattern: sync callback to ref to avoid effect re-runs\n onErrorRef.current = onError;\n\n useEffect(() => {\n if (snapshot.error) {\n onErrorRef.current?.(\n snapshot.error,\n snapshot.toRoute,\n snapshot.fromRoute,\n );\n }\n // eslint-disable-next-line @eslint-react/exhaustive-deps -- onError tracked via ref, snapshot fields accessed inside callback\n }, [snapshot.version]);\n\n return (\n <Fragment>\n {children}\n {snapshot.error ? fallback(snapshot.error, snapshot.resetError) : null}\n </Fragment>\n );\n}\n","import { useContext } from \"preact/hooks\";\n\nimport { NavigatorContext } from \"../context\";\n\nimport type { Navigator } from \"@real-router/core\";\n\nexport const useNavigator = (): Navigator => {\n const navigator = useContext(NavigatorContext);\n\n if (!navigator) {\n throw new Error(\"useNavigator must be used within a RouterProvider\");\n }\n\n return navigator;\n};\n","import { getPluginApi } from \"@real-router/core/api\";\nimport { getRouteUtils } from \"@real-router/route-utils\";\n\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouteUtils } from \"@real-router/route-utils\";\n\nexport const useRouteUtils = (): RouteUtils => {\n const router = useRouter();\n\n return getRouteUtils(getPluginApi(router).getTree());\n};\n","import { useContext } from \"preact/hooks\";\n\nimport { RouteContext } from \"../context\";\n\nimport type { RouteContext as RouteContextType } from \"../types\";\nimport type { Params, State } from \"@real-router/core\";\n\nexport const useRoute = <P extends Params = Params>(): Omit<\n RouteContextType<P>,\n \"route\"\n> & { route: State<P> } => {\n const routeContext = useContext(RouteContext);\n\n if (!routeContext) {\n throw new Error(\"useRoute must be used within a RouterProvider\");\n }\n\n if (!routeContext.route) {\n throw new Error(\n \"useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?\",\n );\n }\n\n return routeContext as Omit<RouteContextType<P>, \"route\"> & {\n route: State<P>;\n };\n};\n","import { getTransitionSource } from \"@real-router/sources\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouterTransitionSnapshot } from \"@real-router/sources\";\n\nexport function useRouterTransition(): RouterTransitionSnapshot {\n const router = useRouter();\n const store = getTransitionSource(router);\n\n return useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n}\n","import { useEffect, useLayoutEffect, useRef } from \"preact/hooks\";\n\nimport { useRouter } from \"./useRouter\";\n\nimport type { State } from \"@real-router/core\";\n\nexport interface RouteExitContext {\n /** The route being left. */\n route: State;\n /** The route being navigated to. */\n nextRoute: State;\n /**\n * AbortSignal that fires when this navigation is superseded by a later\n * one (rapid clicks). Already filtered: when the handler runs,\n * `signal.aborted` is guaranteed to be `false`. Use\n * `signal.addEventListener(\"abort\", cleanup, { once: true })` for\n * cleanup that must run on cancellation.\n */\n signal: AbortSignal;\n}\n\nexport interface UseRouteExitOptions {\n /**\n * Skip the handler when `route.name === nextRoute.name`\n * (sort/filter/query-only navigations on the same route). Default:\n * `true`.\n */\n skipSameRoute?: boolean;\n}\n\nexport type RouteExitHandler = (\n context: RouteExitContext,\n) => void | Promise<void>;\n\n/**\n * Subscribe to the router's leave-window with the universal guards baked\n * in. Wraps `router.subscribeLeave` so consumers don't repeat the same\n * boilerplate every time:\n *\n * - **Reentrant abort pre-check**: if `signal.aborted` is already `true`\n * when the handler would run (rapid navigation superseded a slower\n * one), the handler is skipped entirely. `signal.addEventListener(\n * \"abort\", ...)` does not fire retroactively, so without this guard\n * downstream cleanup would never trigger.\n * - **Same-route skip**: by default, `route.name === nextRoute.name`\n * short-circuits the handler — query-only navigations (sort, filter,\n * pagination) skip the work. Opt out with `skipSameRoute: false`.\n * - **Stable handler reference**: the handler can change identity on\n * every render without causing resubscription — internal ref keeps\n * the latest handler accessible to the long-lived subscription.\n *\n * Returns nothing — the subscription's lifecycle is bound to the\n * component's mount.\n *\n * If the handler returns a Promise, the router blocks on it. If the\n * Promise resolves, navigation proceeds. If it rejects, the router emits\n * `TRANSITION_CANCELLED` (existing core behavior, no change here).\n *\n * @example Animation\n * ```tsx\n * const ref = useRef<HTMLDivElement>(null);\n *\n * useRouteExit(async ({ signal }) => {\n * const el = ref.current;\n * if (!el) return;\n * el.classList.add(\"fade-out\");\n * const cleanup = () => el.classList.remove(\"fade-out\");\n * signal.addEventListener(\"abort\", cleanup, { once: true });\n * try {\n * el.getBoundingClientRect(); // style flush\n * await Promise.allSettled(el.getAnimations().map((a) => a.finished));\n * } finally {\n * cleanup();\n * }\n * });\n * ```\n *\n * @example Auto-save form draft\n * ```tsx\n * useRouteExit(async ({ signal }) => {\n * if (formState.dirty) await api.saveDraft(formState, { signal });\n * });\n * ```\n *\n * @example Cancel inflight requests\n * ```tsx\n * useRouteExit(() => {\n * inflightController.abort();\n * });\n * ```\n *\n * @example Library-coordinated exit (motion / framer-motion)\n * ```tsx\n * const exitResolverRef = useRef<(() => void) | null>(null);\n *\n * useRouteExit(({ signal }) => {\n * return new Promise<void>((resolve) => {\n * exitResolverRef.current = resolve;\n * signal.addEventListener(\"abort\", () => resolve(), { once: true });\n * });\n * });\n *\n * const onExitComplete = () => exitResolverRef.current?.();\n * // pass onExitComplete to <AnimatePresence>\n * ```\n *\n * @example Reading rich transition metadata via `nextRoute.transition`\n * ```tsx\n * useRouteExit(({ route, nextRoute }) => {\n * // nextRoute.transition: TransitionMeta — preview of the upcoming nav\n * if (nextRoute.transition.segments.deactivated.includes(\"products\")) {\n * // leaving the products subtree entirely — flush product-related caches\n * productCache.clear();\n * }\n * if (nextRoute.transition.redirected) {\n * // skip animation when navigation arrived via redirect\n * return;\n * }\n * });\n * ```\n */\nexport function useRouteExit(\n handler: RouteExitHandler,\n options?: UseRouteExitOptions,\n): void {\n const router = useRouter();\n const handlerRef = useRef(handler);\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n // Keep the latest handler accessible to the subscription without\n // resubscribing on every render — the subscription registers the\n // wrapper once and dispatches to whatever ref points to.\n // useLayoutEffect (synchronous, post-render, pre-paint) updates the\n // ref before the browser can dispatch any router events that could\n // observe a stale closure.\n useLayoutEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n return router.subscribeLeave(({ route, nextRoute, signal }) => {\n if (skipSameRoute && route.name === nextRoute.name) {\n return;\n }\n\n // Reentrant abort: signal is already aborted when listener fires\n // (e.g. a newer navigate superseded this one before subscribeLeave\n // even ran). addEventListener(\"abort\", ...) does not fire\n // retroactively, so we skip the handler entirely — any cleanup the\n // handler would have wired via abort listener must not run because\n // the corresponding `add` work never happened.\n if (signal.aborted) {\n return;\n }\n\n return handlerRef.current({ route, nextRoute, signal });\n });\n }, [router, skipSameRoute]);\n}\n","import { useEffect, useLayoutEffect, useRef } from \"preact/hooks\";\n\nimport { useRoute } from \"./useRoute\";\n\nimport type { State } from \"@real-router/core\";\n\nexport interface RouteEnterContext {\n /** The route that was just activated. */\n route: State;\n /** The route that was active immediately before this navigation. */\n previousRoute: State;\n}\n\nexport type RouteEnterHandler = (context: RouteEnterContext) => void;\n\nexport interface UseRouteEnterOptions {\n /**\n * Skip the handler when `route.name === previousRoute.name`\n * (sort/filter/query-only navigations on the same route). Default:\n * `true`. Symmetric with `useRouteExit`'s same-name option.\n */\n skipSameRoute?: boolean;\n}\n\n/**\n * Fire `handler` once when the component mounts as a result of a\n * navigation. Mirror of `useRouteExit` for the entry side.\n *\n * What this hook covers that ad-hoc `useEffect` + `useRoute()` doesn't:\n *\n * - **Skip-initial**: handler is skipped when there is no\n * `previousRoute` (i.e. first-load mount). Most consumers want to\n * fire side effects only on real navigations, not on hydration.\n * - **Same-route skip** (default): handler is skipped when\n * `route.name === previousRoute.name`. Sort/filter/query-only\n * navigations re-run the effect (because `route` reference changes\n * in `useRoute`'s snapshot), but they are not \"entries\" in the\n * animation / analytics sense — the component instance has stayed\n * mounted throughout. Opt out with `skipSameRoute: false` when\n * the handler legitimately needs to fire on every navigation\n * (e.g. analytics tracking each query-param flip).\n * - **Latest-handler ref**: the handler can change identity on every\n * render without re-running the effect — the registered wrapper\n * dispatches to whatever `handlerRef.current` points to.\n * - **Mount-time `route` / `previousRoute` snapshot**: the handler\n * receives the values that were live at the moment of mount, not\n * the latest ones (which may have moved on if the user navigated\n * again before the effect drained).\n *\n * Race-safety: `useRoute()` is wired through `useSyncExternalStore` from\n * `@real-router/sources` (Preact polyfill: useState + useEffect, same\n * post-commit semantics), so by the time the new component's effect\n * runs, the snapshot is the post-commit one. This is the reason we can\n * read mount-time context from `useRoute()` instead of subscribing to\n * `router.subscribe` directly (which fires before Preact schedules a\n * re-render — the well-known race in distributed components).\n *\n * Note: Preact does not expose a `StrictMode` equivalent, so the\n * `lastHandledRouteRef` guard exists primarily for defensive symmetry\n * with the React implementation. It is harmless in Preact.\n *\n * @example Direction-aware entry animation\n * ```tsx\n * useRouteEnter(({ route }) => {\n * const direction = route.context.browser?.direction;\n * ref.current?.classList.add(\n * direction === \"back\" ? \"slide-from-left\" : \"slide-from-right\",\n * );\n * });\n * ```\n *\n * @example Source-aware focus management\n * ```tsx\n * useRouteEnter(({ route }) => {\n * if (route.context.browser?.source === \"navigate\") {\n * headingRef.current?.focus();\n * }\n * });\n * ```\n *\n * @example Analytics page-enter event (skip-initial built-in)\n * ```tsx\n * useRouteEnter(({ route, previousRoute }) => {\n * analytics.track(\"page_enter\", {\n * route: route.name,\n * from: previousRoute.name,\n * });\n * });\n * ```\n *\n * @example Reading rich transition metadata via `route.transition`\n * ```tsx\n * useRouteEnter(({ route }) => {\n * // route.transition: TransitionMeta — populated by core for every state\n * if (route.transition.redirected) {\n * showToast(`Redirected from ${route.transition.from}`);\n * }\n * if (route.transition.segments.activated.includes(\"products\")) {\n * // products subtree just became active (could be products or\n * // products.detail). Useful for subtree-scoped side effects.\n * }\n * });\n * ```\n */\nexport function useRouteEnter(\n handler: RouteEnterHandler,\n options?: UseRouteEnterOptions,\n): void {\n const { route, previousRoute } = useRoute();\n const handlerRef = useRef(handler);\n const lastHandledRouteRef = useRef<State | null>(null);\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n // Keep the latest handler reference accessible without re-running\n // the effect. useLayoutEffect (synchronous, post-render, pre-paint)\n // updates the ref before the effect can read it.\n useLayoutEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n // Early-exit guards, top-down:\n //\n // - **Skip-initial**: `state.transition.from` is undefined only\n // for the very first state committed by `router.start()`.\n // - **Skip-same-route**: query-only navigations have\n // `transition.from === route.name`. Opt-out via\n // `skipSameRoute: false`.\n // - **Defensive dedupe**: same `route` ref between effect\n // cleanup + re-run. Preact has no StrictMode, but we keep the\n // guard for parity with React; v8-ignored.\n if (!route.transition.from) {\n return;\n }\n if (skipSameRoute && route.transition.from === route.name) {\n return;\n }\n /* v8 ignore start */\n if (lastHandledRouteRef.current === route || !previousRoute) {\n return;\n }\n /* v8 ignore stop */\n\n lastHandledRouteRef.current = route;\n handlerRef.current({ route, previousRoute });\n }, [route, previousRoute, skipSameRoute]);\n}\n","import { getNavigator } from \"@real-router/core\";\nimport { createRouteSource } from \"@real-router/sources\";\nimport { useEffect, useMemo } from \"preact/hooks\";\n\nimport { NavigatorContext, RouteContext, RouterContext } from \"./context\";\nimport {\n createRouteAnnouncer,\n createScrollRestoration,\n createViewTransitions,\n} from \"./dom-utils\";\nimport { useSyncExternalStore } from \"./useSyncExternalStore\";\n\nimport type { ScrollRestorationOptions } from \"./dom-utils\";\nimport type { Router } from \"@real-router/core\";\nimport type { FunctionComponent, ComponentChildren } from \"preact\";\n\nexport interface RouteProviderProps {\n router: Router;\n children: ComponentChildren;\n announceNavigation?: boolean;\n scrollRestoration?: ScrollRestorationOptions;\n viewTransitions?: boolean;\n}\n\nexport const RouterProvider: FunctionComponent<RouteProviderProps> = ({\n router,\n children,\n announceNavigation,\n scrollRestoration,\n viewTransitions,\n}) => {\n useEffect(() => {\n if (!announceNavigation) {\n return;\n }\n\n const announcer = createRouteAnnouncer(router);\n\n return () => {\n announcer.destroy();\n };\n }, [announceNavigation, router]);\n\n // Primitive deps so inline `{ mode: \"restore\" }` doesn't thrash on every\n // render. scrollContainer is a getter invoked lazily on every event inside\n // the utility — swapping its reference doesn't change the resolved element,\n // so we intentionally omit it from deps to keep inline getters stable.\n const srMode = scrollRestoration?.mode;\n const srAnchor = scrollRestoration?.anchorScrolling;\n const srEnabled = scrollRestoration !== undefined;\n\n useEffect(() => {\n if (!srEnabled) {\n return;\n }\n\n const sr = createScrollRestoration(router, {\n mode: srMode,\n anchorScrolling: srAnchor,\n // srEnabled check above guarantees scrollRestoration is defined.\n scrollContainer: scrollRestoration.scrollContainer,\n });\n\n return () => {\n sr.destroy();\n };\n // scrollRestoration (for scrollContainer) omitted — see comment above.\n // eslint-disable-next-line @eslint-react/exhaustive-deps\n }, [router, srEnabled, srMode, srAnchor]);\n\n useEffect(() => {\n if (!viewTransitions) {\n return;\n }\n\n const vt = createViewTransitions(router);\n\n return () => {\n vt.destroy();\n };\n }, [router, viewTransitions]);\n\n const navigator = useMemo(() => getNavigator(router), [router]);\n\n // useSyncExternalStore manages the router subscription lifecycle:\n // subscribe connects to router on first listener, unsubscribes on last.\n const store = useMemo(() => createRouteSource(router), [router]);\n const { route, previousRoute } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot, // SSR: router returns same state on server and client\n );\n\n const routeContextValue = useMemo(\n () => ({ navigator, route, previousRoute }),\n [navigator, route, previousRoute],\n );\n\n return (\n <RouterContext.Provider value={router}>\n <NavigatorContext.Provider value={navigator}>\n <RouteContext.Provider value={routeContextValue}>\n {children}\n </RouteContext.Provider>\n </NavigatorContext.Provider>\n </RouterContext.Provider>\n );\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/preact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Preact integration for Real-Router",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"sideEffects": false,
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@real-router/core": "^0.50.
|
|
51
|
+
"@real-router/core": "^0.50.2",
|
|
52
52
|
"@real-router/route-utils": "^0.2.1",
|
|
53
53
|
"@real-router/sources": "^0.7.2"
|
|
54
54
|
},
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@testing-library/preact": "3.2.4",
|
|
59
59
|
"@testing-library/user-event": "14.6.1",
|
|
60
60
|
"preact": "10.25.4",
|
|
61
|
-
"@real-router/browser-plugin": "^0.
|
|
61
|
+
"@real-router/browser-plugin": "^0.16.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"preact": ">=10.0.0"
|
package/src/RouterProvider.tsx
CHANGED
|
@@ -3,7 +3,11 @@ import { createRouteSource } from "@real-router/sources";
|
|
|
3
3
|
import { useEffect, useMemo } from "preact/hooks";
|
|
4
4
|
|
|
5
5
|
import { NavigatorContext, RouteContext, RouterContext } from "./context";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createRouteAnnouncer,
|
|
8
|
+
createScrollRestoration,
|
|
9
|
+
createViewTransitions,
|
|
10
|
+
} from "./dom-utils";
|
|
7
11
|
import { useSyncExternalStore } from "./useSyncExternalStore";
|
|
8
12
|
|
|
9
13
|
import type { ScrollRestorationOptions } from "./dom-utils";
|
|
@@ -15,6 +19,7 @@ export interface RouteProviderProps {
|
|
|
15
19
|
children: ComponentChildren;
|
|
16
20
|
announceNavigation?: boolean;
|
|
17
21
|
scrollRestoration?: ScrollRestorationOptions;
|
|
22
|
+
viewTransitions?: boolean;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export const RouterProvider: FunctionComponent<RouteProviderProps> = ({
|
|
@@ -22,6 +27,7 @@ export const RouterProvider: FunctionComponent<RouteProviderProps> = ({
|
|
|
22
27
|
children,
|
|
23
28
|
announceNavigation,
|
|
24
29
|
scrollRestoration,
|
|
30
|
+
viewTransitions,
|
|
25
31
|
}) => {
|
|
26
32
|
useEffect(() => {
|
|
27
33
|
if (!announceNavigation) {
|
|
@@ -61,6 +67,19 @@ export const RouterProvider: FunctionComponent<RouteProviderProps> = ({
|
|
|
61
67
|
// scrollRestoration (for scrollContainer) omitted — see comment above.
|
|
62
68
|
// eslint-disable-next-line @eslint-react/exhaustive-deps
|
|
63
69
|
}, [router, srEnabled, srMode, srAnchor]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!viewTransitions) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const vt = createViewTransitions(router);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
vt.destroy();
|
|
80
|
+
};
|
|
81
|
+
}, [router, viewTransitions]);
|
|
82
|
+
|
|
64
83
|
const navigator = useMemo(() => getNavigator(router), [router]);
|
|
65
84
|
|
|
66
85
|
// useSyncExternalStore manages the router subscription lifecycle:
|
package/src/hooks/useRoute.tsx
CHANGED
|
@@ -3,14 +3,25 @@ import { useContext } from "preact/hooks";
|
|
|
3
3
|
import { RouteContext } from "../context";
|
|
4
4
|
|
|
5
5
|
import type { RouteContext as RouteContextType } from "../types";
|
|
6
|
-
import type { Params } from "@real-router/core";
|
|
6
|
+
import type { Params, State } from "@real-router/core";
|
|
7
7
|
|
|
8
|
-
export const useRoute = <P extends Params = Params>():
|
|
8
|
+
export const useRoute = <P extends Params = Params>(): Omit<
|
|
9
|
+
RouteContextType<P>,
|
|
10
|
+
"route"
|
|
11
|
+
> & { route: State<P> } => {
|
|
9
12
|
const routeContext = useContext(RouteContext);
|
|
10
13
|
|
|
11
14
|
if (!routeContext) {
|
|
12
|
-
throw new Error("useRoute must be used within a
|
|
15
|
+
throw new Error("useRoute must be used within a RouterProvider");
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
if (!routeContext.route) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return routeContext as Omit<RouteContextType<P>, "route"> & {
|
|
25
|
+
route: State<P>;
|
|
26
|
+
};
|
|
16
27
|
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
import { useRoute } from "./useRoute";
|
|
4
|
+
|
|
5
|
+
import type { State } from "@real-router/core";
|
|
6
|
+
|
|
7
|
+
export interface RouteEnterContext {
|
|
8
|
+
/** The route that was just activated. */
|
|
9
|
+
route: State;
|
|
10
|
+
/** The route that was active immediately before this navigation. */
|
|
11
|
+
previousRoute: State;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type RouteEnterHandler = (context: RouteEnterContext) => void;
|
|
15
|
+
|
|
16
|
+
export interface UseRouteEnterOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Skip the handler when `route.name === previousRoute.name`
|
|
19
|
+
* (sort/filter/query-only navigations on the same route). Default:
|
|
20
|
+
* `true`. Symmetric with `useRouteExit`'s same-name option.
|
|
21
|
+
*/
|
|
22
|
+
skipSameRoute?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fire `handler` once when the component mounts as a result of a
|
|
27
|
+
* navigation. Mirror of `useRouteExit` for the entry side.
|
|
28
|
+
*
|
|
29
|
+
* What this hook covers that ad-hoc `useEffect` + `useRoute()` doesn't:
|
|
30
|
+
*
|
|
31
|
+
* - **Skip-initial**: handler is skipped when there is no
|
|
32
|
+
* `previousRoute` (i.e. first-load mount). Most consumers want to
|
|
33
|
+
* fire side effects only on real navigations, not on hydration.
|
|
34
|
+
* - **Same-route skip** (default): handler is skipped when
|
|
35
|
+
* `route.name === previousRoute.name`. Sort/filter/query-only
|
|
36
|
+
* navigations re-run the effect (because `route` reference changes
|
|
37
|
+
* in `useRoute`'s snapshot), but they are not "entries" in the
|
|
38
|
+
* animation / analytics sense — the component instance has stayed
|
|
39
|
+
* mounted throughout. Opt out with `skipSameRoute: false` when
|
|
40
|
+
* the handler legitimately needs to fire on every navigation
|
|
41
|
+
* (e.g. analytics tracking each query-param flip).
|
|
42
|
+
* - **Latest-handler ref**: the handler can change identity on every
|
|
43
|
+
* render without re-running the effect — the registered wrapper
|
|
44
|
+
* dispatches to whatever `handlerRef.current` points to.
|
|
45
|
+
* - **Mount-time `route` / `previousRoute` snapshot**: the handler
|
|
46
|
+
* receives the values that were live at the moment of mount, not
|
|
47
|
+
* the latest ones (which may have moved on if the user navigated
|
|
48
|
+
* again before the effect drained).
|
|
49
|
+
*
|
|
50
|
+
* Race-safety: `useRoute()` is wired through `useSyncExternalStore` from
|
|
51
|
+
* `@real-router/sources` (Preact polyfill: useState + useEffect, same
|
|
52
|
+
* post-commit semantics), so by the time the new component's effect
|
|
53
|
+
* runs, the snapshot is the post-commit one. This is the reason we can
|
|
54
|
+
* read mount-time context from `useRoute()` instead of subscribing to
|
|
55
|
+
* `router.subscribe` directly (which fires before Preact schedules a
|
|
56
|
+
* re-render — the well-known race in distributed components).
|
|
57
|
+
*
|
|
58
|
+
* Note: Preact does not expose a `StrictMode` equivalent, so the
|
|
59
|
+
* `lastHandledRouteRef` guard exists primarily for defensive symmetry
|
|
60
|
+
* with the React implementation. It is harmless in Preact.
|
|
61
|
+
*
|
|
62
|
+
* @example Direction-aware entry animation
|
|
63
|
+
* ```tsx
|
|
64
|
+
* useRouteEnter(({ route }) => {
|
|
65
|
+
* const direction = route.context.browser?.direction;
|
|
66
|
+
* ref.current?.classList.add(
|
|
67
|
+
* direction === "back" ? "slide-from-left" : "slide-from-right",
|
|
68
|
+
* );
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example Source-aware focus management
|
|
73
|
+
* ```tsx
|
|
74
|
+
* useRouteEnter(({ route }) => {
|
|
75
|
+
* if (route.context.browser?.source === "navigate") {
|
|
76
|
+
* headingRef.current?.focus();
|
|
77
|
+
* }
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example Analytics page-enter event (skip-initial built-in)
|
|
82
|
+
* ```tsx
|
|
83
|
+
* useRouteEnter(({ route, previousRoute }) => {
|
|
84
|
+
* analytics.track("page_enter", {
|
|
85
|
+
* route: route.name,
|
|
86
|
+
* from: previousRoute.name,
|
|
87
|
+
* });
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @example Reading rich transition metadata via `route.transition`
|
|
92
|
+
* ```tsx
|
|
93
|
+
* useRouteEnter(({ route }) => {
|
|
94
|
+
* // route.transition: TransitionMeta — populated by core for every state
|
|
95
|
+
* if (route.transition.redirected) {
|
|
96
|
+
* showToast(`Redirected from ${route.transition.from}`);
|
|
97
|
+
* }
|
|
98
|
+
* if (route.transition.segments.activated.includes("products")) {
|
|
99
|
+
* // products subtree just became active (could be products or
|
|
100
|
+
* // products.detail). Useful for subtree-scoped side effects.
|
|
101
|
+
* }
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function useRouteEnter(
|
|
106
|
+
handler: RouteEnterHandler,
|
|
107
|
+
options?: UseRouteEnterOptions,
|
|
108
|
+
): void {
|
|
109
|
+
const { route, previousRoute } = useRoute();
|
|
110
|
+
const handlerRef = useRef(handler);
|
|
111
|
+
const lastHandledRouteRef = useRef<State | null>(null);
|
|
112
|
+
const skipSameRoute = options?.skipSameRoute ?? true;
|
|
113
|
+
|
|
114
|
+
// Keep the latest handler reference accessible without re-running
|
|
115
|
+
// the effect. useLayoutEffect (synchronous, post-render, pre-paint)
|
|
116
|
+
// updates the ref before the effect can read it.
|
|
117
|
+
useLayoutEffect(() => {
|
|
118
|
+
handlerRef.current = handler;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
// Early-exit guards, top-down:
|
|
123
|
+
//
|
|
124
|
+
// - **Skip-initial**: `state.transition.from` is undefined only
|
|
125
|
+
// for the very first state committed by `router.start()`.
|
|
126
|
+
// - **Skip-same-route**: query-only navigations have
|
|
127
|
+
// `transition.from === route.name`. Opt-out via
|
|
128
|
+
// `skipSameRoute: false`.
|
|
129
|
+
// - **Defensive dedupe**: same `route` ref between effect
|
|
130
|
+
// cleanup + re-run. Preact has no StrictMode, but we keep the
|
|
131
|
+
// guard for parity with React; v8-ignored.
|
|
132
|
+
if (!route.transition.from) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (skipSameRoute && route.transition.from === route.name) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
/* v8 ignore start */
|
|
139
|
+
if (lastHandledRouteRef.current === route || !previousRoute) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
/* v8 ignore stop */
|
|
143
|
+
|
|
144
|
+
lastHandledRouteRef.current = route;
|
|
145
|
+
handlerRef.current({ route, previousRoute });
|
|
146
|
+
}, [route, previousRoute, skipSameRoute]);
|
|
147
|
+
}
|