UNPKG

@tanstack/react-router

Version:

Modern and scalable routing for React applications

305 lines (304 loc) 8.97 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, useLayoutEffect } from "./utils.js"; import { useMatches } from "./Matches.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 = () => ({ className: "active" }), 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, ...rest } = options; const { // prevent these from being returned params: _params, search: _search, hash: _hash, state: _state, mask: _mask, reloadDocument: _reloadDocument, ...propsSafeToSpread } = rest; const type = React.useMemo(() => { try { new URL(`${to}`); return "external"; } catch { } return "internal"; }, [to]); const currentSearch = useRouterState({ select: (s) => s.location.search, structuralSharing: true }); const from = useMatches({ select: (matches) => { var _a; return options.from ?? ((_a = matches[matches.length - 1]) == null ? void 0 : _a.fullPath); } }); const _options = React.useMemo(() => ({ ...options, from }), [options, from]); const next = React.useMemo( () => router.buildLocation(_options), // eslint-disable-next-line react-hooks/exhaustive-deps [router, _options, currentSearch] ); const preload = React.useMemo(() => { if (_options.reloadDocument) { return false; } return userPreload ?? router.options.defaultPreload; }, [router.options.defaultPreload, userPreload, _options.reloadDocument]); const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0; const isActive = useRouterState({ select: (s) => { if (activeOptions == null ? void 0 : 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 ).split("/"); const nextPathSplit = removeTrailingSlash( next.pathname, router.basepath ).split("/"); const pathIsFuzzyEqual = nextPathSplit.every( (d, i) => d === currentPathSplit[i] ); if (!pathIsFuzzyEqual) { return false; } } if ((activeOptions == null ? void 0 : activeOptions.includeSearch) ?? true) { const searchTest = deepEqual(s.location.search, next.search, { partial: !(activeOptions == null ? void 0 : activeOptions.exact), ignoreUndefined: !(activeOptions == null ? void 0 : activeOptions.explicitUndefined) }); if (!searchTest) { return false; } } if (activeOptions == null ? void 0 : 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); }); }, [_options, router]); const preloadViewportIoCallback = React.useCallback( (entry) => { if (entry == null ? void 0 : entry.isIntersecting) { doPreload(); } }, [doPreload] ); useIntersectionObserver( innerRef, preloadViewportIoCallback, { rootMargin: "100px" }, { disabled: !!disabled || !(preload === "viewport") } ); useLayoutEffect(() => { if (hasRenderFetched.current) { return; } if (!disabled && preload === "render") { doPreload(); hasRenderFetched.current = true; } }, [disabled, doPreload, preload]); if (type === "external") { return { ...propsSafeToSpread, ref: innerRef, type, href: to, ...children && { children }, ...target && { target }, ...disabled && { disabled }, ...style && { style }, ...className && { className }, ...onClick && { onClick }, ...onFocus && { onFocus }, ...onMouseEnter && { onMouseEnter }, ...onMouseLeave && { onMouseLeave }, ...onTouchStart && { onTouchStart } }; } const handleClick = (e) => { if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!target || target === "_self") && e.button === 0) { e.preventDefault(); flushSync(() => { setIsTransitioning(true); }); const unsub = router.subscribe("onResolved", () => { unsub(); setIsTransitioning(false); }); return router.navigate({ ..._options, replace, resetScroll, hashScrollIntoView, startTransition, viewTransition, ignoreBlocker }); } }; const handleFocus = (_) => { if (disabled) return; if (preload) { doPreload(); } }; const handleTouchStart = handleFocus; const handleEnter = (e) => { if (disabled) return; const eventTarget = e.target || {}; if (preload) { if (eventTarget.preloadTimeout) { return; } eventTarget.preloadTimeout = setTimeout(() => { eventTarget.preloadTimeout = null; doPreload(); }, preloadDelay); } }; const handleLeave = (e) => { if (disabled) return; const eventTarget = e.target || {}; if (eventTarget.preloadTimeout) { clearTimeout(eventTarget.preloadTimeout); eventTarget.preloadTimeout = null; } }; const composeHandlers = (handlers) => (e) => { var _a; (_a = e.persist) == null ? void 0 : _a.call(e); handlers.filter(Boolean).forEach((handler) => { if (e.defaultPrevented) return; handler(e); }); }; const resolvedActiveProps = isActive ? functionalUpdate(activeProps, {}) ?? {} : {}; const resolvedInactiveProps = isActive ? {} : functionalUpdate(inactiveProps, {}); const resolvedClassName = [ className, resolvedActiveProps.className, resolvedInactiveProps.className ].filter(Boolean).join(" "); const resolvedStyle = { ...style, ...resolvedActiveProps.style, ...resolvedInactiveProps.style }; return { ...propsSafeToSpread, ...resolvedActiveProps, ...resolvedInactiveProps, href: disabled ? void 0 : next.maskedLocation ? router.history.createHref(next.maskedLocation.href) : router.history.createHref(next.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, ...Object.keys(resolvedStyle).length && { style: resolvedStyle }, ...resolvedClassName && { className: resolvedClassName }, ...disabled && { role: "link", "aria-disabled": true }, ...isActive && { "data-status": "active", "aria-current": "page" }, ...isTransitioning && { "data-transitioning": "transitioning" } }; } 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 (typeof _asChild === "undefined") { 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