@tanstack/react-router
Version:
Modern and scalable routing for React applications
305 lines (304 loc) • 8.97 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, 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