diff --git a/.changeset/unlucky-hats-play.md b/.changeset/unlucky-hats-play.md new file mode 100644 index 000000000..7ccf2de7c --- /dev/null +++ b/.changeset/unlucky-hats-play.md @@ -0,0 +1,5 @@ +--- +"react-router-dom": patch +--- + +Fix NavLink behavior for root urls diff --git a/packages/react-router-dom/__tests__/nav-link-active-test.tsx b/packages/react-router-dom/__tests__/nav-link-active-test.tsx index 8b9da3c44..4e00e30fe 100644 --- a/packages/react-router-dom/__tests__/nav-link-active-test.tsx +++ b/packages/react-router-dom/__tests__/nav-link-active-test.tsx @@ -288,6 +288,57 @@ describe("NavLink", () => { expect(anchors.map((a) => a.props.className)).toEqual(["active", ""]); }); + + it("does not automatically apply to root non-layout segments", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + Root} /> + Root} + > + + + ); + }); + + let anchor = renderer.root.findByType("a"); + + expect(anchor.props.className).not.toMatch("active"); + }); + + it("does not automatically apply to root layout segments", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + +

Root

+ + + } + > + Root} + > +
+
+
+ ); + }); + + let anchor = renderer.root.findByType("a"); + + expect(anchor.props.className).not.toMatch("active"); + }); }); describe("when it matches just the beginning but not to the end", () => { diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 9c1e46fda..6cd01dc86 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -442,24 +442,36 @@ export const NavLink = React.forwardRef( ref ) { let path = useResolvedPath(to, { relative: rest.relative }); - let match = useMatch({ path: path.pathname, end, caseSensitive }); - + let location = useLocation(); let routerState = React.useContext(DataRouterStateContext); - let nextLocation = routerState?.navigation.location; - let nextPath = useResolvedPath(nextLocation || ""); - let nextMatch = React.useMemo( - () => - nextLocation - ? matchPath( - { path: path.pathname, end, caseSensitive }, - nextPath.pathname - ) - : null, - [nextLocation, path.pathname, caseSensitive, end, nextPath.pathname] - ); - let isPending = nextMatch != null; - let isActive = match != null; + let toPathname = 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(); + } + + let isActive = + locationPathname === toPathname || + (!end && + locationPathname.startsWith(toPathname) && + locationPathname.charAt(toPathname.length) === "/"); + + let isPending = + nextLocationPathname != null && + (nextLocationPathname === toPathname || + (!end && + nextLocationPathname.startsWith(toPathname) && + nextLocationPathname.charAt(toPathname.length) === "/")); let ariaCurrent = isActive ? ariaCurrentProp : undefined;