import type { RouteLayerOptions, RouteLocationInput, RouteMatchType, RouterLinkResolved, RouterLinkType } from '@esmx/router'; import { type CSSProperties, createElement, type ElementType, forwardRef, type MouseEvent, type ReactElement, type ReactNode, type Ref, useCallback, useMemo } from 'react'; import { useRouter } from './context'; /** * Props for RouterLink component. * Similar to RouterLinkProps from @esmx/router with React-specific additions. */ export interface RouterLinkComponentProps { /** Target route location (required) */ to: RouteLocationInput; /** Navigation type */ type?: RouterLinkType; /** @deprecated Use type='replace' instead */ replace?: boolean; /** Active matching mode */ exact?: RouteMatchType; /** CSS class for active state */ activeClass?: string; /** Event(s) that trigger navigation */ event?: string | string[]; /** HTML tag to render */ tag?: string; /** Layer navigation options */ layerOptions?: RouteLayerOptions; /** Hook function called before navigation */ beforeNavigate?: (event: Event, eventName: string) => void; /** Link content */ children?: ReactNode; /** Additional CSS class name */ className?: string; /** Inline styles */ style?: CSSProperties; } /** * RouterLink component for declarative navigation. * Renders an anchor tag with proper navigation behavior and active state management. * Supports all navigation types: push, replace, pushWindow, replaceWindow, pushLayer. * * @param props - Component props * @param props.to - Target route location * @param props.type - Navigation type ('push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer') * @param props.exact - Active matching mode ('include' | 'exact' | 'route') * @param props.activeClass - CSS class for active state * @param props.event - Event(s) that trigger navigation * @param props.tag - HTML tag to render (default: 'a') * @param props.layerOptions - Options for layer navigation * @param props.beforeNavigate - Callback before navigation * @param props.children - Link content * * @example * ```tsx * // Basic navigation * Home * About * * // With object location * * Users * * * // Replace navigation (no history entry) * Login * * // Open in new window * External * * // Custom active class * * Dashboard * * * // Custom tag (button) * * Submit * * * // With beforeNavigate callback * { * if (!isAuthenticated) { * e.preventDefault(); * showLoginModal(); * } * }} * > * Protected Page * * ``` */ function RouterLinkInner( props: RouterLinkComponentProps & { [key: string]: any }, ref: Ref ): ReactElement { const { to, type = 'push', replace, exact = 'include', activeClass, event = 'click', tag = 'a', layerOptions, beforeNavigate, children, className, style, ...rest } = props; const router = useRouter(); // Resolve the link using router's built-in resolver const linkResolved: RouterLinkResolved = useMemo(() => { return router.resolveLink({ to, type, replace, exact, activeClass, event, tag, layerOptions, beforeNavigate }); }, [ router, to, type, replace, exact, activeClass, event, tag, layerOptions, beforeNavigate ]); // Handle click event const handleClick = useCallback( async (e: MouseEvent) => { // Call beforeNavigate callback if provided beforeNavigate?.(e as unknown as Event, 'click'); // If default was prevented by beforeNavigate or by modifier keys, skip navigation if (e.defaultPrevented) return; // Check for modifier keys - let browser handle these if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return; if (e.button !== 0) return; // Prevent default browser navigation e.preventDefault(); // Navigate using the resolved link's navigate function await linkResolved.navigate(e as unknown as Event); }, [linkResolved, beforeNavigate] ); // Build class names const computedClassName = useMemo(() => { const classes: string[] = []; if (linkResolved.attributes.class) { classes.push(linkResolved.attributes.class); } if (className) { classes.push(className); } return classes.join(' ') || undefined; }, [linkResolved.attributes.class, className]); // Build props for the element const elementProps = { ref, href: linkResolved.attributes.href, target: linkResolved.attributes.target, rel: linkResolved.attributes.rel, className: computedClassName, style, onClick: handleClick, ...rest }; // Render the element with the appropriate tag using createElement return createElement(tag as ElementType, elementProps, children); } // Cast needed to fix TypeScript forwardRef type inference issue // This is a common pattern when index signatures conflict with forwardRef's prop handling export const RouterLink = forwardRef( RouterLinkInner as any ) as React.ForwardRefExoticComponent< RouterLinkComponentProps & { [key: string]: any; } & React.RefAttributes >; RouterLink.displayName = 'RouterLink';