Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed NavLink Issue #10734

Merged
merged 12 commits into from
Oct 31, 2023
Merged
1 change: 1 addition & 0 deletions contributors.yml
Expand Up @@ -228,4 +228,5 @@
- smithki
- istarkov
- louis-young
- bilalk711
- robbtraister
15 changes: 10 additions & 5 deletions docs/components/nav-link.md
Expand Up @@ -84,28 +84,33 @@ You can pass a render prop as children to customize the content of the `<NavLink

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.

Without the end prop, this link is always active because every URL matches `/`.
For example, suppose we have an url `/tasks`

```tsx
<NavLink to="/">Home</NavLink>
<NavLink to="/tasks">Tasks</NavLink>
```
The link will be active for sub-paths such as `/tasks/123`


To match the URL "to the end" of `to`, use `end`:

```tsx
<NavLink to="/" end>
Home
<NavLink to="/tasks" end>
Tasks
</NavLink>
```

Now this link will only be active at `"/"`. This works for paths with more segments as well:
Now this link will only be active at `"/tasks"`. This works for paths with more segments as well:

| Link | URL | isActive |
| ----------------------------- | ------------ | -------- |
| `<NavLink to="/tasks" />` | `/tasks` | true |
| `<NavLink to="/tasks" />` | `/tasks/123` | true |
| `<NavLink to="/tasks" end />` | `/tasks` | true |
| `<NavLink to="/tasks" end />` | `/tasks/123` | false |
| `<NavLink to="/tasks/" end />`| `/tasks` | false |
| `<NavLink to="/tasks/" end />`| `/tasks/` | true |
| `<NavLink to="/tasks/" end />`| `/tasks` | false |

## `caseSensitive`

Expand Down
51 changes: 51 additions & 0 deletions packages/react-router-dom/__tests__/nav-link-active-test.tsx
Expand Up @@ -397,6 +397,57 @@ describe("NavLink", () => {
expect(anchor.props.className).toMatch("active");
});

bilalk711 marked this conversation as resolved.
Show resolved Hide resolved
it("In case of trailing slash at the end of link", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/home/child"]}>
<Routes>
<Route
path="home"
element={
<div>
<NavLink to="/home/">Home</NavLink>
<Outlet />
</div>
}
>
<Route path="child" element={<div>Child</div>} />
</Route>
</Routes>
</MemoryRouter>
);
});

let anchor = renderer.root.findByType("a");
expect(anchor.props.className).toMatch("active");
});

it("does not apply the default 'active' className to the underlying <a> when at root'/'", ()=>{
brophdawg11 marked this conversation as resolved.
Show resolved Hide resolved
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/"]}>
<Routes>
<Route
path="/"
element={
<div>
<NavLink to="child">Child</NavLink>
<Outlet />
</div>
}
>
</Route>
</Routes>
</MemoryRouter>
);
});

let anchor = renderer.root.findByType("a");
expect(anchor.props.className).not.toMatch("active");
});

describe("when end=true", () => {
it("does not apply the default 'active' className to the underlying <a>", () => {
let renderer: TestRenderer.ReactTestRenderer;
Expand Down
7 changes: 4 additions & 3 deletions packages/react-router-dom/index.tsx
Expand Up @@ -657,12 +657,13 @@ export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
: null;
toPathname = toPathname.toLowerCase();
}

const endSlashPosition = (toPathname.length > 1 && toPathname[toPathname.length - 1] === "/") ? toPathname.length - 1 : toPathname.length;
brophdawg11 marked this conversation as resolved.
Show resolved Hide resolved
let isActive =
locationPathname === toPathname ||
(!end &&
locationPathname.startsWith(toPathname) &&
locationPathname.charAt(toPathname.length) === "/");
locationPathname.startsWith(toPathname) &&
locationPathname.charAt(endSlashPosition) === "/"
);

let isPending =
nextLocationPathname != null &&
Expand Down