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
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,5 @@
- smithki
- istarkov
- louis-young
- bilalk711
- robbtraister
15 changes: 10 additions & 5 deletions docs/components/nav-link.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,57 @@ describe("NavLink", () => {
expect(anchor.props.className).toMatch("active");
});

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'/'", ()=>{
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
Original file line number Diff line number Diff line change
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;
let isActive =
locationPathname === toPathname ||
(!end &&
locationPathname.startsWith(toPathname) &&
locationPathname.charAt(toPathname.length) === "/");
locationPathname.startsWith(toPathname) &&
locationPathname.charAt(endSlashPosition) === "/"
);

let isPending =
nextLocationPathname != null &&
Expand Down