UNPKG

@tanstack/react-router

Version:

Modern and scalable routing for React applications

338 lines (337 loc) • 9.8 kB
import { jsx } from "react/jsx-runtime"; import * as React from "react"; import { flushSync } from "react-dom"; import { preloadWarning, functionalUpdate, exactPathTest, removeTrailingSlash, deepEqual } from "@tanstack/router-core"; import { useRouterState } from "./useRouterState.js"; import { useRouter } from "./useRouter.js"; import { useForwardedRef, useIntersectionObserver } from "./utils.js"; function useLinkProps(options, forwardedRef) { const router = useRouter(); const [isTransitioning, setIsTransitioning] = React.useState(false); const hasRenderFetched = React.useRef(false); const innerRef = useForwardedRef(forwardedRef); const { // custom props activeProps, inactiveProps, activeOptions, to, preload: userPreload, preloadDelay: userPreloadDelay, hashScrollIntoView, replace, startTransition, resetScroll, viewTransition, // element props children, target, disabled, style, className, onClick, onFocus, onMouseEnter, onMouseLeave, onTouchStart, ignoreBlocker, // prevent these from being returned params: _params, search: _search, hash: _hash, state: _state, mask: _mask, reloadDocument: _reloadDocument, unsafeRelative: _unsafeRelative, from: _from, _fromLocation, ...propsSafeToSpread } = options; const currentSearch = useRouterState({ select: (s) => s.location.search, structuralSharing: true }); const from = options.from; const _options = React.useMemo( () => { return { ...options, from }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [ router, currentSearch, from, options._fromLocation, options.hash, options.to, options.search, options.params, options.state, options.mask, options.unsafeRelative ] ); const next = React.useMemo( () => router.buildLocation({ ..._options }), [router, _options] ); const hrefOption = React.useMemo(() => { if (disabled) { return void 0; } let href = next.maskedLocation ? next.maskedLocation.url : next.url; let external = false; if (router.origin) { if (href.startsWith(router.origin)) { href = href.replace(router.origin, "") || "/"; } else { external = true; } } return { href, external }; }, [disabled, next.maskedLocation, next.url, router.origin]); const externalLink = React.useMemo(() => { if (hrefOption?.external) { return hrefOption.href; } try { new URL(to); return to; } catch { } return void 0; }, [to, hrefOption]); const preload = options.reloadDocument || externalLink ? false : userPreload ?? router.options.defaultPreload; const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0; const isActive = useRouterState({ select: (s) => { if (externalLink) return false; if (activeOptions?.exact) { const testExact = exactPathTest( s.location.pathname, next.pathname, router.basepath ); if (!testExact) { return false; } } else { const currentPathSplit = removeTrailingSlash( s.location.pathname, router.basepath ); const nextPathSplit = removeTrailingSlash( next.pathname, router.basepath ); const pathIsFuzzyEqual = currentPathSplit.startsWith(nextPathSplit) && (currentPathSplit.length === nextPathSplit.length || currentPathSplit[nextPathSplit.length] === "/"); if (!pathIsFuzzyEqual) { return false; } } if (activeOptions?.includeSearch ?? true) { const searchTest = deepEqual(s.location.search, next.search, { partial: !activeOptions?.exact, ignoreUndefined: !activeOptions?.explicitUndefined }); if (!searchTest) { return false; } } if (activeOptions?.includeHash) { return s.location.hash === next.hash; } return true; } }); const doPreload = React.useCallback(() => { router.preloadRoute({ ..._options }).catch((err) => { console.warn(err); console.warn(preloadWarning); }); }, [router, _options]); const preloadViewportIoCallback = React.useCallback( (entry) => { if (entry?.isIntersecting) { doPreload(); } }, [doPreload] ); useIntersectionObserver( innerRef, preloadViewportIoCallback, 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.target; const effectiveTarget = target !== void 0 ? target : elementTarget; if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!effectiveTarget || effectiveTarget === "_self") && e.button === 0) { e.preventDefault(); 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 }, ...onFocus && { onFocus }, ...onMouseEnter && { onMouseEnter }, ...onMouseLeave && { onMouseLeave }, ...onTouchStart && { onTouchStart } }; } const handleFocus = (_) => { if (disabled) return; if (preload) { doPreload(); } }; const handleTouchStart = handleFocus; const handleEnter = (e) => { if (disabled || !preload) return; if (!preloadDelay) { doPreload(); } else { const eventTarget = e.target; if (timeoutMap.has(eventTarget)) { return; } const id = setTimeout(() => { timeoutMap.delete(eventTarget); doPreload(); }, preloadDelay); timeoutMap.set(eventTarget, id); } }; const handleLeave = (e) => { if (disabled || !preload || !preloadDelay) return; const eventTarget = e.target; const id = timeoutMap.get(eventTarget); if (id) { clearTimeout(id); timeoutMap.delete(eventTarget); } }; const resolvedActiveProps = isActive ? functionalUpdate(activeProps, {}) ?? STATIC_ACTIVE_OBJECT : STATIC_EMPTY_OBJECT; const resolvedInactiveProps = isActive ? STATIC_EMPTY_OBJECT : 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 }; return { ...propsSafeToSpread, ...resolvedActiveProps, ...resolvedInactiveProps, href: hrefOption?.href, ref: innerRef, onClick: composeHandlers([onClick, handleClick]), onFocus: composeHandlers([onFocus, handleFocus]), onMouseEnter: composeHandlers([onMouseEnter, handleEnter]), 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, ...isTransitioning && STATIC_TRANSITIONING_PROPS }; } const STATIC_EMPTY_OBJECT = {}; const STATIC_ACTIVE_OBJECT = { className: "active" }; const STATIC_DISABLED_PROPS = { role: "link", "aria-disabled": true }; const STATIC_ACTIVE_PROPS = { "data-status": "active", "aria-current": "page" }; const STATIC_TRANSITIONING_PROPS = { "data-transitioning": "transitioning" }; const timeoutMap = /* @__PURE__ */ new WeakMap(); const intersectionObserverOptions = { rootMargin: "100px" }; const composeHandlers = (handlers) => (e) => { for (const handler of handlers) { if (!handler) continue; if (e.defaultPrevented) return; handler(e); } }; function createLink(Comp) { return React.forwardRef(function CreatedLink(props, ref) { return /* @__PURE__ */ jsx(Link, { ...props, _asChild: Comp, ref }); }); } const Link = React.forwardRef( (props, ref) => { const { _asChild, ...rest } = props; const { type: _type, ref: innerRef, ...linkProps } = useLinkProps(rest, ref); const children = typeof rest.children === "function" ? rest.children({ isActive: linkProps["data-status"] === "active" }) : rest.children; if (_asChild === void 0) { delete linkProps.disabled; } return React.createElement( _asChild ? _asChild : "a", { ...linkProps, ref: innerRef }, children ); } ); function isCtrlEvent(e) { return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); } const linkOptions = (options) => { return options; }; export { Link, createLink, linkOptions, useLinkProps }; //# sourceMappingURL=link.js.map