From 1ed7f01c98224e31b049f425a56f9cec0d57396d Mon Sep 17 00:00:00 2001 From: Mark Lawlor Date: Tue, 7 Nov 2023 11:26:21 +1000 Subject: [PATCH] fix relative hrefs inside groups (#900) backport of https://github.com/expo/expo/pull/25111 --- .../src/__tests__/navigation.test.tsx | 104 ++++++++++++++++++ .../expo-router/src/global-state/routing.ts | 32 ++++-- 2 files changed, 126 insertions(+), 10 deletions(-) diff --git a/packages/expo-router/src/__tests__/navigation.test.tsx b/packages/expo-router/src/__tests__/navigation.test.tsx index 9feaa758..027de855 100644 --- a/packages/expo-router/src/__tests__/navigation.test.tsx +++ b/packages/expo-router/src/__tests__/navigation.test.tsx @@ -331,3 +331,107 @@ it("can push & replace with nested Slots", async () => { expect(screen).toHavePathname("/"); expect(screen.getByTestId("index")).toBeOnTheScreen(); }); + +it("can navigation to a relative route without losing path params", async () => { + renderRouter( + { + _layout: () => , + "(group)/[value]/one": () => , + "(group)/[value]/two": () => , + "(group)/[...value]/three": () => , + "(group)/[...value]/four": () => , + }, + { + initialUrl: "/test/one", + } + ); + + expect(screen).toHavePathname("/test/one"); + expect(screen.getByTestId("one")).toBeOnTheScreen(); + + act(() => router.push("./two")); + expect(screen).toHavePathname("/test/two"); + expect(screen.getByTestId("two")).toBeOnTheScreen(); + + act(() => router.push("../apple/one?orange=1")); + expect(screen).toHavePathname("/apple/one"); + expect(screen.getByTestId("one")).toBeOnTheScreen(); + + act(() => router.push("./two")); + expect(screen).toHavePathname("/apple/two"); + expect(screen.getByTestId("two")).toBeOnTheScreen(); + + act(() => router.push("./three")); + expect(screen).toHavePathname("/apple/three"); + expect(screen.getByTestId("three")).toBeOnTheScreen(); + + act(() => router.push("./banana/four")); + expect(screen).toHavePathname("/apple/banana/four"); + expect(screen.getByTestId("four")).toBeOnTheScreen(); + + act(() => router.push("./three")); + expect(screen).toHavePathname("/apple/banana/three"); + expect(screen.getByTestId("three")).toBeOnTheScreen(); +}); + +describe("shared routes with tabs", () => { + function renderSharedTabs() { + renderRouter({ + "(one,two)/_layout": () => , + "(one,two)/one": () => , + "(one,two)/post": () => , + "(two)/two": () => , + _layout: () => , + index: () => , + }); + + expect(screen).toHavePathname("/one"); + } + + describe("tab one (default)", () => { + it("pushes post in tab one using absolute /post", async () => { + renderSharedTabs(); + act(() => router.push("/post")); + expect(screen).toHavePathname("/post"); + expect(screen).toHaveSegments(["(one)", "post"]); + }); + it("pushes post in tab one using absolute /(tabs)/(one)/post", async () => { + renderSharedTabs(); + act(() => router.push("/(one)/post")); + expect(screen).toHavePathname("/post"); + expect(screen).toHaveSegments(["(one)", "post"]); + }); + it("pushes post in tab one using relative ./post", async () => { + renderSharedTabs(); + act(() => router.push("./post")); + expect(screen).toHavePathname("/post"); + expect(screen).toHaveSegments(["(one)", "post"]); + }); + }); + describe("tab two", () => { + // Navigate to tab two before each case here. + beforeEach(() => { + renderSharedTabs(); + act(() => router.push("/two")); + expect(screen).toHavePathname("/two"); + expect(screen).toHaveSegments(["(two)", "two"]); + }); + + it("pushes post in tab two with absolute `/post` goes to default tab", async () => { + act(() => router.push("/post")); + expect(screen).toHavePathname("/post"); + expect(screen).toHaveSegments(["(one)", "post"]); + }); + it("pushes post in tab two using absolute /(tabs)/(two)/post", async () => { + act(() => router.push("/(two)/post")); + expect(screen).toHavePathname("/post"); + expect(screen).toHaveSegments(["(two)", "post"]); + }); + it("pushes post in tab two using relative ./post", async () => { + // Pushing `./post` should preserve the relative position in tab two and NOT swap to the default tab one variation of the `/post` route. + act(() => router.push("./post")); + expect(screen).toHavePathname("/post"); + expect(screen).toHaveSegments(["(two)", "post"]); + }); + }); +}); diff --git a/packages/expo-router/src/global-state/routing.ts b/packages/expo-router/src/global-state/routing.ts index f3b228af..0e23ac8a 100644 --- a/packages/expo-router/src/global-state/routing.ts +++ b/packages/expo-router/src/global-state/routing.ts @@ -78,16 +78,28 @@ export function linkTo(this: RouterStore, href: string, event?: string) { const rootState = navigationRef.getRootState(); if (href.startsWith(".")) { - let base = - this.linking.getPathFromState?.(rootState, { - screens: [], - preserveGroups: true, - }) ?? ""; - - if (base && !base.endsWith("/")) { - base += "/.."; - } - href = resolve(base, href); + const base = + this.routeInfo?.segments + ?.map((segment) => { + if (!segment.startsWith("[")) return segment; + + if (segment.startsWith("[...")) { + segment = segment.slice(4, -1); + const params = this.routeInfo?.params?.[segment]; + if (Array.isArray(params)) { + return params.join("/"); + } else { + return params?.split(",")?.join("/") ?? ""; + } + } else { + segment = segment.slice(1, -1); + return this.routeInfo?.params?.[segment]; + } + }) + .filter(Boolean) + .join("/") ?? "/"; + + href = resolve(base + "/..", href); } const state = this.linking.getStateFromPath!(href, this.linking.config);