'use client';
/**
* — framework-agnostic link component.
*
* WHY:
* We want a single Link import everywhere, regardless of whether the
* host app is Next.js, Wails, Electron, Vite, or plain CRA. When a
* framework adapter is mounted (e.g. NextLinkProvider) the native
* component handles prefetch / locale / RSC wiring. Otherwise we
* render a plain and route clicks through the active router
* adapter (which itself defaults to History API).
*
* Behavior parity with ``:
* - Cmd/Ctrl/Shift+click and middle-click open a new tab natively
* (we let the browser handle modifier clicks — DON'T preventDefault).
* - External / non-internal hrefs (http://, mailto:, tel:, target=_blank)
* skip SPA nav entirely and behave as the browser would.
*
* @example
* import { Link } from '@djangocfg/ui-core/components';
* Dashboard
* Reset
* External
*/
import { forwardRef, useCallback, type AnchorHTMLAttributes, type MouseEvent, type ReactNode } from 'react';
import { useNavigate } from '../../../hooks/router/useNavigate';
import { useLinkComponent } from './LinkContext';
type AnchorProps = Omit<
AnchorHTMLAttributes,
'href' | 'onClick'
>;
export interface LinkProps extends AnchorProps {
href: string;
/** Use replaceState instead of pushState. Default: false. */
replace?: boolean;
/** Scroll to top after SPA navigation. Default: false. */
scroll?: boolean;
/**
* Hint for the host framework's prefetcher (Next.js).
* Ignored by the agnostic fallback.
*/
prefetch?: boolean | null;
children?: ReactNode;
onClick?: (event: MouseEvent) => void;
}
/** True for protocols / schemes the browser must handle natively. */
function isExternalHref(href: string): boolean {
// Allow same-origin absolute paths (`/foo`, `./foo`, `#hash`, `?q=1`).
if (href.startsWith('/') && !href.startsWith('//')) return false;
if (href.startsWith('#') || href.startsWith('?')) return false;
// Anything with a scheme (http:, https:, mailto:, tel:, ipfs:, …).
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) return true;
// `//example.com` protocol-relative.
if (href.startsWith('//')) return true;
return false;
}
/**
* True when the click should be left to the browser:
* modifier keys, non-primary mouse button, or `target=_blank`.
*/
function shouldBypassClick(event: MouseEvent, target?: string): boolean {
if (event.defaultPrevented) return true;
if (event.button !== 0) return true;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return true;
if (target && target !== '' && target !== '_self') return true;
return false;
}
export const Link = forwardRef(function Link(
{ href, replace, scroll, prefetch, target, rel, onClick, children, ...rest },
ref
) {
const Adapter = useLinkComponent();
const { navigate } = useNavigate();
const handleClick = useCallback(
(event: MouseEvent) => {
onClick?.(event);
if (event.defaultPrevented) return;
if (isExternalHref(href)) return;
if (shouldBypassClick(event, target)) return;
event.preventDefault();
navigate(href, { replace, scroll });
},
[onClick, href, target, navigate, replace, scroll]
);
if (Adapter) {
return (
{children}
);
}
return (
{children}
);
});