@tanstack/react-router
Version:
Modern and scalable routing for React applications
338 lines (337 loc) • 9.8 kB
JavaScript
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