File size: 2,395 Bytes
9b906ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | import React from "react";
import {
useNavigation,
type NavigationOptions,
} from "#/context/navigation-context";
interface NavigationLinkClassNameState {
isActive: boolean;
}
export interface NavigationLinkProps extends Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
"className" | "href"
> {
to: string;
replace?: boolean;
end?: boolean;
className?:
| string
| ((state: NavigationLinkClassNameState) => string | undefined);
}
function isModifiedEvent(event: React.MouseEvent<HTMLAnchorElement>) {
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
}
/**
* `currentPath` is a pathname, so compare against the pathname portion of
* `to`. Links may carry a query string (e.g. the backend identity pinned on
* sidebar conversation links) that must not defeat the active check.
*/
function toPathname(to: string) {
return to.split(/[?#]/)[0];
}
function isPathActive(currentPath: string, to: string, end: boolean) {
const path = toPathname(to);
if (path === "/") {
return currentPath === path;
}
if (end) {
return currentPath === path;
}
return currentPath === path || currentPath.startsWith(`${path}/`);
}
export const NavigationLink = React.forwardRef<
HTMLAnchorElement,
NavigationLinkProps
>(
(
{
to,
replace = false,
end = false,
onClick,
className,
children,
target,
rel,
...props
},
ref,
) => {
const { currentPath, navigate } = useNavigation();
const isActive = isPathActive(currentPath, to, end);
const resolvedClassName =
typeof className === "function" ? className({ isActive }) : className;
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
onClick?.(event);
if (
event.defaultPrevented ||
event.button !== 0 ||
isModifiedEvent(event) ||
target === "_blank"
) {
return;
}
event.preventDefault();
navigate(to, { replace } satisfies NavigationOptions);
};
return (
<a
{...props}
ref={ref}
href={to}
target={target}
rel={rel}
onClick={handleClick}
className={resolvedClassName}
aria-current={isActive ? "page" : undefined}
>
{children}
</a>
);
},
);
NavigationLink.displayName = "NavigationLink";
|