UNPKG

@tanstack/react-router

Version:

Modern and scalable routing for React applications

484 lines (483 loc) • 18.4 kB
const require_runtime = require("./_virtual/_rolldown/runtime.cjs"); const require_utils = require("./utils.cjs"); const require_ClientOnly = require("./ClientOnly.cjs"); const require_useRouter = require("./useRouter.cjs"); let _tanstack_router_core = require("@tanstack/router-core"); let react = require("react"); react = require_runtime.__toESM(react); let react_jsx_runtime = require("react/jsx-runtime"); let _tanstack_react_store = require("@tanstack/react-store"); let _tanstack_router_core_isServer = require("@tanstack/router-core/isServer"); let react_dom = require("react-dom"); //#region src/link.tsx /** * Build anchor-like props for declarative navigation and preloading. * * Returns stable `href`, event handlers and accessibility props derived from * router options and active state. Used internally by `Link` and custom links. * * Options cover `to`, `params`, `search`, `hash`, `state`, `preload`, * `activeProps`, `inactiveProps`, and more. * * @returns React anchor props suitable for `<a>` or custom components. * @link https://tanstack.com/router/latest/docs/framework/react/api/router/useLinkPropsHook */ function useLinkProps(options, forwardedRef) { const router = require_useRouter.useRouter(); const innerRef = require_utils.useForwardedRef(forwardedRef); const _isServer = _tanstack_router_core_isServer.isServer ?? router.isServer; const { activeProps, inactiveProps, activeOptions, to, preload: userPreload, preloadDelay: userPreloadDelay, hashScrollIntoView, replace, startTransition, resetScroll, viewTransition, children, target, disabled, style, className, onClick, onBlur, onFocus, onMouseEnter, onMouseLeave, onTouchStart, ignoreBlocker, params: _params, search: _search, hash: _hash, state: _state, mask: _mask, reloadDocument: _reloadDocument, unsafeRelative: _unsafeRelative, from: _from, _fromLocation, ...propsSafeToSpread } = options; if (_isServer) { const safeInternal = isSafeInternal(to); if (typeof to === "string" && !safeInternal && to.indexOf(":") > -1) try { new URL(to); if ((0, _tanstack_router_core.isDangerousProtocol)(to, router.protocolAllowlist)) { if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`); return { ...propsSafeToSpread, ref: innerRef, href: void 0, ...children && { children }, ...target && { target }, ...disabled && { disabled }, ...style && { style }, ...className && { className } }; } return { ...propsSafeToSpread, ref: innerRef, href: to, ...children && { children }, ...target && { target }, ...disabled && { disabled }, ...style && { style }, ...className && { className } }; } catch {} const next = router.buildLocation({ ...options, from: options.from }); const hrefOption = getHrefOption(next.maskedLocation ? next.maskedLocation.publicHref : next.publicHref, next.maskedLocation ? next.maskedLocation.external : next.external, router.history, disabled); const externalLink = (() => { if (hrefOption?.external) { if ((0, _tanstack_router_core.isDangerousProtocol)(hrefOption.href, router.protocolAllowlist)) { if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${hrefOption.href}`); return; } return hrefOption.href; } if (safeInternal) return void 0; if (typeof to === "string" && to.indexOf(":") > -1) try { new URL(to); if ((0, _tanstack_router_core.isDangerousProtocol)(to, router.protocolAllowlist)) { if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`); return; } return to; } catch {} })(); const isActive = (() => { if (externalLink) return false; const currentLocation = router.stores.location.state; const exact = activeOptions?.exact ?? false; if (exact) { if (!(0, _tanstack_router_core.exactPathTest)(currentLocation.pathname, next.pathname, router.basepath)) return false; } else { const currentPathSplit = (0, _tanstack_router_core.removeTrailingSlash)(currentLocation.pathname, router.basepath); const nextPathSplit = (0, _tanstack_router_core.removeTrailingSlash)(next.pathname, router.basepath); if (!(currentPathSplit.startsWith(nextPathSplit) && (currentPathSplit.length === nextPathSplit.length || currentPathSplit[nextPathSplit.length] === "/"))) return false; } if (activeOptions?.includeSearch ?? true) { if (currentLocation.search !== next.search) { const currentSearchEmpty = !currentLocation.search || typeof currentLocation.search === "object" && Object.keys(currentLocation.search).length === 0; const nextSearchEmpty = !next.search || typeof next.search === "object" && Object.keys(next.search).length === 0; if (!(currentSearchEmpty && nextSearchEmpty)) { if (!(0, _tanstack_router_core.deepEqual)(currentLocation.search, next.search, { partial: !exact, ignoreUndefined: !activeOptions?.explicitUndefined })) return false; } } } if (activeOptions?.includeHash) return false; return true; })(); if (externalLink) return { ...propsSafeToSpread, ref: innerRef, href: externalLink, ...children && { children }, ...target && { target }, ...disabled && { disabled }, ...style && { style }, ...className && { className } }; const resolvedActiveProps = isActive ? (0, _tanstack_router_core.functionalUpdate)(activeProps, {}) ?? STATIC_ACTIVE_OBJECT : STATIC_EMPTY_OBJECT; const resolvedInactiveProps = isActive ? STATIC_EMPTY_OBJECT : (0, _tanstack_router_core.functionalUpdate)(inactiveProps, {}) ?? STATIC_EMPTY_OBJECT; const resolvedStyle = (() => { const baseStyle = style; const activeStyle = resolvedActiveProps.style; const inactiveStyle = resolvedInactiveProps.style; if (!baseStyle && !activeStyle && !inactiveStyle) return; if (baseStyle && !activeStyle && !inactiveStyle) return baseStyle; if (!baseStyle && activeStyle && !inactiveStyle) return activeStyle; if (!baseStyle && !activeStyle && inactiveStyle) return inactiveStyle; return { ...baseStyle, ...activeStyle, ...inactiveStyle }; })(); const resolvedClassName = (() => { const baseClassName = className; const activeClassName = resolvedActiveProps.className; const inactiveClassName = resolvedInactiveProps.className; if (!baseClassName && !activeClassName && !inactiveClassName) return ""; let out = ""; if (baseClassName) out = baseClassName; if (activeClassName) out = out ? `${out} ${activeClassName}` : activeClassName; if (inactiveClassName) out = out ? `${out} ${inactiveClassName}` : inactiveClassName; return out; })(); return { ...propsSafeToSpread, ...resolvedActiveProps, ...resolvedInactiveProps, href: hrefOption?.href, ref: innerRef, disabled: !!disabled, target, ...resolvedStyle && { style: resolvedStyle }, ...resolvedClassName && { className: resolvedClassName }, ...disabled && STATIC_DISABLED_PROPS, ...isActive && STATIC_ACTIVE_PROPS }; } const isHydrated = require_ClientOnly.useHydrated(); const _options = react.useMemo(() => options, [ router, options.from, options._fromLocation, options.hash, options.to, options.search, options.params, options.state, options.mask, options.unsafeRelative ]); const currentLocation = (0, _tanstack_react_store.useStore)(router.stores.location, (l) => l, (prev, next) => prev.href === next.href); const next = react.useMemo(() => { const opts = { _fromLocation: currentLocation, ..._options }; return router.buildLocation(opts); }, [ router, currentLocation, _options ]); const hrefOptionPublicHref = next.maskedLocation ? next.maskedLocation.publicHref : next.publicHref; const hrefOptionExternal = next.maskedLocation ? next.maskedLocation.external : next.external; const hrefOption = react.useMemo(() => getHrefOption(hrefOptionPublicHref, hrefOptionExternal, router.history, disabled), [ disabled, hrefOptionExternal, hrefOptionPublicHref, router.history ]); const externalLink = react.useMemo(() => { if (hrefOption?.external) { if ((0, _tanstack_router_core.isDangerousProtocol)(hrefOption.href, router.protocolAllowlist)) { if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${hrefOption.href}`); return; } return hrefOption.href; } if (isSafeInternal(to)) return void 0; if (typeof to !== "string" || to.indexOf(":") === -1) return void 0; try { new URL(to); if ((0, _tanstack_router_core.isDangerousProtocol)(to, router.protocolAllowlist)) { if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`); return; } return to; } catch {} }, [ to, hrefOption, router.protocolAllowlist ]); const isActive = react.useMemo(() => { if (externalLink) return false; if (activeOptions?.exact) { if (!(0, _tanstack_router_core.exactPathTest)(currentLocation.pathname, next.pathname, router.basepath)) return false; } else { const currentPathSplit = (0, _tanstack_router_core.removeTrailingSlash)(currentLocation.pathname, router.basepath); const nextPathSplit = (0, _tanstack_router_core.removeTrailingSlash)(next.pathname, router.basepath); if (!(currentPathSplit.startsWith(nextPathSplit) && (currentPathSplit.length === nextPathSplit.length || currentPathSplit[nextPathSplit.length] === "/"))) return false; } if (activeOptions?.includeSearch ?? true) { if (!(0, _tanstack_router_core.deepEqual)(currentLocation.search, next.search, { partial: !activeOptions?.exact, ignoreUndefined: !activeOptions?.explicitUndefined })) return false; } if (activeOptions?.includeHash) return isHydrated && currentLocation.hash === next.hash; return true; }, [ activeOptions?.exact, activeOptions?.explicitUndefined, activeOptions?.includeHash, activeOptions?.includeSearch, currentLocation, externalLink, isHydrated, next.hash, next.pathname, next.search, router.basepath ]); const resolvedActiveProps = isActive ? (0, _tanstack_router_core.functionalUpdate)(activeProps, {}) ?? STATIC_ACTIVE_OBJECT : STATIC_EMPTY_OBJECT; const resolvedInactiveProps = isActive ? STATIC_EMPTY_OBJECT : (0, _tanstack_router_core.functionalUpdate)(inactiveProps, {}) ?? STATIC_EMPTY_OBJECT; const resolvedClassName = [ className, resolvedActiveProps.className, resolvedInactiveProps.className ].filter(Boolean).join(" "); const resolvedStyle = (style || resolvedActiveProps.style || resolvedInactiveProps.style) && { ...style, ...resolvedActiveProps.style, ...resolvedInactiveProps.style }; const [isTransitioning, setIsTransitioning] = react.useState(false); const hasRenderFetched = react.useRef(false); const preload = options.reloadDocument || externalLink ? false : userPreload ?? router.options.defaultPreload; const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0; const doPreload = react.useCallback(() => { router.preloadRoute({ ..._options, _builtLocation: next }).catch((err) => { console.warn(err); console.warn(_tanstack_router_core.preloadWarning); }); }, [ router, _options, next ]); require_utils.useIntersectionObserver(innerRef, react.useCallback((entry) => { if (entry?.isIntersecting) doPreload(); }, [doPreload]), intersectionObserverOptions, { disabled: !!disabled || !(preload === "viewport") }); react.useEffect(() => { if (hasRenderFetched.current) return; if (!disabled && preload === "render") { doPreload(); hasRenderFetched.current = true; } }, [ disabled, doPreload, preload ]); const handleClick = (e) => { const elementTarget = e.currentTarget.getAttribute("target"); const effectiveTarget = target !== void 0 ? target : elementTarget; if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!effectiveTarget || effectiveTarget === "_self") && e.button === 0) { e.preventDefault(); (0, react_dom.flushSync)(() => { setIsTransitioning(true); }); const unsub = router.subscribe("onResolved", () => { unsub(); setIsTransitioning(false); }); router.navigate({ ..._options, replace, resetScroll, hashScrollIntoView, startTransition, viewTransition, ignoreBlocker }); } }; if (externalLink) return { ...propsSafeToSpread, ref: innerRef, href: externalLink, ...children && { children }, ...target && { target }, ...disabled && { disabled }, ...style && { style }, ...className && { className }, ...onClick && { onClick }, ...onBlur && { onBlur }, ...onFocus && { onFocus }, ...onMouseEnter && { onMouseEnter }, ...onMouseLeave && { onMouseLeave }, ...onTouchStart && { onTouchStart } }; const enqueueIntentPreload = (e) => { if (disabled || preload !== "intent") return; if (!preloadDelay) { doPreload(); return; } const eventTarget = e.currentTarget; if (timeoutMap.has(eventTarget)) return; const id = setTimeout(() => { timeoutMap.delete(eventTarget); doPreload(); }, preloadDelay); timeoutMap.set(eventTarget, id); }; const handleTouchStart = (_) => { if (disabled || preload !== "intent") return; doPreload(); }; const handleLeave = (e) => { if (disabled || !preload || !preloadDelay) return; const eventTarget = e.currentTarget; const id = timeoutMap.get(eventTarget); if (id) { clearTimeout(id); timeoutMap.delete(eventTarget); } }; return { ...propsSafeToSpread, ...resolvedActiveProps, ...resolvedInactiveProps, href: hrefOption?.href, ref: innerRef, onClick: composeHandlers([onClick, handleClick]), onBlur: composeHandlers([onBlur, handleLeave]), onFocus: composeHandlers([onFocus, enqueueIntentPreload]), onMouseEnter: composeHandlers([onMouseEnter, enqueueIntentPreload]), onMouseLeave: composeHandlers([onMouseLeave, handleLeave]), onTouchStart: composeHandlers([onTouchStart, handleTouchStart]), disabled: !!disabled, target, ...resolvedStyle && { style: resolvedStyle }, ...resolvedClassName && { className: resolvedClassName }, ...disabled && STATIC_DISABLED_PROPS, ...isActive && STATIC_ACTIVE_PROPS, ...isHydrated && isTransitioning && STATIC_TRANSITIONING_PROPS }; } var STATIC_EMPTY_OBJECT = {}; var STATIC_ACTIVE_OBJECT = { className: "active" }; var STATIC_DISABLED_PROPS = { role: "link", "aria-disabled": true }; var STATIC_ACTIVE_PROPS = { "data-status": "active", "aria-current": "page" }; var STATIC_TRANSITIONING_PROPS = { "data-transitioning": "transitioning" }; var timeoutMap = /* @__PURE__ */ new WeakMap(); var intersectionObserverOptions = { rootMargin: "100px" }; var composeHandlers = (handlers) => (e) => { for (const handler of handlers) { if (!handler) continue; if (e.defaultPrevented) return; handler(e); } }; function getHrefOption(publicHref, external, history, disabled) { if (disabled) return void 0; if (external) return { href: publicHref, external: true }; return { href: history.createHref(publicHref) || "/", external: false }; } function isSafeInternal(to) { if (typeof to !== "string") return false; const zero = to.charCodeAt(0); if (zero === 47) return to.charCodeAt(1) !== 47; return zero === 46; } /** * Creates a typed Link-like component that preserves TanStack Router's * navigation semantics and type-safety while delegating rendering to the * provided host component. * * Useful for integrating design system anchors/buttons while keeping * router-aware props (eg. `to`, `params`, `search`, `preload`). * * @param Comp The host component to render (eg. a design-system Link/Button) * @returns A router-aware component with the same API as `Link`. * @link https://tanstack.com/router/latest/docs/framework/react/guide/custom-link */ function createLink(Comp) { return react.forwardRef(function CreatedLink(props, ref) { return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Link, { ...props, _asChild: Comp, ref }); }); } /** * A strongly-typed anchor component for declarative navigation. * Handles path, search, hash and state updates with optional route preloading * and active-state styling. * * Props: * - `preload`: Controls route preloading (eg. 'intent', 'render', 'viewport', true/false) * - `preloadDelay`: Delay in ms before preloading on hover * - `activeProps`/`inactiveProps`: Additional props merged when link is active/inactive * - `resetScroll`/`hashScrollIntoView`: Control scroll behavior on navigation * - `viewTransition`/`startTransition`: Use View Transitions/React transitions for navigation * - `ignoreBlocker`: Bypass registered blockers * * @returns An anchor-like element that navigates without full page reloads. * @link https://tanstack.com/router/latest/docs/framework/react/api/router/linkComponent */ var Link = react.forwardRef((props, ref) => { const { _asChild, ...rest } = props; const { type: _type, ...linkProps } = useLinkProps(rest, ref); const children = typeof rest.children === "function" ? rest.children({ isActive: linkProps["data-status"] === "active" }) : rest.children; if (!_asChild) { const { disabled: _, ...rest } = linkProps; return react.createElement("a", rest, children); } return react.createElement(_asChild, linkProps, children); }); function isCtrlEvent(e) { return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); } /** * Validate and reuse navigation options for `Link`, `navigate` or `redirect`. * Accepts a literal options object and returns it typed for later spreading. * @example * const opts = linkOptions({ to: '/dashboard', search: { tab: 'home' } }) * @link https://tanstack.com/router/latest/docs/framework/react/api/router/linkOptions */ var linkOptions = (options) => { return options; }; /** * Type-check a literal object for use with `Link`, `navigate` or `redirect`. * Use to validate and reuse navigation options across your app. * @example * const opts = linkOptions({ to: '/dashboard', search: { tab: 'home' } }) * @link https://tanstack.com/router/latest/docs/framework/react/api/router/linkOptions */ //#endregion exports.Link = Link; exports.createLink = createLink; exports.linkOptions = linkOptions; exports.useLinkProps = useLinkProps; //# sourceMappingURL=link.cjs.map