From 80b977b8c940e321554f0c8c9d94ad169faa9704 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Fri, 13 Jan 2023 12:30:21 -0500 Subject: [PATCH 01/10] feat: allow Link's "to" prop to accept external urls Signed-off-by: Logan McAnsh --- .../__tests__/link-href-test.tsx | 22 +++++++++++++++++++ packages/react-router-dom/index.tsx | 6 +++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/react-router-dom/__tests__/link-href-test.tsx b/packages/react-router-dom/__tests__/link-href-test.tsx index 36a5d9ed6d..0704c0e72d 100644 --- a/packages/react-router-dom/__tests__/link-href-test.tsx +++ b/packages/react-router-dom/__tests__/link-href-test.tsx @@ -89,6 +89,28 @@ describe(" href", () => { ["/about", "/about"] ); }); + + test(' is treated as external link', () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + + } + /> + + + + ); + }); + + expect(renderer.root.findByType("a").props.href).toEqual( + "https://remix.run" + ); + }); }); describe("in a dynamic route", () => { diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index cf0fd14240..b4c6940c00 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -409,6 +409,8 @@ export const Link = React.forwardRef( }, ref ) { + let toString = to.toString(); + let isExternal = /^[a-z]+:/.test(toString); let href = useHref(to, { relative }); let internalOnClick = useLinkClickHandler(to, { replace, @@ -430,8 +432,8 @@ export const Link = React.forwardRef( // eslint-disable-next-line jsx-a11y/anchor-has-content From aae933c8d4e16e8525572b09893eb2a6268c3dce Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Fri, 13 Jan 2023 13:05:21 -0500 Subject: [PATCH 02/10] chore: update external link detection to account for no protocol Signed-off-by: Logan McAnsh --- .../__tests__/link-href-test.tsx | 17 +++++++++++++++++ packages/react-router-dom/index.tsx | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/react-router-dom/__tests__/link-href-test.tsx b/packages/react-router-dom/__tests__/link-href-test.tsx index 0704c0e72d..92b2e828e9 100644 --- a/packages/react-router-dom/__tests__/link-href-test.tsx +++ b/packages/react-router-dom/__tests__/link-href-test.tsx @@ -111,6 +111,23 @@ describe(" href", () => { "https://remix.run" ); }); + + test(' is treated as external link', () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + + } /> + + + + ); + }); + + expect(renderer.root.findByType("a").props.href).toEqual("//remix.run"); + }); }); describe("in a dynamic route", () => { diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index b4c6940c00..7b4dfffb1f 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -409,9 +409,9 @@ export const Link = React.forwardRef( }, ref ) { - let toString = to.toString(); - let isExternal = /^[a-z]+:/.test(toString); - let href = useHref(to, { relative }); + let toString = typeof to === "string" ? to : createPath(to); + let isExternal = toString.startsWith("//") || /^[a-z]+:/.test(toString); + let href = useHref(toString, { relative }); let internalOnClick = useLinkClickHandler(to, { replace, state, From 9f3f1be9a8730ee4dcdc269740e9798bf8a348fe Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Sun, 15 Jan 2023 21:43:45 -0500 Subject: [PATCH 03/10] Update index.tsx --- packages/react-router-dom/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 7b4dfffb1f..9dc6d35e03 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -410,7 +410,7 @@ export const Link = React.forwardRef( ref ) { let toString = typeof to === "string" ? to : createPath(to); - let isExternal = toString.startsWith("//") || /^[a-z]+:/.test(toString); + let isExternal = let isAbsolute = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(toString); let href = useHref(toString, { relative }); let internalOnClick = useLinkClickHandler(to, { replace, From 666939c357ff9d72f9390e89d7be513e4b744d2f Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Sun, 15 Jan 2023 21:48:04 -0500 Subject: [PATCH 04/10] Update index.tsx --- packages/react-router-dom/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 9dc6d35e03..11b8e14f76 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -410,7 +410,7 @@ export const Link = React.forwardRef( ref ) { let toString = typeof to === "string" ? to : createPath(to); - let isExternal = let isAbsolute = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(toString); + let isExternal = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(toString); let href = useHref(toString, { relative }); let internalOnClick = useLinkClickHandler(to, { replace, From 86e1c152f5e7c3869f9d040de3df78b8bae547c7 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Tue, 17 Jan 2023 16:22:12 -0500 Subject: [PATCH 05/10] chore: update absolute url checking per https://github.com/remix-run/react-router/pull/9900#discussion_r1072458181 Signed-off-by: Logan McAnsh --- packages/react-router-dom/index.tsx | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 11b8e14f76..566c570542 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -409,9 +409,21 @@ export const Link = React.forwardRef( }, ref ) { - let toString = typeof to === "string" ? to : createPath(to); - let isExternal = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(toString); - let href = useHref(toString, { relative }); + let location = typeof to === "string" ? to : createPath(to); + let isAbsolute = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(location); + + if (isAbsolute) { + let currentUrl = new URL(window.location.href); + let targetUrl = location.startsWith("//") + ? new URL(currentUrl.protocol + location) + : new URL(location); + if (targetUrl.origin === currentUrl.origin) { + location = targetUrl.pathname + targetUrl.search + targetUrl.hash; + } + } + + let href = useHref(location, { relative }); + let internalOnClick = useLinkClickHandler(to, { replace, state, @@ -432,8 +444,8 @@ export const Link = React.forwardRef( // eslint-disable-next-line jsx-a11y/anchor-has-content From 8a916a5bd34bd37058725d0434023c8c7a27f56e Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Tue, 17 Jan 2023 16:26:29 -0500 Subject: [PATCH 06/10] Create silent-oranges-pay.md --- .changeset/silent-oranges-pay.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/silent-oranges-pay.md diff --git a/.changeset/silent-oranges-pay.md b/.changeset/silent-oranges-pay.md new file mode 100644 index 0000000000..e1dd391960 --- /dev/null +++ b/.changeset/silent-oranges-pay.md @@ -0,0 +1,11 @@ +--- +"react-router": patch +"react-router-dom": patch +--- + +allow using `` with external URLs + +```tsx + + +``` From 0dc51fb458d0efd9941180fd59f6da5d24f8ea4b Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Tue, 17 Jan 2023 16:35:37 -0500 Subject: [PATCH 07/10] chore: bump bundle size Signed-off-by: Logan McAnsh --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 037d859131..257fa03296 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "none": "11 kB" }, "packages/react-router-dom/dist/umd/react-router-dom.production.min.js": { - "none": "16.5 kB" + "none": "16.6 kB" } } } From 727a824306857093be797e1ed89b5b49b6a5f0b5 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Tue, 17 Jan 2023 16:59:08 -0500 Subject: [PATCH 08/10] chore: add browser check Signed-off-by: Logan McAnsh --- packages/react-router-dom/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 566c570542..a7c64262f4 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -391,6 +391,11 @@ export interface LinkProps to: To; } +const isBrowser = + typeof window !== "undefined" && + typeof window.document !== "undefined" && + typeof window.document.createElement !== "undefined"; + /** * The public API for rendering a history-aware . */ @@ -410,9 +415,10 @@ export const Link = React.forwardRef( ref ) { let location = typeof to === "string" ? to : createPath(to); - let isAbsolute = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(location); + let isAbsolute = + /^[a-z+]+:\/\//i.test(location) || location.startsWith("//"); - if (isAbsolute) { + if (isBrowser && isAbsolute) { let currentUrl = new URL(window.location.href); let targetUrl = location.startsWith("//") ? new URL(currentUrl.protocol + location) From e165b7c0829e12dea7519e31ab1325636b66dd55 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 18 Jan 2023 12:53:47 -0500 Subject: [PATCH 09/10] Preserve absolute same origin and don't add listener on external links --- packages/react-router-dom/index.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index a7c64262f4..9d630a7da9 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -414,23 +414,32 @@ export const Link = React.forwardRef( }, ref ) { + // `location` is the unaltered href we will render in the tag for absolute URLs let location = typeof to === "string" ? to : createPath(to); let isAbsolute = /^[a-z+]+:\/\//i.test(location) || location.startsWith("//"); + // Location to use in the click handler + let navigationLocation = location; + let isExternal = false; if (isBrowser && isAbsolute) { let currentUrl = new URL(window.location.href); let targetUrl = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location); if (targetUrl.origin === currentUrl.origin) { - location = targetUrl.pathname + targetUrl.search + targetUrl.hash; + // Strip the protocol/origin for same-origin absolute URLs + navigationLocation = + targetUrl.pathname + targetUrl.search + targetUrl.hash; + } else { + isExternal = true; } } - let href = useHref(location, { relative }); + // `href` is what we render in the tag for relative URLs + let href = useHref(navigationLocation, { relative }); - let internalOnClick = useLinkClickHandler(to, { + let internalOnClick = useLinkClickHandler(navigationLocation, { replace, state, target, @@ -451,7 +460,7 @@ export const Link = React.forwardRef( From 30e9e3ea378c7c1d86a4664c66f6df9946d078bb Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Wed, 18 Jan 2023 13:22:02 -0500 Subject: [PATCH 10/10] chore: bump bundle size Signed-off-by: Logan McAnsh --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 257fa03296..bd5f2beedc 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "none": "11 kB" }, "packages/react-router-dom/dist/umd/react-router-dom.production.min.js": { - "none": "16.6 kB" + "none": "16.7 kB" } } }