Skip to content

Commit

Permalink
feat: allow <Link> to link to external urls (#9900)
Browse files Browse the repository at this point in the history
* feat: allow Link's "to" prop to accept external urls

Signed-off-by: Logan McAnsh <logan@mcan.sh>

* chore: update external link detection to account for no protocol

Signed-off-by: Logan McAnsh <logan@mcan.sh>

* Update index.tsx

* Update index.tsx

* chore: update absolute url checking per #9900 (comment)

Signed-off-by: Logan McAnsh <logan@mcan.sh>

* Create silent-oranges-pay.md

* chore: bump bundle size

Signed-off-by: Logan McAnsh <logan@mcan.sh>

* chore: add browser check

Signed-off-by: Logan McAnsh <logan@mcan.sh>

* Preserve absolute same origin and don't add listener on external links

* chore: bump bundle size

Signed-off-by: Logan McAnsh <logan@mcan.sh>

Signed-off-by: Logan McAnsh <logan@mcan.sh>
Co-authored-by: Matt Brophy <matt@brophy.org>
  • Loading branch information
mcansh and brophdawg11 authored Jan 18, 2023
1 parent 2cd8266 commit e9f271d
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 5 deletions.
11 changes: 11 additions & 0 deletions .changeset/silent-oranges-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"react-router": patch
"react-router-dom": patch
---

allow using `<Link>` with external URLs

```tsx
<Link to="//example.com/some/path">
<Link to="https://www.currentorigin.com/path">
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"none": "11 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "16.5 kB"
"none": "16.7 kB"
}
}
}
39 changes: 39 additions & 0 deletions packages/react-router-dom/__tests__/link-href-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,45 @@ describe("<Link> href", () => {
["/about", "/about"]
);
});

test('<Link to="https://remix.run"> is treated as external link', () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/inbox/messages"]}>
<Routes>
<Route path="inbox">
<Route
path="messages"
element={<Link to="https://remix.run" />}
/>
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.root.findByType("a").props.href).toEqual(
"https://remix.run"
);
});

test('<Link to="//remix.run"> is treated as external link', () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/inbox/messages"]}>
<Routes>
<Route path="inbox">
<Route path="messages" element={<Link to="//remix.run" />} />
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.root.findByType("a").props.href).toEqual("//remix.run");
});
});

describe("in a dynamic route", () => {
Expand Down
37 changes: 33 additions & 4 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,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 <a>.
*/
Expand All @@ -412,8 +417,32 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
},
ref
) {
let href = useHref(to, { relative });
let internalOnClick = useLinkClickHandler(to, {
// `location` is the unaltered href we will render in the <a> 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 <a> tag for relative URLs
let href = useHref(navigationLocation, { relative });

let internalOnClick = useLinkClickHandler(navigationLocation, {
replace,
state,
target,
Expand All @@ -433,8 +462,8 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
{...rest}
href={href}
onClick={reloadDocument ? onClick : handleClick}
href={isAbsolute ? location : href}
onClick={isExternal || reloadDocument ? onClick : handleClick}
ref={ref}
target={target}
/>
Expand Down

0 comments on commit e9f271d

Please sign in to comment.