Skip to content

Commit d43e1ab

Browse files
authored
fix: treat absolute/same-origin/different-basename <Link to> values as external (#10135)
1 parent 1d2417b commit d43e1ab

File tree

5 files changed

+129
-5
lines changed

5 files changed

+129
-5
lines changed

.changeset/big-olives-doubt.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Treat absolute/same-origin/different-basename <Link to> values as external

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"none": "15 kB"
115115
},
116116
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
117-
"none": "11.5 kB"
117+
"none": "11.6 kB"
118118
},
119119
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
120120
"none": "17.5 kB"

packages/react-router-dom/__tests__/link-click-test.tsx

+72-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ describe("A <Link> click", () => {
138138
<div>
139139
<h1>Home</h1>
140140
<Link
141-
reloadDocument
142141
to="https://remix.run"
143142
onClick={(e) => {
144143
handlerCalled = true;
@@ -171,6 +170,78 @@ describe("A <Link> click", () => {
171170
});
172171
});
173172

173+
describe("when a same-origin/different-basename absolute URL is specified", () => {
174+
it("does not prevent default", () => {
175+
function Home() {
176+
return (
177+
<div>
178+
<h1>Home</h1>
179+
<Link to="http://localhost/not/base">About</Link>
180+
</div>
181+
);
182+
}
183+
184+
act(() => {
185+
ReactDOM.createRoot(node).render(
186+
<MemoryRouter initialEntries={["/base/home"]} basename="/base">
187+
<Routes>
188+
<Route path="home" element={<Home />} />
189+
</Routes>
190+
</MemoryRouter>
191+
);
192+
});
193+
194+
let anchor = node.querySelector("a");
195+
expect(anchor).not.toBeNull();
196+
197+
let event: MouseEvent;
198+
act(() => {
199+
event = click(anchor);
200+
});
201+
202+
expect(event.defaultPrevented).toBe(false);
203+
});
204+
205+
it("calls provided listener", () => {
206+
let handlerCalled;
207+
let defaultPrevented;
208+
209+
function Home() {
210+
return (
211+
<div>
212+
<h1>Home</h1>
213+
<Link
214+
to="http://localhost/not/base"
215+
onClick={(e) => {
216+
handlerCalled = true;
217+
defaultPrevented = e.defaultPrevented;
218+
}}
219+
>
220+
About
221+
</Link>
222+
</div>
223+
);
224+
}
225+
226+
act(() => {
227+
ReactDOM.createRoot(node).render(
228+
<MemoryRouter initialEntries={["/base/home"]} basename="/base">
229+
<Routes>
230+
<Route path="home" element={<Home />} />
231+
</Routes>
232+
</MemoryRouter>
233+
);
234+
});
235+
236+
act(() => {
237+
click(node.querySelector("a"));
238+
});
239+
240+
expect(handlerCalled).toBe(true);
241+
expect(defaultPrevented).toBe(false);
242+
});
243+
});
244+
174245
describe("when reloadDocument is specified", () => {
175246
it("does not prevent default", () => {
176247
function Home() {

packages/react-router-dom/__tests__/link-push-test.tsx

+43
Original file line numberDiff line numberDiff line change
@@ -283,4 +283,47 @@ describe("Link push and replace", () => {
283283
`);
284284
});
285285
});
286+
287+
describe("to an absolute same-origin/same-basename URL, when it is clicked", () => {
288+
it("performs a push", () => {
289+
function Home() {
290+
return (
291+
<div>
292+
<h1>Home</h1>
293+
<Link to="http://localhost/base/about">About</Link>
294+
</div>
295+
);
296+
}
297+
298+
let renderer: TestRenderer.ReactTestRenderer;
299+
TestRenderer.act(() => {
300+
renderer = TestRenderer.create(
301+
<MemoryRouter initialEntries={["/base/home"]} basename="/base">
302+
<Routes>
303+
<Route path="home" element={<Home />} />
304+
<Route path="about" element={<ShowNavigationType />} />
305+
</Routes>
306+
</MemoryRouter>
307+
);
308+
});
309+
310+
let anchor = renderer.root.findByType("a");
311+
312+
TestRenderer.act(() => {
313+
anchor.props.onClick(
314+
new MouseEvent("click", {
315+
view: window,
316+
bubbles: true,
317+
cancelable: true,
318+
})
319+
);
320+
});
321+
322+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
323+
<p>
324+
PUSH
325+
</p>
326+
`);
327+
});
328+
});
286329
});

packages/react-router-dom/index.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
createHashHistory,
4343
UNSAFE_invariant as invariant,
4444
joinPaths,
45+
stripBasename,
4546
ErrorResponse,
4647
} from "@remix-run/router";
4748

@@ -420,6 +421,8 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
420421
},
421422
ref
422423
) {
424+
let { basename } = React.useContext(NavigationContext);
425+
423426
// Rendered into <a href> for absolute URLs
424427
let absoluteHref;
425428
let isExternal = false;
@@ -434,9 +437,11 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
434437
let targetUrl = to.startsWith("//")
435438
? new URL(currentUrl.protocol + to)
436439
: new URL(to);
437-
if (targetUrl.origin === currentUrl.origin) {
438-
// Strip the protocol/origin for same-origin absolute URLs
439-
to = targetUrl.pathname + targetUrl.search + targetUrl.hash;
440+
let path = stripBasename(targetUrl.pathname, basename);
441+
442+
if (targetUrl.origin === currentUrl.origin && path != null) {
443+
// Strip the protocol/origin/basename for same-origin absolute URLs
444+
to = path + targetUrl.search + targetUrl.hash;
440445
} else {
441446
isExternal = true;
442447
}

0 commit comments

Comments
 (0)