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 + + +``` diff --git a/package.json b/package.json index 037d859131..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.5 kB" + "none": "16.7 kB" } } } diff --git a/packages/react-router-dom/__tests__/link-href-test.tsx b/packages/react-router-dom/__tests__/link-href-test.tsx index 36a5d9ed6d..92b2e828e9 100644 --- a/packages/react-router-dom/__tests__/link-href-test.tsx +++ b/packages/react-router-dom/__tests__/link-href-test.tsx @@ -89,6 +89,45 @@ 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" + ); + }); + + 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 cf0fd14240..9d630a7da9 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 . */ @@ -409,8 +414,32 @@ export const Link = React.forwardRef( }, ref ) { - let href = useHref(to, { relative }); - let internalOnClick = useLinkClickHandler(to, { + // `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) { + // Strip the protocol/origin for same-origin absolute URLs + navigationLocation = + targetUrl.pathname + targetUrl.search + targetUrl.hash; + } else { + isExternal = true; + } + } + + // `href` is what we render in the tag for relative URLs + let href = useHref(navigationLocation, { relative }); + + let internalOnClick = useLinkClickHandler(navigationLocation, { replace, state, target, @@ -430,8 +459,8 @@ export const Link = React.forwardRef( // eslint-disable-next-line jsx-a11y/anchor-has-content