Skip to content

Commit ddc425c

Browse files
authored
fix: navigate hash urls natively (#4929)
Fixes #4830 Remix incorrectly navigates hashes and clear search params. Here fixed this by using native `<a>` and tweaked the logic in builder. Test navigating hashes (when search params are present) in builder and on published site.
1 parent 11c6521 commit ddc425c

File tree

7 files changed

+95
-106
lines changed

7 files changed

+95
-106
lines changed

apps/builder/app/canvas/interceptor.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,21 @@ const switchPageAndUpdateSystem = (href: string, formData?: FormData) => {
3737
return;
3838
}
3939
// preserve pathname when not specified in href/action
40-
if (href === "" || href.startsWith("?") || href.startsWith("#")) {
40+
if (href === "" || href.startsWith("?")) {
4141
const pathname = getSelectedPagePathname();
4242
if (pathname) {
43-
href = pathname + href;
43+
href = `${pathname}${href}`;
44+
}
45+
}
46+
// preserve also search params when navigate with hash
47+
if (href.startsWith("#")) {
48+
const pathname = getSelectedPagePathname();
49+
if (pathname) {
50+
const system = $currentSystem.get();
51+
const searchParams = new URLSearchParams(
52+
system.search as Record<string, string>
53+
);
54+
href = `${pathname}?${searchParams}${href}`;
4455
}
4556
}
4657
const pageHref = new URL(href, "https://any-valid.url");
Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,42 @@
1+
import { forwardRef, type ComponentPropsWithoutRef, useContext } from "react";
2+
import { NavLink as RemixLink } from "@remix-run/react";
3+
import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime";
14
import { Link as BaseLink } from "@webstudio-is/sdk-components-react";
2-
import { wrapLinkComponent } from "./shared/remix-link";
35

4-
export const Link = wrapLinkComponent(BaseLink);
6+
type Props = Omit<ComponentPropsWithoutRef<typeof BaseLink>, "target"> & {
7+
// override (string & {}) in target to generate keywords
8+
target?: "_self" | "_blank" | "_parent" | "_top";
9+
10+
// useful remix props
11+
prefetch?: "none" | "intent" | "render" | "viewport";
12+
reloadDocument?: boolean;
13+
replace?: boolean;
14+
preventScrollReset?: boolean;
15+
};
16+
17+
export const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
18+
const { assetBaseUrl } = useContext(ReactSdkContext);
19+
// cast to string when invalid value type is provided with binding
20+
const href = String(props.href ?? "");
21+
22+
// use remix link for self reference and all relative urls
23+
// ignore asset paths which can be relative too
24+
// urls starting with # should be handled natively to not override search params
25+
if (
26+
// remix appends ?index in runtime but not in ssr
27+
href === "" ||
28+
href.startsWith("?") ||
29+
(href.startsWith("/") && href.startsWith(assetBaseUrl) === false)
30+
) {
31+
// In the future, we will switch to the :local-link pseudo-class (https://developer.mozilla.org/en-US/docs/Web/CSS/:local-link). (aria-current="page" is used now)
32+
// Therefore, we decided to use end={true} (exact route matching) for all links to facilitate easier migration.
33+
return <RemixLink {...props} to={href} ref={ref} end />;
34+
}
35+
36+
const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } =
37+
props;
38+
39+
return <BaseLink {...rest} ref={ref} />;
40+
});
41+
42+
Link.displayName = BaseLink.displayName;
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
import { RichTextLink as BaseLink } from "@webstudio-is/sdk-components-react";
2-
import { wrapLinkComponent } from "./shared/remix-link";
3-
4-
export const RichTextLink = wrapLinkComponent(BaseLink);
1+
export { Link as RichTextLink } from "./link";

packages/sdk-components-react-remix/src/shared/remix-link.tsx

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,42 @@
1+
import { type ComponentPropsWithoutRef, forwardRef, useContext } from "react";
2+
import { NavLink as RemixLink } from "react-router";
3+
import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime";
14
import { Link as BaseLink } from "@webstudio-is/sdk-components-react";
2-
import { wrapLinkComponent } from "./shared/remix-link";
35

4-
export const Link = wrapLinkComponent(BaseLink);
6+
type Props = Omit<ComponentPropsWithoutRef<typeof BaseLink>, "target"> & {
7+
// override (string & {}) in target to generate keywords
8+
target?: "_self" | "_blank" | "_parent" | "_top";
9+
10+
// useful remix props
11+
prefetch?: "none" | "intent" | "render" | "viewport";
12+
reloadDocument?: boolean;
13+
replace?: boolean;
14+
preventScrollReset?: boolean;
15+
};
16+
17+
export const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
18+
const { assetBaseUrl } = useContext(ReactSdkContext);
19+
// cast to string when invalid value type is provided with binding
20+
const href = String(props.href ?? "");
21+
22+
// use remix link for self reference and all relative urls
23+
// ignore asset paths which can be relative too
24+
// urls starting with # should be handled natively to not override search params
25+
if (
26+
// remix appends ?index in runtime but not in ssr
27+
href === "" ||
28+
href.startsWith("?") ||
29+
(href.startsWith("/") && href.startsWith(assetBaseUrl) === false)
30+
) {
31+
// In the future, we will switch to the :local-link pseudo-class (https://developer.mozilla.org/en-US/docs/Web/CSS/:local-link). (aria-current="page" is used now)
32+
// Therefore, we decided to use end={true} (exact route matching) for all links to facilitate easier migration.
33+
return <RemixLink {...props} to={href} ref={ref} end />;
34+
}
35+
36+
const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } =
37+
props;
38+
39+
return <BaseLink {...rest} ref={ref} />;
40+
});
41+
42+
Link.displayName = BaseLink.displayName;
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
import { RichTextLink as BaseLink } from "@webstudio-is/sdk-components-react";
2-
import { wrapLinkComponent } from "./shared/remix-link";
3-
4-
export const RichTextLink = wrapLinkComponent(BaseLink);
1+
export { Link as RichTextLink } from "./link";

packages/sdk-components-react-router/src/shared/remix-link.tsx

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)