|
import React from "react"; |
|
|
|
import type { LinkProps } from "@remix-run/react"; |
|
import { stripBasename } from "@remix-run/router"; |
|
|
|
import { |
|
UNSAFE_DataRouterStateContext as DataRouterStateContext, |
|
UNSAFE_NavigationContext as NavigationContext, |
|
useLocation, |
|
useResolvedPath, |
|
} from "react-router"; |
|
import { unstable_useViewTransitionState as useViewTransitionState } from "react-router-dom"; |
|
|
|
/*eslint prefer-const: "off"*/ |
|
|
|
export type NavLinkRenderProps = { |
|
isActive: boolean; |
|
isPending: boolean; |
|
isTransitioning: boolean; |
|
}; |
|
|
|
export interface NavProps |
|
extends Pick<LinkProps, "to" | "unstable_viewTransition" | "relative"> { |
|
children?: (props: NavLinkRenderProps) => React.ReactNode; |
|
caseSensitive?: boolean; |
|
end?: boolean; |
|
} |
|
|
|
/** |
|
* A component wrapper that knows if it's "active" or not. Adapted from NavLink. |
|
*/ |
|
export function Nav({ |
|
caseSensitive = false, |
|
end = false, |
|
to, |
|
unstable_viewTransition, |
|
children, |
|
...rest |
|
}: NavProps) { |
|
let path = useResolvedPath(to, { relative: rest.relative }); |
|
let location = useLocation(); |
|
let routerState = React.useContext(DataRouterStateContext); |
|
let { navigator, basename } = React.useContext(NavigationContext); |
|
let isTransitioning = |
|
routerState != null && |
|
// Conditional usage is OK here because the usage of a data router is static |
|
// eslint-disable-next-line react-hooks/rules-of-hooks |
|
useViewTransitionState(path) && |
|
unstable_viewTransition === true; |
|
|
|
let toPathname = navigator.encodeLocation |
|
? navigator.encodeLocation(path).pathname |
|
: path.pathname; |
|
let locationPathname = location.pathname; |
|
let nextLocationPathname = |
|
routerState && routerState.navigation && routerState.navigation.location |
|
? routerState.navigation.location.pathname |
|
: null; |
|
|
|
if (!caseSensitive) { |
|
locationPathname = locationPathname.toLowerCase(); |
|
nextLocationPathname = nextLocationPathname |
|
? nextLocationPathname.toLowerCase() |
|
: null; |
|
toPathname = toPathname.toLowerCase(); |
|
} |
|
|
|
if (nextLocationPathname && basename) { |
|
nextLocationPathname = |
|
stripBasename(nextLocationPathname, basename) || nextLocationPathname; |
|
} |
|
|
|
// If the `to` has a trailing slash, look at that exact spot. Otherwise, |
|
// we're looking for a slash _after_ what's in `to`. For example: |
|
// |
|
// <NavLink to="/users"> and <NavLink to="/users/"> |
|
// both want to look for a / at index 6 to match URL `/users/matt` |
|
const endSlashPosition = |
|
toPathname !== "/" && toPathname.endsWith("/") |
|
? toPathname.length - 1 |
|
: toPathname.length; |
|
let isActive = |
|
locationPathname === toPathname || |
|
(!end && |
|
locationPathname.startsWith(toPathname) && |
|
locationPathname.charAt(endSlashPosition) === "/"); |
|
|
|
let isPending = |
|
nextLocationPathname != null && |
|
(nextLocationPathname === toPathname || |
|
(!end && |
|
nextLocationPathname.startsWith(toPathname) && |
|
nextLocationPathname.charAt(toPathname.length) === "/")); |
|
|
|
let renderProps = { |
|
isActive, |
|
isPending, |
|
isTransitioning, |
|
}; |
|
|
|
return typeof children === "function" ? children(renderProps) : children; |
|
} |
Since I was targeting Remix, I used those imports where relevant, but there should be equivalent export
LinkProps
from React Router. Most of the imports are already from react router.