diff --git a/apps/builder/app/canvas/interceptor.ts b/apps/builder/app/canvas/interceptor.ts index 755258076ea6..c7d42c4f7c4c 100644 --- a/apps/builder/app/canvas/interceptor.ts +++ b/apps/builder/app/canvas/interceptor.ts @@ -37,10 +37,21 @@ const switchPageAndUpdateSystem = (href: string, formData?: FormData) => { return; } // preserve pathname when not specified in href/action - if (href === "" || href.startsWith("?") || href.startsWith("#")) { + if (href === "" || href.startsWith("?")) { const pathname = getSelectedPagePathname(); if (pathname) { - href = pathname + href; + href = `${pathname}${href}`; + } + } + // preserve also search params when navigate with hash + if (href.startsWith("#")) { + const pathname = getSelectedPagePathname(); + if (pathname) { + const system = $currentSystem.get(); + const searchParams = new URLSearchParams( + system.search as Record + ); + href = `${pathname}?${searchParams}${href}`; } } const pageHref = new URL(href, "https://any-valid.url"); diff --git a/packages/sdk-components-react-remix/src/link.tsx b/packages/sdk-components-react-remix/src/link.tsx index 166d42c171e6..aa5f39edc426 100644 --- a/packages/sdk-components-react-remix/src/link.tsx +++ b/packages/sdk-components-react-remix/src/link.tsx @@ -1,4 +1,42 @@ +import { forwardRef, type ComponentPropsWithoutRef, useContext } from "react"; +import { NavLink as RemixLink } from "@remix-run/react"; +import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime"; import { Link as BaseLink } from "@webstudio-is/sdk-components-react"; -import { wrapLinkComponent } from "./shared/remix-link"; -export const Link = wrapLinkComponent(BaseLink); +type Props = Omit, "target"> & { + // override (string & {}) in target to generate keywords + target?: "_self" | "_blank" | "_parent" | "_top"; + + // useful remix props + prefetch?: "none" | "intent" | "render" | "viewport"; + reloadDocument?: boolean; + replace?: boolean; + preventScrollReset?: boolean; +}; + +export const Link = forwardRef((props, ref) => { + const { assetBaseUrl } = useContext(ReactSdkContext); + // cast to string when invalid value type is provided with binding + const href = String(props.href ?? ""); + + // use remix link for self reference and all relative urls + // ignore asset paths which can be relative too + // urls starting with # should be handled natively to not override search params + if ( + // remix appends ?index in runtime but not in ssr + href === "" || + href.startsWith("?") || + (href.startsWith("/") && href.startsWith(assetBaseUrl) === false) + ) { + // 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) + // Therefore, we decided to use end={true} (exact route matching) for all links to facilitate easier migration. + return ; + } + + const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } = + props; + + return ; +}); + +Link.displayName = BaseLink.displayName; diff --git a/packages/sdk-components-react-remix/src/rich-text-link.tsx b/packages/sdk-components-react-remix/src/rich-text-link.tsx index b66fc095654e..dd18b64a91ba 100644 --- a/packages/sdk-components-react-remix/src/rich-text-link.tsx +++ b/packages/sdk-components-react-remix/src/rich-text-link.tsx @@ -1,4 +1 @@ -import { RichTextLink as BaseLink } from "@webstudio-is/sdk-components-react"; -import { wrapLinkComponent } from "./shared/remix-link"; - -export const RichTextLink = wrapLinkComponent(BaseLink); +export { Link as RichTextLink } from "./link"; diff --git a/packages/sdk-components-react-remix/src/shared/remix-link.tsx b/packages/sdk-components-react-remix/src/shared/remix-link.tsx deleted file mode 100644 index 962f4825f74f..000000000000 --- a/packages/sdk-components-react-remix/src/shared/remix-link.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { forwardRef, type ComponentPropsWithoutRef, useContext } from "react"; -import { NavLink as RemixLink } from "@remix-run/react"; -import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime"; -import type { Link } from "@webstudio-is/sdk-components-react"; - -type Props = Omit, "target"> & { - // override (string & {}) in target to generate keywords - target?: "_self" | "_blank" | "_parent" | "_top"; - - // useful remix props - prefetch?: "none" | "intent" | "render" | "viewport"; - reloadDocument?: boolean; - replace?: boolean; - preventScrollReset?: boolean; -}; - -export const wrapLinkComponent = (BaseLink: typeof Link) => { - const Component = forwardRef((props, ref) => { - const { assetBaseUrl } = useContext(ReactSdkContext); - // cast to string when invalid value type is provided with binding - const href = String(props.href ?? ""); - - // use remix link for self reference and all relative urls - // ignore asset paths which can be relative too - if ( - // remix appends ?index in runtime but not in ssr - href === "" || - href.startsWith("?") || - href.startsWith("#") || - (href.startsWith("/") && href.startsWith(assetBaseUrl) === false) - ) { - // 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) - // Therefore, we decided to use end={true} (exact route matching) for all links to facilitate easier migration. - return ; - } - - const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } = - props; - - return ; - }); - - Component.displayName = BaseLink.displayName; - - return Component; -}; diff --git a/packages/sdk-components-react-router/src/link.tsx b/packages/sdk-components-react-router/src/link.tsx index 166d42c171e6..dd5bc824fdbc 100644 --- a/packages/sdk-components-react-router/src/link.tsx +++ b/packages/sdk-components-react-router/src/link.tsx @@ -1,4 +1,42 @@ +import { type ComponentPropsWithoutRef, forwardRef, useContext } from "react"; +import { NavLink as RemixLink } from "react-router"; +import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime"; import { Link as BaseLink } from "@webstudio-is/sdk-components-react"; -import { wrapLinkComponent } from "./shared/remix-link"; -export const Link = wrapLinkComponent(BaseLink); +type Props = Omit, "target"> & { + // override (string & {}) in target to generate keywords + target?: "_self" | "_blank" | "_parent" | "_top"; + + // useful remix props + prefetch?: "none" | "intent" | "render" | "viewport"; + reloadDocument?: boolean; + replace?: boolean; + preventScrollReset?: boolean; +}; + +export const Link = forwardRef((props, ref) => { + const { assetBaseUrl } = useContext(ReactSdkContext); + // cast to string when invalid value type is provided with binding + const href = String(props.href ?? ""); + + // use remix link for self reference and all relative urls + // ignore asset paths which can be relative too + // urls starting with # should be handled natively to not override search params + if ( + // remix appends ?index in runtime but not in ssr + href === "" || + href.startsWith("?") || + (href.startsWith("/") && href.startsWith(assetBaseUrl) === false) + ) { + // 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) + // Therefore, we decided to use end={true} (exact route matching) for all links to facilitate easier migration. + return ; + } + + const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } = + props; + + return ; +}); + +Link.displayName = BaseLink.displayName; diff --git a/packages/sdk-components-react-router/src/rich-text-link.tsx b/packages/sdk-components-react-router/src/rich-text-link.tsx index b66fc095654e..dd18b64a91ba 100644 --- a/packages/sdk-components-react-router/src/rich-text-link.tsx +++ b/packages/sdk-components-react-router/src/rich-text-link.tsx @@ -1,4 +1 @@ -import { RichTextLink as BaseLink } from "@webstudio-is/sdk-components-react"; -import { wrapLinkComponent } from "./shared/remix-link"; - -export const RichTextLink = wrapLinkComponent(BaseLink); +export { Link as RichTextLink } from "./link"; diff --git a/packages/sdk-components-react-router/src/shared/remix-link.tsx b/packages/sdk-components-react-router/src/shared/remix-link.tsx deleted file mode 100644 index 77f2f0d84430..000000000000 --- a/packages/sdk-components-react-router/src/shared/remix-link.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { forwardRef, type ComponentPropsWithoutRef, useContext } from "react"; -import { NavLink as RemixLink } from "react-router"; -import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime"; -import type { Link } from "@webstudio-is/sdk-components-react"; - -type Props = Omit, "target"> & { - // override (string & {}) in target to generate keywords - target?: "_self" | "_blank" | "_parent" | "_top"; - - // useful remix props - prefetch?: "none" | "intent" | "render" | "viewport"; - reloadDocument?: boolean; - replace?: boolean; - preventScrollReset?: boolean; -}; - -export const wrapLinkComponent = (BaseLink: typeof Link) => { - const Component = forwardRef((props, ref) => { - const { assetBaseUrl } = useContext(ReactSdkContext); - // cast to string when invalid value type is provided with binding - const href = String(props.href ?? ""); - - // use remix link for self reference and all relative urls - // ignore asset paths which can be relative too - if ( - // remix appends ?index in runtime but not in ssr - href === "" || - href.startsWith("?") || - href.startsWith("#") || - (href.startsWith("/") && href.startsWith(assetBaseUrl) === false) - ) { - // 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) - // Therefore, we decided to use end={true} (exact route matching) for all links to facilitate easier migration. - return ; - } - - const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } = - props; - - return ; - }); - - Component.displayName = BaseLink.displayName; - - return Component; -};