import NextLink, { LinkProps as NextLinkProps } from 'next/link'; import React from 'react'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { useRouter } from 'next/router'; import { UrlObject } from 'url'; /** * Represents the route strategy options. */ export enum LinkStrategy { Default = 'default', Prefetch = 'prefetch', None = 'none', } function getLinkStrategy(): LinkStrategy { let linkStrategy = LinkStrategy.Default; if (typeof process !== 'undefined' && process.env.LINK_STRATEGY) { linkStrategy = process.env.LINK_STRATEGY as LinkStrategy; } return linkStrategy; } export const linkStrategy: LinkStrategy = getLinkStrategy(); export type LinkProps = Omit & { href?: string | UrlObject; target?: string; children: React.ReactNode; }; export type LinkComponent = React.ForwardRefExoticComponent, keyof LinkProps> & LinkProps & React.RefAttributes>; const Link_: LinkComponent = React.forwardRef( function Link_({ children, href, passHref, legacyBehavior, prefetch, ...props }: LinkProps, forwardedRef) { passHref = passHref === false ? false : true; legacyBehavior = legacyBehavior === false ? false : true; /** * original prefetch is setted by default to true and activates with viewport visibility * we are setting it by default to false, however prefetching will still occur on hover */ prefetch = linkStrategy === LinkStrategy.Default ? (prefetch === true ? true : false) : (prefetch === false ? false : true); if (!href) { console.warn('LinkComponent missing href'); } return href ? ( {children} ) : ( <>{children} ); } ); const RouterLink_: LinkComponent = React.forwardRef( function RouterLink_({ children: childrenProp, href: hrefProp, target, as:asProp, passHref, legacyBehavior, prefetch, locale, replace, shallow, scroll, onClick, ...props }: LinkProps, forwardedRef) { const router = useRouter(); legacyBehavior = legacyBehavior === false ? false : true; passHref = passHref === false ? false : true; let children: React.ReactNode = childrenProp; if (legacyBehavior && (typeof children === 'string' || typeof children === 'number') ) { children = {children}; } // This will return the first child, if multiple are provided it will throw an error let child: any; if (legacyBehavior) { if (process.env.NODE_ENV === 'development') { if (onClick) { console.warn( `"onClick" was passed to with \`href\` of \`${hrefProp}\` but "legacyBehavior" was set. The legacy behavior requires onClick be set on the child of next/link` ); } try { child = React.Children.only(children); } catch (err) { if (!children) { throw new Error( `No children were passed to with \`href\` of \`${hrefProp}\` but one child is required https://nextjs.org/docs/messages/link-no-children` ); } throw new Error( `Multiple children were passed to with \`href\` of \`${hrefProp}\` but only one child is supported https://nextjs.org/docs/messages/link-multiple-children` + (typeof window !== 'undefined' ? ' \nOpen your browser\'s console to view the Component stack trace.' : '') ); } } else { child = React.Children.only(children); } } else { if (process.env.NODE_ENV === 'development') { if ((children as any)?.type === 'a') { throw new Error( 'Invalid with child. Please remove or use .\nLearn more: https://nextjs.org/docs/messages/invalid-new-link-with-extra-anchor' ); } } } const childRef: any = legacyBehavior ? child && typeof child === 'object' && child.ref : forwardedRef; const setRef = React.useCallback((el: Element) => { if (childRef) { if (typeof childRef === 'function') { childRef(el); } else if (typeof childRef === 'object') { childRef.current = el; } } }, [childRef]); const href = hrefProp?.toString(); if (!href) { console.warn('LinkComponent missing href'); return <>{childrenProp}; } // console.log('RouterLink_', href, passHref); const onNavToUrl = (event: React.MouseEvent) => { // const { nodeName } = event.currentTarget; // anchors inside an svg have a lowercase nodeName // const isAnchorNodeName = nodeName.toUpperCase() === 'A'; if (href && (href.indexOf('http') !== -1 || target === '_blank')) { // ignore click for browser’s default behavior return; } event.preventDefault(); const routerScroll = scroll ?? true; if (href) { if (replace) { router.replace(href, asProp, { shallow, locale, scroll:routerScroll }); } else { router.push(href, asProp, { shallow, locale, scroll:routerScroll }); } } }; const childProps: { href?: string; target?: string; ref?: any; onClick: React.MouseEventHandler; } = { ref: setRef, onClick(event) { if (process.env.NODE_ENV !== 'production') { if (!event) { throw new Error( 'Component rendered inside next/link has to pass click event to "onClick" prop.' ); } } if (!legacyBehavior && typeof onClick === 'function') { onClick(event); } if (child.props && typeof child.props.onClick === 'function') { child.props.onClick(event); } if (!router) { return; } if (event.defaultPrevented) { return; } onNavToUrl(event); }, }; // If child is an tag and doesn't have a href attribute, or if the 'passHref' property is // defined, we specify the current 'href', so that repetition is not needed by the user. // If the url is absolute, we can bypass the logic to prepend the domain and locale. if (href && (href.indexOf('http') !== -1 || target === '_blank')) { childProps.href = href; childProps.target = target; } else if ( passHref || (child.type === 'a' && !('href' in child.props)) ) { childProps.href = href; childProps.target = target; } return legacyBehavior ? ( React.cloneElement(child, childProps) ) : ( {children} ); /* const childrenWithProps = React.Children.map(children, (child, index) => { if (React.isValidElement(child)) { return React.cloneElement(child, { ref: forwardedRef, href, onClick: onNavToUrl, ...props, } as any); } else { return child; } }); if (passHref && href) { return <>{childrenWithProps}; } if (!href) { console.warn('LinkComponent missing href'); } return href ? ( {children} ) : ( <>{children} ); */ } ); export const Link = linkStrategy === LinkStrategy.None ? RouterLink_ : Link_; Link.displayName = 'Link';