import clsx from "clsx"; import * as React from "react"; import { UNSAFE_FrameworkContext as FrameworkContext, generatePath, NavLink, useSearchParams, type UNSAFE_AssetsManifest as AssetsManifest, } from "react-router"; import { composeEventHandlers, useDebugEvents } from "../../utils"; import { useComposedRefs } from "../../utils/mergeRefs"; import { TooltipSymbol } from "../Tooltip"; import * as styles from "./styles.module.css"; export const LinkContext = React.createContext(false); export const ButtonContext = React.createContext(false); export const handleEnterPress = (e) => { if (e.key === "Enter") { e.target.click(); } }; export type RouteRecord = { id: string; path: string; params?: Record; }; export function resolveRoute(to: RouteRecord, context: AssetsManifest) { if (to) { const routes = context?.routes || globalThis?.__reactRouterManifest?.routes; const route = routes?.[to.id]; if (route) { const fallback = `/${route.path}`; try { const completePath = fullpath(lineage(routes, route)) || fallback; return generatePath(completePath, to.params || {}); } catch (e) { console.error(e); return fallback; } } return `/${to.path}`; } return ""; } function lineage(routes: Record, route: any): any[] { const result: any[] = []; while (route) { result.push(route); if (!route.parentId) break; route = routes[route.parentId]; } result.reverse(); return result; } function fullpath(lineage: any[]) { const route = lineage.at(-1); // root if (lineage.length === 1 && route?.id === "root") return "/"; // layout const isLayout = route && route.index !== true && route.path === undefined; if (isLayout) return undefined; return ( "/" + lineage .map((route) => route.path?.replace(/^\//, "")?.replace(/\/$/, "")) .filter((path) => path !== undefined && path !== "") .join("/") ); } export function InternalLink({ className, to, query = [], prefetch = "intent", newTab = false, preserveSearchParams = false, ref, variant = "none", ...props }: React.ComponentProps<"a"> & { "data-id"?: string; prefetch?: PrefetchBehavior; newTab?: boolean; to: RouteRecord; preserveSearchParams?: boolean; query: { key: string; value: string }[]; variant?: "solid" | "subtle" | "outline" | "elevated" | "line" | "none"; }) { const [searchParams] = useSearchParams(); const linkRef = React.useRef(null); const framework = React.use(FrameworkContext)!; if (!framework) { throw new Error( "InternalLink requires FrameworkContext to be provided, please ensure you are using it within a Router.", ); } const resolved = React.useMemo(() => { return resolveRoute(to, framework.manifest); // eslint-disable-next-line react-hooks/exhaustive-deps }, [to, framework.manifest]); const queryStr = React.useMemo(() => { const search = new URLSearchParams( preserveSearchParams ? searchParams : undefined, ); query?.forEach(({ key, value }) => { search.set(key, value); }); return search.size > 0 ? `?${search.toString()}` : ""; }, [query, searchParams, preserveSearchParams]); const linkProps = { ...props, onClick: composeEventHandlers((e: any) => { e?.stopPropagation(); }, props.onClick), }; if (newTab) { linkProps.target = "_blank"; linkProps.rel = "noopener noreferrer"; } const finalRef = useComposedRefs(linkRef, ref); return ( clsx( styles.link, className, isActive ? "_active" : null, isPending ? "_pending" : null, ) } to={resolved + queryStr} data-variant={variant} data-component="InternalLink$Brevity" prefetch={prefetch} {...linkProps} /> ); } InternalLink[TooltipSymbol] = true; export function ExternalLink({ className, href, newTab = false, onClick, ref, ...rest }: React.ComponentProps<"a"> & { newTab?: boolean; "data-id"?: string }) { const isChild = React.useContext(LinkContext); const props = useDebugEvents(rest); if (newTab) { props.target = "_blank"; props.rel = "noopener noreferrer"; } return ( {isChild ? ( { e.stopPropagation(); e.preventDefault(); if (onClick) { onClick(e as any); } const href = (e.target as any).dataset.href; if (href) { window.location.href = href; } }} {...props} /> ) : ( { e.stopPropagation(); if (onClick) { onClick(e); } }} {...props} /> )} ); } ExternalLink[TooltipSymbol] = true; export type PrefetchBehavior = "intent" | "render" | "none";