diff --git a/.changeset/violet-apes-pay.md b/.changeset/violet-apes-pay.md
new file mode 100644
index 0000000000..031444d5e9
--- /dev/null
+++ b/.changeset/violet-apes-pay.md
@@ -0,0 +1,5 @@
+---
+"react-router-dom": patch
+---
+
+Fix partial object (search or hash only) pathnames losing current path value
diff --git a/packages/react-router-dom/__tests__/link-click-test.tsx b/packages/react-router-dom/__tests__/link-click-test.tsx
index b5eacee4f8..d3a8facd4c 100644
--- a/packages/react-router-dom/__tests__/link-click-test.tsx
+++ b/packages/react-router-dom/__tests__/link-click-test.tsx
@@ -61,6 +61,116 @@ describe("A click", () => {
expect(h1?.textContent).toEqual("About");
});
+ it("navigates to the new page when using an absolute URL on the same origin", () => {
+ function Home() {
+ return (
+
+
Home
+ About
+
+ );
+ }
+
+ act(() => {
+ ReactDOM.createRoot(node).render(
+
+
+ } />
+ About} />
+
+
+ );
+ });
+
+ let anchor = node.querySelector("a");
+ expect(anchor).not.toBeNull();
+
+ let event: MouseEvent;
+ act(() => {
+ event = click(anchor);
+ });
+
+ expect(event.defaultPrevented).toBe(true);
+ let h1 = node.querySelector("h1");
+ expect(h1).not.toBeNull();
+ expect(h1?.textContent).toEqual("About");
+ });
+
+ describe("when an external absolute URL is specified", () => {
+ it("does not prevent default", () => {
+ function Home() {
+ return (
+
+
Home
+ About
+
+ );
+ }
+
+ act(() => {
+ ReactDOM.createRoot(node).render(
+
+
+ } />
+ About} />
+
+
+ );
+ });
+
+ let anchor = node.querySelector("a");
+ expect(anchor).not.toBeNull();
+
+ let event: MouseEvent;
+ act(() => {
+ event = click(anchor);
+ });
+
+ expect(event.defaultPrevented).toBe(false);
+ });
+
+ it("calls provided listener", () => {
+ let handlerCalled;
+ let defaultPrevented;
+
+ function Home() {
+ return (
+
+
Home
+ {
+ handlerCalled = true;
+ defaultPrevented = e.defaultPrevented;
+ }}
+ >
+ About
+
+
+ );
+ }
+
+ act(() => {
+ ReactDOM.createRoot(node).render(
+
+
+ } />
+ About} />
+
+
+ );
+ });
+
+ act(() => {
+ click(node.querySelector("a"));
+ });
+
+ expect(handlerCalled).toBe(true);
+ expect(defaultPrevented).toBe(false);
+ });
+ });
+
describe("when reloadDocument is specified", () => {
it("does not prevent default", () => {
function Home() {
diff --git a/packages/react-router-dom/__tests__/link-href-test.tsx b/packages/react-router-dom/__tests__/link-href-test.tsx
index a37d7a7b7d..386fb7d9b2 100644
--- a/packages/react-router-dom/__tests__/link-href-test.tsx
+++ b/packages/react-router-dom/__tests__/link-href-test.tsx
@@ -172,6 +172,72 @@ describe(" href", () => {
"web+remix://somepath"
);
});
+
+ test(' is treated as an absolute link', () => {
+ let renderer: TestRenderer.ReactTestRenderer;
+ TestRenderer.act(() => {
+ renderer = TestRenderer.create(
+
+
+
+ }
+ />
+
+
+
+ );
+ });
+
+ expect(renderer.root.findByType("a").props.href).toEqual(
+ "http://localhost/inbox"
+ );
+ });
+
+ test(" is handled with the current pathname", () => {
+ let renderer: TestRenderer.ReactTestRenderer;
+ TestRenderer.act(() => {
+ renderer = TestRenderer.create(
+
+
+
+ }
+ />
+
+
+
+ );
+ });
+
+ expect(renderer.root.findByType("a").props.href).toEqual(
+ "/inbox/messages?key=value"
+ );
+ });
+
+ test(" is handled with the current pathname", () => {
+ let renderer: TestRenderer.ReactTestRenderer;
+ TestRenderer.act(() => {
+ renderer = TestRenderer.create(
+
+
+
+ }
+ />
+
+
+
+ );
+ });
+
+ expect(renderer.root.findByType("a").props.href).toEqual(
+ "/inbox/messages#hash"
+ );
+ });
});
describe("in a dynamic route", () => {
diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx
index a2adb57978..882159e6cc 100644
--- a/packages/react-router-dom/index.tsx
+++ b/packages/react-router-dom/index.tsx
@@ -418,31 +418,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][a-z0-9+.-]*:|\/\/)/i.test(location);
-
- // Location to use in the click handler
- let navigationLocation = location;
+ // Rendered into for absolute URLs
+ let absoluteHref;
let isExternal = false;
- if (isBrowser && isAbsolute) {
+
+ if (
+ isBrowser &&
+ typeof to === "string" &&
+ /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(to)
+ ) {
+ absoluteHref = to;
let currentUrl = new URL(window.location.href);
- let targetUrl = location.startsWith("//")
- ? new URL(currentUrl.protocol + location)
- : new URL(location);
+ let targetUrl = to.startsWith("//")
+ ? new URL(currentUrl.protocol + to)
+ : new URL(to);
if (targetUrl.origin === currentUrl.origin) {
// Strip the protocol/origin for same-origin absolute URLs
- navigationLocation =
- targetUrl.pathname + targetUrl.search + targetUrl.hash;
+ to = 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 });
+ // Rendered into for relative URLs
+ let href = useHref(to, { relative });
- let internalOnClick = useLinkClickHandler(navigationLocation, {
+ let internalOnClick = useLinkClickHandler(to, {
replace,
state,
target,
@@ -462,7 +463,7 @@ export const Link = React.forwardRef(
// eslint-disable-next-line jsx-a11y/anchor-has-content