Skip to content

Commit 63960a8

Browse files
authored
Fix relative=path issue (#11006)
1 parent 4ce9f73 commit 63960a8

File tree

5 files changed

+70
-9
lines changed

5 files changed

+70
-9
lines changed

.changeset/fix-relative-path.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
Fix `relative="path"` bug where relative path calculations started from the full location pathname, instead of from the current contextual route pathname.
6+
7+
```jsx
8+
<Route path="/a">
9+
<Route path="/b" element={<Component />}>
10+
<Route path="/c" />
11+
</Route>
12+
</Route>;
13+
14+
function Component() {
15+
return (
16+
<>
17+
{/* This is now correctly relative to /a/b, not /a/b/c */}
18+
<Link to=".." relative="path" />
19+
<Outlet />
20+
</>
21+
);
22+
}
23+
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
},
111111
"filesize": {
112112
"packages/router/dist/router.umd.min.js": {
113-
"none": "49.2 kB"
113+
"none": "49.3 kB"
114114
},
115115
"packages/react-router/dist/react-router.production.min.js": {
116116
"none": "13.9 kB"

packages/router/__tests__/path-resolution-test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,27 @@ describe("path resolution", () => {
451451
expect(router.state.location.pathname).toBe("/a/b/c/d");
452452
router.navigate("/a/b/c/d/e/f");
453453

454+
// Navigating with relative:path from mid-route-hierarchy
455+
router.navigate("..", { relative: "path", fromRouteId: "f" });
456+
expect(router.state.location.pathname).toBe("/a/b/c/d/e");
457+
router.navigate("/a/b/c/d/e/f");
458+
459+
router.navigate("../..", { relative: "path", fromRouteId: "de" });
460+
expect(router.state.location.pathname).toBe("/a/b/c");
461+
router.navigate("/a/b/c/d/e/f");
462+
463+
router.navigate("../..", { relative: "path", fromRouteId: "bc" });
464+
expect(router.state.location.pathname).toBe("/a");
465+
router.navigate("/a/b/c/d/e/f");
466+
467+
// Go up farther than # of URL segments
468+
router.navigate("../../../../../../../../..", {
469+
relative: "path",
470+
fromRouteId: "f",
471+
});
472+
expect(router.state.location.pathname).toBe("/");
473+
router.navigate("/a/b/c/d/e/f");
474+
454475
router.dispose();
455476
});
456477

packages/router/router.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3319,11 +3319,9 @@ function normalizeTo(
33193319
) {
33203320
let contextualMatches: AgnosticDataRouteMatch[];
33213321
let activeRouteMatch: AgnosticDataRouteMatch | undefined;
3322-
if (fromRouteId != null && relative !== "path") {
3322+
if (fromRouteId) {
33233323
// Grab matches up to the calling route so our route-relative logic is
3324-
// relative to the correct source route. When using relative:path,
3325-
// fromRouteId is ignored since that is always relative to the current
3326-
// location path
3324+
// relative to the correct source route
33273325
contextualMatches = [];
33283326
for (let match of matches) {
33293327
contextualMatches.push(match);

packages/router/utils.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,17 +1188,36 @@ export function resolveTo(
11881188
// `to` values that do not provide a pathname. `to` can simply be a search or
11891189
// hash string, in which case we should assume that the navigation is relative
11901190
// to the current location's pathname and *not* the route pathname.
1191-
if (isPathRelative || toPathname == null) {
1191+
if (toPathname == null) {
11921192
from = locationPathname;
1193+
} else if (isPathRelative) {
1194+
let fromSegments = routePathnames[routePathnames.length - 1]
1195+
.replace(/^\//, "")
1196+
.split("/");
1197+
1198+
if (toPathname.startsWith("..")) {
1199+
let toSegments = toPathname.split("/");
1200+
1201+
// With relative="path", each leading .. segment means "go up one URL segment"
1202+
while (toSegments[0] === "..") {
1203+
toSegments.shift();
1204+
fromSegments.pop();
1205+
}
1206+
1207+
to.pathname = toSegments.join("/");
1208+
}
1209+
1210+
from = "/" + fromSegments.join("/");
11931211
} else {
11941212
let routePathnameIndex = routePathnames.length - 1;
11951213

11961214
if (toPathname.startsWith("..")) {
11971215
let toSegments = toPathname.split("/");
11981216

1199-
// Each leading .. segment means "go up one route" instead of "go up one
1200-
// URL segment". This is a key difference from how <a href> works and a
1201-
// major reason we call this a "to" value instead of a "href".
1217+
// With relative="route" (the default), each leading .. segment means
1218+
// "go up one route" instead of "go up one URL segment". This is a key
1219+
// difference from how <a href> works and a major reason we call this a
1220+
// "to" value instead of a "href".
12021221
while (toSegments[0] === "..") {
12031222
toSegments.shift();
12041223
routePathnameIndex -= 1;

0 commit comments

Comments
 (0)