2021-07-14 20:12:04 +02:00
import React from 'react'
2020-07-13 18:08:12 +02:00
import { UrlObject } from 'url'
2020-08-05 20:12:17 +02:00
import {
isLocalURL ,
2020-08-18 18:36:40 +02:00
NextRouter ,
PrefetchOptions ,
resolveHref ,
2021-06-30 11:43:31 +02:00
} from '../shared/lib/router/router'
2022-05-30 20:19:37 +02:00
import { addLocale } from './add-locale'
2022-05-29 20:53:12 +02:00
import { RouterContext } from '../shared/lib/router-context'
import { AppRouterContext } from '../shared/lib/app-router-context'
2020-11-07 00:03:15 +01:00
import { useIntersection } from './use-intersection'
2022-05-30 20:19:37 +02:00
import { getDomainLocale } from './get-domain-locale'
import { addBasePath } from './add-base-path'
2020-07-13 18:08:12 +02:00
2022-06-01 13:52:57 +02:00
// @ts-ignore useTransition exist
const hasUseTransition = typeof React . useTransition !== 'undefined'
2019-05-30 03:19:32 +02:00
type Url = string | UrlObject
2020-08-18 18:36:40 +02:00
type RequiredKeys < T > = {
[ K in keyof T ] - ? : { } extends Pick < T , K > ? never : K
} [ keyof T ]
type OptionalKeys < T > = {
[ K in keyof T ] - ? : { } extends Pick < T , K > ? K : never
} [ keyof T ]
2018-08-07 07:44:18 +02:00
2022-04-26 00:01:30 +02:00
type InternalLinkProps = {
2019-05-30 03:19:32 +02:00
href : Url
2019-07-11 20:52:21 +02:00
as ? : Url
2019-05-30 03:19:32 +02:00
replace? : boolean
scroll? : boolean
shallow? : boolean
passHref? : boolean
prefetch? : boolean
2020-10-22 17:08:01 +02:00
locale? : string | false
2022-04-26 00:01:30 +02:00
legacyBehavior? : boolean
2022-04-26 13:46:09 +02:00
// e: any because as it would otherwise overlap with existing types
2022-04-26 00:01:30 +02:00
/ * *
* requires experimental . newNextLinkBehavior
* /
2022-04-26 13:46:09 +02:00
onMouseEnter ? : ( e : any ) = > void
// e: any because as it would otherwise overlap with existing types
2022-04-26 00:01:30 +02:00
/ * *
* requires experimental . newNextLinkBehavior
* /
2022-04-26 13:46:09 +02:00
onClick ? : ( e : any ) = > void
2019-04-25 21:31:53 +02:00
}
2022-04-26 00:01:30 +02:00
2022-04-26 13:46:09 +02:00
// TODO: Include the full set of Anchor props
// adding this to the publicly exported type currently breaks existing apps
export type LinkProps = InternalLinkProps
2020-08-18 18:36:40 +02:00
type LinkPropsRequired = RequiredKeys < LinkProps >
2022-04-26 00:01:30 +02:00
type LinkPropsOptional = OptionalKeys < InternalLinkProps >
2019-04-25 21:31:53 +02:00
2020-03-01 00:06:18 +01:00
const prefetched : { [ cacheKey : string ] : boolean } = { }
2019-05-01 15:14:27 +02:00
2020-07-07 06:52:26 +02:00
function prefetch (
router : NextRouter ,
href : string ,
as : string ,
options? : PrefetchOptions
) : void {
2021-01-06 17:19:57 +01:00
if ( typeof window === 'undefined' || ! router ) return
2020-08-05 20:12:17 +02:00
if ( ! isLocalURL ( href ) ) return
2020-07-02 06:53:17 +02:00
// Prefetch the JSON page if asked (only in the client)
// We need to handle a prefetch error here since we may be
// loading with priority which can reject but we don't
// want to force navigation since this is only a prefetch
2020-07-07 06:52:26 +02:00
router . prefetch ( href , as , options ) . catch ( ( err ) = > {
2019-08-12 06:26:25 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
2020-07-02 06:53:17 +02:00
// rethrow to show invalid URL errors
throw err
2019-08-12 06:26:25 +02:00
}
2020-07-02 06:53:17 +02:00
} )
2020-11-17 19:04:07 +01:00
const curLocale =
options && typeof options . locale !== 'undefined'
? options . locale
: router && router . locale
2020-07-02 06:53:17 +02:00
// Join on an invalid URI character
2020-11-17 19:04:07 +01:00
prefetched [ href + '%' + as + ( curLocale ? '%' + curLocale : '' ) ] = true
2020-07-02 06:53:17 +02:00
}
2019-05-01 15:14:27 +02:00
2021-01-05 16:11:37 +01:00
function isModifiedEvent ( event : React.MouseEvent ) : boolean {
2020-08-05 20:12:17 +02:00
const { target } = event . currentTarget as HTMLAnchorElement
return (
( target && target !== '_self' ) ||
event . metaKey ||
event . ctrlKey ||
event . shiftKey ||
2020-08-10 22:32:47 +02:00
event . altKey || // triggers resource download
2020-08-05 20:12:17 +02:00
( event . nativeEvent && event . nativeEvent . which === 2 )
)
}
2020-07-02 06:53:17 +02:00
function linkClicked (
e : React.MouseEvent ,
2020-07-07 06:52:26 +02:00
router : NextRouter ,
2020-07-02 06:53:17 +02:00
href : string ,
2020-07-07 06:52:26 +02:00
as : string ,
2020-07-02 06:53:17 +02:00
replace? : boolean ,
shallow? : boolean ,
2020-10-15 10:58:26 +02:00
scroll? : boolean ,
2022-06-01 13:52:57 +02:00
locale? : string | false ,
startTransition ? : ( cb : any ) = > void
2020-07-02 06:53:17 +02:00
) : void {
2020-08-05 20:12:17 +02:00
const { nodeName } = e . currentTarget
2018-08-07 05:53:06 +02:00
2022-02-06 21:53:03 +01:00
// anchors inside an svg have a lowercase nodeName
const isAnchorNodeName = nodeName . toUpperCase ( ) === 'A'
if ( isAnchorNodeName && ( isModifiedEvent ( e ) || ! isLocalURL ( href ) ) ) {
2020-08-10 22:32:47 +02:00
// ignore click for browser’ s default behavior
2020-07-02 06:53:17 +02:00
return
2019-12-20 22:30:58 +01:00
}
2020-07-02 06:53:17 +02:00
e . preventDefault ( )
2018-08-07 05:53:06 +02:00
2022-06-01 13:52:57 +02:00
const navigate = ( ) = > {
// replace state instead of push if prop is present
router [ replace ? 'replace' : 'push' ] ( href , as , {
shallow ,
locale ,
scroll ,
} )
}
if ( startTransition ) {
startTransition ( navigate )
} else {
navigate ( )
}
2020-07-02 06:53:17 +02:00
}
2016-10-06 09:07:41 +02:00
2022-04-28 11:32:32 +02:00
type LinkPropsReal = React . PropsWithChildren <
Omit < React.AnchorHTMLAttributes < HTMLAnchorElement > , keyof LinkProps > &
LinkProps
>
const Link = React . forwardRef < HTMLAnchorElement , LinkPropsReal > (
2022-05-24 18:00:22 +02:00
function LinkComponent ( props , forwardedRef ) {
2022-04-28 11:32:32 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
function createPropError ( args : {
key : string
expected : string
actual : string
} ) {
return new Error (
` Failed prop type: The prop \` ${ args . key } \` expects a ${ args . expected } in \` <Link> \` , but got \` ${ args . actual } \` instead. ` +
( typeof window !== 'undefined'
? "\nOpen your browser's console to view the Component stack trace."
: '' )
)
2020-08-18 18:36:40 +02:00
}
2022-04-28 11:32:32 +02:00
// TypeScript trick for type-guarding:
const requiredPropsGuard : Record < LinkPropsRequired , true > = {
href : true ,
} as const
const requiredProps : LinkPropsRequired [ ] = Object . keys (
requiredPropsGuard
) as LinkPropsRequired [ ]
requiredProps . forEach ( ( key : LinkPropsRequired ) = > {
if ( key === 'href' ) {
if (
props [ key ] == null ||
( typeof props [ key ] !== 'string' && typeof props [ key ] !== 'object' )
) {
throw createPropError ( {
key ,
expected : '`string` or `object`' ,
actual : props [ key ] === null ? 'null' : typeof props [ key ] ,
} )
}
} else {
// TypeScript trick for type-guarding:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ : never = key
2020-10-15 10:58:26 +02:00
}
2022-04-28 11:32:32 +02:00
} )
// TypeScript trick for type-guarding:
const optionalPropsGuard : Record < LinkPropsOptional , true > = {
as : true ,
replace : true ,
scroll : true ,
shallow : true ,
passHref : true ,
prefetch : true ,
locale : true ,
onClick : true ,
onMouseEnter : true ,
legacyBehavior : true ,
} as const
const optionalProps : LinkPropsOptional [ ] = Object . keys (
optionalPropsGuard
) as LinkPropsOptional [ ]
optionalProps . forEach ( ( key : LinkPropsOptional ) = > {
const valType = typeof props [ key ]
if ( key === 'as' ) {
if ( props [ key ] && valType !== 'string' && valType !== 'object' ) {
throw createPropError ( {
key ,
expected : '`string` or `object`' ,
actual : valType ,
} )
}
} else if ( key === 'locale' ) {
if ( props [ key ] && valType !== 'string' ) {
throw createPropError ( {
key ,
expected : '`string`' ,
actual : valType ,
} )
}
} else if ( key === 'onClick' || key === 'onMouseEnter' ) {
if ( props [ key ] && valType !== 'function' ) {
throw createPropError ( {
key ,
expected : '`function`' ,
actual : valType ,
} )
}
} else if (
key === 'replace' ||
key === 'scroll' ||
key === 'shallow' ||
key === 'passHref' ||
key === 'prefetch' ||
key === 'legacyBehavior'
) {
if ( props [ key ] != null && valType !== 'boolean' ) {
throw createPropError ( {
key ,
expected : '`boolean`' ,
actual : valType ,
} )
}
} else {
// TypeScript trick for type-guarding:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ : never = key
2020-08-18 18:36:40 +02:00
}
2022-04-28 11:32:32 +02:00
} )
2020-08-18 18:36:40 +02:00
2022-04-28 11:32:32 +02:00
// This hook is in a conditional but that is ok because `process.env.NODE_ENV` never changes
// eslint-disable-next-line react-hooks/rules-of-hooks
const hasWarned = React . useRef ( false )
if ( props . prefetch && ! hasWarned . current ) {
hasWarned . current = true
console . warn (
'Next.js auto-prefetches automatically based on viewport. The prefetch attribute is no longer needed. More: https://nextjs.org/docs/messages/prefetch-true-deprecated'
)
}
2016-10-06 09:07:41 +02:00
}
2022-04-26 00:01:30 +02:00
2022-04-28 11:32:32 +02:00
let children : React.ReactNode
const {
href : hrefProp ,
as : asProp ,
children : childrenProp ,
prefetch : prefetchProp ,
passHref ,
replace ,
shallow ,
scroll ,
locale ,
onClick ,
onMouseEnter ,
2022-05-30 16:09:14 +02:00
legacyBehavior = Boolean ( process . env . __NEXT_NEW_LINK_BEHAVIOR ) !== true ,
2022-04-28 11:32:32 +02:00
. . . restProps
} = props
children = childrenProp
2022-06-25 22:07:40 +02:00
if (
legacyBehavior &&
( typeof children === 'string' || typeof children === 'number' )
) {
2022-04-28 11:32:32 +02:00
children = < a > { children } < / a >
2020-07-07 06:52:26 +02:00
}
2016-10-06 09:07:41 +02:00
2022-04-28 11:32:32 +02:00
const p = prefetchProp !== false
2022-06-01 13:52:57 +02:00
const [ , /* isPending */ startTransition ] = hasUseTransition
? // Rules of hooks is disabled here because the useTransition will always exist with React 18.
// There is no difference between renders in this case, only between using React 18 vs 17.
// @ts-ignore useTransition exists
// eslint-disable-next-line react-hooks/rules-of-hooks
React . useTransition ( )
: [ ]
2022-05-29 20:53:12 +02:00
let router = React . useContext ( RouterContext )
const appRouter = React . useContext ( AppRouterContext )
if ( appRouter ) {
router = appRouter
}
2022-04-04 20:18:49 +02:00
2022-04-28 11:32:32 +02:00
const { href , as } = React . useMemo ( ( ) = > {
const [ resolvedHref , resolvedAs ] = resolveHref ( router , hrefProp , true )
return {
href : resolvedHref ,
as : asProp ? resolveHref ( router , asProp ) : resolvedAs || resolvedHref ,
2022-04-26 00:01:30 +02:00
}
2022-04-28 11:32:32 +02:00
} , [ router , hrefProp , asProp ] )
const previousHref = React . useRef < string > ( href )
const previousAs = React . useRef < string > ( as )
// 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 <Link> with \` href \` of \` ${ hrefProp } \` but "legacyBehavior" was set. The legacy behavior requires onClick be set on the child of next/link `
)
}
if ( onMouseEnter ) {
console . warn (
` "onMouseEnter" was passed to <Link> with \` href \` of \` ${ hrefProp } \` but "legacyBehavior" was set. The legacy behavior requires onMouseEnter 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 <Link> with \` href \` of \` ${ hrefProp } \` but one child is required https://nextjs.org/docs/messages/link-no-children `
)
}
2022-04-26 00:01:30 +02:00
throw new Error (
2022-04-28 11:32:32 +02:00
` Multiple children were passed to <Link> 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."
: '' )
2022-04-26 00:01:30 +02:00
)
}
2022-04-28 11:32:32 +02:00
} else {
child = React . Children . only ( children )
2022-03-22 19:58:55 +01:00
}
2021-05-31 21:41:57 +02:00
}
2022-04-26 00:01:30 +02:00
2022-04-28 11:32:32 +02:00
const childRef : any = legacyBehavior
? child && typeof child === 'object' && child . ref
: forwardedRef
2020-11-01 04:37:28 +01:00
2022-04-28 11:32:32 +02:00
const [ setIntersectionRef , isVisible , resetVisible ] = useIntersection ( {
rootMargin : '200px' ,
} )
2022-04-04 20:18:49 +02:00
2022-04-28 11:32:32 +02:00
const setRef = React . useCallback (
( el : Element ) = > {
// Before the link getting observed, check if visible state need to be reset
if ( previousAs . current !== as || previousHref . current !== href ) {
resetVisible ( )
previousAs . current = as
previousHref . current = href
}
2022-04-04 20:18:49 +02:00
2022-04-28 11:32:32 +02:00
setIntersectionRef ( el )
if ( childRef ) {
if ( typeof childRef === 'function' ) childRef ( el )
else if ( typeof childRef === 'object' ) {
childRef . current = el
}
2020-11-01 04:37:28 +01:00
}
2022-04-28 11:32:32 +02:00
} ,
[ as , childRef , href , resetVisible , setIntersectionRef ]
)
React . useEffect ( ( ) = > {
const shouldPrefetch = isVisible && p && isLocalURL ( href )
const curLocale =
typeof locale !== 'undefined' ? locale : router && router . locale
const isPrefetched =
prefetched [ href + '%' + as + ( curLocale ? '%' + curLocale : '' ) ]
if ( shouldPrefetch && ! isPrefetched ) {
prefetch ( router , href , as , {
locale : curLocale ,
} )
2020-11-01 04:37:28 +01:00
}
2022-04-28 11:32:32 +02:00
} , [ as , href , isVisible , locale , p , router ] )
const childProps : {
onMouseEnter : React.MouseEventHandler
onClick : React.MouseEventHandler
href? : string
ref? : any
} = {
ref : setRef ,
onClick : ( e : React.MouseEvent ) = > {
if ( process . env . NODE_ENV !== 'production' ) {
if ( ! e ) {
throw new Error (
` Component rendered inside next/link has to pass click event to "onClick" prop. `
)
}
2022-02-07 01:04:37 +01:00
}
2022-04-26 00:01:30 +02:00
2022-04-28 11:32:32 +02:00
if ( ! legacyBehavior && typeof onClick === 'function' ) {
onClick ( e )
}
if (
legacyBehavior &&
child . props &&
typeof child . props . onClick === 'function'
) {
child . props . onClick ( e )
}
if ( ! e . defaultPrevented ) {
2022-06-01 13:52:57 +02:00
linkClicked (
e ,
router ,
href ,
as ,
replace ,
shallow ,
scroll ,
locale ,
appRouter ? startTransition : undefined
)
2022-04-28 11:32:32 +02:00
}
} ,
onMouseEnter : ( e : React.MouseEvent ) = > {
if ( ! legacyBehavior && typeof onMouseEnter === 'function' ) {
onMouseEnter ( e )
}
if (
legacyBehavior &&
child . props &&
typeof child . props . onMouseEnter === 'function'
) {
child . props . onMouseEnter ( e )
}
if ( isLocalURL ( href ) ) {
prefetch ( router , href , as , { priority : true } )
}
} ,
}
2016-10-06 09:07:41 +02:00
2022-04-28 11:32:32 +02:00
// If child is an <a> 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 (
! legacyBehavior ||
passHref ||
( child . type === 'a' && ! ( 'href' in child . props ) )
) {
const curLocale =
typeof locale !== 'undefined' ? locale : router && router . locale
// we only render domain locales if we are currently on a domain locale
// so that locale links are still visitable in development/preview envs
const localeDomain =
router &&
router . isLocaleDomain &&
2022-05-30 20:19:37 +02:00
getDomainLocale ( as , curLocale , router . locales , router . domainLocales )
2016-10-06 09:07:41 +02:00
2022-04-28 11:32:32 +02:00
childProps . href =
localeDomain ||
addBasePath ( addLocale ( as , curLocale , router && router . defaultLocale ) )
}
return legacyBehavior ? (
React . cloneElement ( child , childProps )
) : (
< a { ...restProps } { ...childProps } >
{ children }
< / a >
)
}
)
2016-10-06 09:07:41 +02:00
2018-08-07 05:23:28 +02:00
export default Link