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";