Skip to content

Commit e6ac5f0

Browse files
bilalk711Bilal Kazmibilal0711brophdawg11
authored
Fixed NavLink Issue (#10734)
Co-authored-by: Bilal Kazmi <[email protected]> Co-authored-by: Bilal Kazmi <[email protected]> Co-authored-by: Matt Brophy <[email protected]>
1 parent 40701e5 commit e6ac5f0

File tree

5 files changed

+132
-7
lines changed

5 files changed

+132
-7
lines changed

.changeset/itchy-items-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Fix `NavLink` `active` logic when `to` location has a trailing slash

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- bbrowning918
3232
- BDomzalski
3333
- bhbs
34+
- bilalk711
3435
- bobziroll
3536
- BrianT1414
3637
- brockross

docs/components/nav-link.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,14 @@ You can pass a render prop as children to customize the content of the `<NavLink
9393

9494
The `end` prop changes the matching logic for the `active` and `pending` states to only match to the "end" of the NavLink's `to` path. If the URL is longer than `to`, it will no longer be considered active.
9595

96-
| Link | URL | isActive |
97-
| ----------------------------- | ------------ | -------- |
98-
| `<NavLink to="/tasks" />` | `/tasks` | true |
99-
| `<NavLink to="/tasks" />` | `/tasks/123` | true |
100-
| `<NavLink to="/tasks" end />` | `/tasks` | true |
101-
| `<NavLink to="/tasks" end />` | `/tasks/123` | false |
96+
| Link | Current URL | isActive |
97+
| ------------------------------ | ------------ | -------- |
98+
| `<NavLink to="/tasks" />` | `/tasks` | true |
99+
| `<NavLink to="/tasks" />` | `/tasks/123` | true |
100+
| `<NavLink to="/tasks" end />` | `/tasks` | true |
101+
| `<NavLink to="/tasks" end />` | `/tasks/123` | false |
102+
| `<NavLink to="/tasks/" end />` | `/tasks` | false |
103+
| `<NavLink to="/tasks/" end />` | `/tasks/` | true |
102104

103105
**A note on links to the root route**
104106

packages/react-router-dom/__tests__/nav-link-active-test.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ describe("NavLink", () => {
106106
expect(anchor.props.className).toMatch("active");
107107
});
108108

109+
it("when the current URL has a trailing slash", () => {
110+
let renderer: TestRenderer.ReactTestRenderer;
111+
TestRenderer.act(() => {
112+
renderer = TestRenderer.create(
113+
<MemoryRouter initialEntries={["/home/"]}>
114+
<Routes>
115+
<Route
116+
path="/home"
117+
element={<NavLink to="/home/">Home</NavLink>}
118+
/>
119+
</Routes>
120+
</MemoryRouter>
121+
);
122+
});
123+
124+
let anchor = renderer.root.findByType("a");
125+
126+
expect(anchor.props.className).toMatch("active");
127+
});
128+
109129
it("applies its className correctly when provided as a function", () => {
110130
let renderer: TestRenderer.ReactTestRenderer;
111131
TestRenderer.act(() => {
@@ -433,6 +453,32 @@ describe("NavLink", () => {
433453
expect(anchor.props.className).toMatch("active");
434454
});
435455

456+
it("In case of trailing slash at the end of link", () => {
457+
let renderer: TestRenderer.ReactTestRenderer;
458+
TestRenderer.act(() => {
459+
renderer = TestRenderer.create(
460+
<MemoryRouter initialEntries={["/home/child"]}>
461+
<Routes>
462+
<Route
463+
path="home"
464+
element={
465+
<div>
466+
<NavLink to="/home/">Home</NavLink>
467+
<Outlet />
468+
</div>
469+
}
470+
>
471+
<Route path="child" element={<div>Child</div>} />
472+
</Route>
473+
</Routes>
474+
</MemoryRouter>
475+
);
476+
});
477+
478+
let anchor = renderer.root.findByType("a");
479+
expect(anchor.props.className).toMatch("active");
480+
});
481+
436482
describe("when end=true", () => {
437483
it("does not apply the default 'active' className to the underlying <a>", () => {
438484
let renderer: TestRenderer.ReactTestRenderer;
@@ -462,6 +508,68 @@ describe("NavLink", () => {
462508

463509
expect(anchor.props.className).not.toMatch("active");
464510
});
511+
512+
it("Handles trailing slashes accordingly when the URL does not have a trailing slash", () => {
513+
let renderer: TestRenderer.ReactTestRenderer;
514+
TestRenderer.act(() => {
515+
renderer = TestRenderer.create(
516+
<MemoryRouter initialEntries={["/home"]}>
517+
<Routes>
518+
<Route
519+
path="home"
520+
element={
521+
<div>
522+
<NavLink to="/home" end>
523+
Home
524+
</NavLink>
525+
<NavLink to="/home/" end>
526+
Home
527+
</NavLink>
528+
<Outlet />
529+
</div>
530+
}
531+
>
532+
<Route path="child" element={<div>Child</div>} />
533+
</Route>
534+
</Routes>
535+
</MemoryRouter>
536+
);
537+
});
538+
539+
let anchors = renderer.root.findAllByType("a");
540+
expect(anchors.map((a) => a.props.className)).toEqual(["active", ""]);
541+
});
542+
543+
it("Handles trailing slashes accordingly when the URL has a trailing slash", () => {
544+
let renderer: TestRenderer.ReactTestRenderer;
545+
TestRenderer.act(() => {
546+
renderer = TestRenderer.create(
547+
<MemoryRouter initialEntries={["/home/"]}>
548+
<Routes>
549+
<Route
550+
path="home"
551+
element={
552+
<div>
553+
<NavLink to="/home" end>
554+
Home
555+
</NavLink>
556+
<NavLink to="/home/" end>
557+
Home
558+
</NavLink>
559+
<Outlet />
560+
</div>
561+
}
562+
>
563+
<Route path="child" element={<div>Child</div>} />
564+
</Route>
565+
</Routes>
566+
</MemoryRouter>
567+
);
568+
});
569+
570+
let anchors = renderer.root.findAllByType("a");
571+
expect(anchors.map((a) => a.props.className)).toEqual(["", "active"]);
572+
});
465573
});
466574
});
467575

packages/react-router-dom/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -964,11 +964,20 @@ export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
964964
toPathname = toPathname.toLowerCase();
965965
}
966966

967+
// If the `to` has a trailing slash, look at that exact spot. Otherwise,
968+
// we're looking for a slash _after_ what's in `to`. For example:
969+
//
970+
// <NavLink to="/users"> and <NavLink to="/users/">
971+
// both want to look for a / at index 6 to match URL `/users/matt`
972+
const endSlashPosition =
973+
toPathname !== "/" && toPathname.endsWith("/")
974+
? toPathname.length - 1
975+
: toPathname.length;
967976
let isActive =
968977
locationPathname === toPathname ||
969978
(!end &&
970979
locationPathname.startsWith(toPathname) &&
971-
locationPathname.charAt(toPathname.length) === "/");
980+
locationPathname.charAt(endSlashPosition) === "/");
972981

973982
let isPending =
974983
nextLocationPathname != null &&

0 commit comments

Comments
 (0)