Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions packages/sdk-components-react-router/src/link.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type ComponentPropsWithoutRef, forwardRef, useContext } from "react";
import { NavLink as RemixLink } from "react-router";
import { NavLink as RemixLink, useLocation } from "react-router";
import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime";
import { Link as BaseLink } from "@webstudio-is/sdk-components-react";

Expand All @@ -18,6 +18,44 @@ export const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
const { assetBaseUrl } = useContext(ReactSdkContext);
// cast to string when invalid value type is provided with binding
const href = String(props.href ?? "");
const location = useLocation();

// computeIsCurrent: inline helper (uses current `location`) to determine
// whether `linkHref` matches the current full URL (pathname + search + hash).
const computeIsCurrent = (linkHref: string): boolean => {
try {
const base =
typeof window !== "undefined" && window.location?.origin
? window.location.origin
: "http://localhost";

const currentFull = `${location.pathname}${location.search}${location.hash}`;

// Treat empty href as a reference to the current location
let normalizedLink = linkHref === "" ? currentFull : linkHref;

// Normalize relative ?search and #hash links
if (normalizedLink.startsWith("?")) {
normalizedLink = `${location.pathname}${normalizedLink}`;
} else if (normalizedLink.startsWith("#")) {
normalizedLink = `${location.pathname}${location.search}${normalizedLink}`;
}

const target = new URL(normalizedLink, base);
const current = new URL(currentFull, base);

const strip = (p: string) =>
p.endsWith("/") && p !== "/" ? p.slice(0, -1) : p;

return (
strip(target.pathname) === strip(current.pathname) &&
target.search === current.search &&
target.hash === current.hash
);
} catch {
return false;
}
};

// use remix link for self reference and all relative urls
// ignore asset paths which can be relative too
Expand All @@ -30,7 +68,18 @@ export const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
) {
// 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 <RemixLink {...props} to={href} ref={ref} end />;
// Compute aria-current based on full URL (pathname + search + hash)
const ariaCurrent = computeIsCurrent(href) ? "page" : undefined;

return (
<RemixLink
{...props}
to={href}
ref={ref}
end
aria-current={ariaCurrent}
/>
);
}

const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } =
Expand Down