2020-11-07 00:03:15 +01:00
import React , { Children , useEffect } from 'react'
2020-07-13 18:08:12 +02:00
import { UrlObject } from 'url'
2020-08-05 20:12:17 +02:00
import {
2020-08-18 18:36:40 +02:00
addBasePath ,
2020-10-07 23:11:01 +02:00
addLocale ,
2020-12-31 09:07:51 +01:00
getDomainLocale ,
2020-08-05 20:12:17 +02:00
isLocalURL ,
2020-08-18 18:36:40 +02:00
NextRouter ,
PrefetchOptions ,
resolveHref ,
2020-08-05 20:12:17 +02:00
} from '../next-server/lib/router/router'
2020-07-07 06:52:26 +02:00
import { useRouter } from './router'
2020-11-07 00:03:15 +01:00
import { useIntersection } from './use-intersection'
2020-07-13 18:08:12 +02:00
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
2019-07-11 20:52:21 +02:00
export type LinkProps = {
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
2019-04-25 21:31:53 +02:00
}
2020-08-18 18:36:40 +02:00
type LinkPropsRequired = RequiredKeys < LinkProps >
type LinkPropsOptional = OptionalKeys < LinkProps >
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 ,
2020-10-22 17:08:01 +02:00
locale? : string | false
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
2020-08-10 22:32:47 +02:00
if ( nodeName === 'A' && ( isModifiedEvent ( e ) || ! isLocalURL ( href ) ) ) {
// 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
2020-07-02 06:53:17 +02:00
// avoid scroll for urls with anchor refs
if ( scroll == null ) {
scroll = as . indexOf ( '#' ) < 0
}
2018-08-07 05:53:06 +02:00
2020-07-02 06:53:17 +02:00
// replace state instead of push if prop is present
2020-12-30 05:33:08 +01:00
router [ replace ? 'replace' : 'push' ] ( href , as , {
shallow ,
locale ,
scroll ,
} ) . then ( ( success : boolean ) = > {
if ( ! success ) return
if ( scroll ) {
// FIXME: proper route announcing at Router level, not Link:
document . body . focus ( )
2016-10-06 09:07:41 +02:00
}
2020-12-30 05:33:08 +01:00
} )
2020-07-02 06:53:17 +02:00
}
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
function Link ( props : React.PropsWithChildren < LinkProps > ) {
if ( process . env . NODE_ENV !== 'production' ) {
2020-08-18 18:36:40 +02:00
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."
: '' )
)
}
// 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
}
} )
// TypeScript trick for type-guarding:
const optionalPropsGuard : Record < LinkPropsOptional , true > = {
as : true ,
replace : true ,
scroll : true ,
shallow : true ,
passHref : true ,
prefetch : true ,
2020-10-15 10:58:26 +02:00
locale : true ,
2020-08-18 18:36:40 +02:00
} as const
const optionalProps : LinkPropsOptional [ ] = Object . keys (
optionalPropsGuard
) as LinkPropsOptional [ ]
optionalProps . forEach ( ( key : LinkPropsOptional ) = > {
2020-10-15 10:58:26 +02:00
const valType = typeof props [ key ]
2020-08-18 18:36:40 +02:00
if ( key === 'as' ) {
2020-10-15 10:58:26 +02:00
if ( props [ key ] && valType !== 'string' && valType !== 'object' ) {
2020-08-18 18:36:40 +02:00
throw createPropError ( {
key ,
expected : '`string` or `object`' ,
2020-10-15 10:58:26 +02:00
actual : valType ,
} )
}
} else if ( key === 'locale' ) {
if ( props [ key ] && valType !== 'string' ) {
throw createPropError ( {
key ,
expected : '`string`' ,
actual : valType ,
2020-08-18 18:36:40 +02:00
} )
}
} else if (
key === 'replace' ||
key === 'scroll' ||
key === 'shallow' ||
key === 'passHref' ||
key === 'prefetch'
) {
2020-10-15 10:58:26 +02:00
if ( props [ key ] != null && valType !== 'boolean' ) {
2020-08-18 18:36:40 +02:00
throw createPropError ( {
key ,
expected : '`boolean`' ,
2020-10-15 10:58:26 +02:00
actual : valType ,
2020-08-18 18:36:40 +02:00
} )
}
} else {
// TypeScript trick for type-guarding:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ : never = key
}
} )
2020-07-02 06:53:17 +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://err.sh/vercel/next.js/prefetch-true-deprecated'
)
2016-10-06 09:07:41 +02:00
}
2020-07-02 06:53:17 +02:00
}
const p = props . prefetch !== false
2016-10-06 09:07:41 +02:00
2020-07-07 06:52:26 +02:00
const router = useRouter ( )
2020-07-29 08:56:33 +02:00
const pathname = ( router && router . pathname ) || '/'
2020-07-07 06:52:26 +02:00
const { href , as } = React . useMemo ( ( ) = > {
2020-09-02 18:23:26 +02:00
const [ resolvedHref , resolvedAs ] = resolveHref ( pathname , props . href , true )
2020-07-07 06:52:26 +02:00
return {
href : resolvedHref ,
2020-09-02 18:23:26 +02:00
as : props . as
? resolveHref ( pathname , props . as )
: resolvedAs || resolvedHref ,
2020-07-07 06:52:26 +02:00
}
2020-07-29 08:56:33 +02:00
} , [ pathname , props . href , props . as ] )
2016-10-06 09:07:41 +02:00
2020-10-15 10:58:26 +02:00
let { children , replace , shallow , scroll , locale } = props
2020-11-02 18:22:40 +01:00
2020-07-02 06:53:17 +02:00
// Deprecated. Warning shown by propType check. If the children provided is a string (<Link>example</Link>) we wrap it in an <a> tag
if ( typeof children === 'string' ) {
children = < a > { children } < / a >
2019-05-30 03:19:32 +02:00
}
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
// This will return the first child, if multiple are provided it will throw an error
const child : any = Children . only ( children )
2020-11-01 04:37:28 +01:00
const childRef : any = child && typeof child === 'object' && child . ref
2020-11-07 00:03:15 +01:00
const [ setIntersectionRef , isVisible ] = useIntersection ( {
rootMargin : '200px' ,
} )
2020-11-01 04:37:28 +01:00
const setRef = React . useCallback (
( el : Element ) = > {
2020-11-07 00:03:15 +01:00
setIntersectionRef ( el )
2020-11-01 04:37:28 +01:00
if ( childRef ) {
if ( typeof childRef === 'function' ) childRef ( el )
else if ( typeof childRef === 'object' ) {
childRef . current = el
}
}
} ,
2020-11-07 00:03:15 +01:00
[ childRef , setIntersectionRef ]
2020-11-01 04:37:28 +01:00
)
2020-11-07 00:03:15 +01:00
useEffect ( ( ) = > {
const shouldPrefetch = isVisible && p && isLocalURL ( href )
2020-11-17 19:04:07 +01:00
const curLocale =
typeof locale !== 'undefined' ? locale : router && router . locale
const isPrefetched =
prefetched [ href + '%' + as + ( curLocale ? '%' + curLocale : '' ) ]
2020-11-07 00:03:15 +01:00
if ( shouldPrefetch && ! isPrefetched ) {
prefetch ( router , href , as , {
2020-11-17 19:04:07 +01:00
locale : curLocale ,
2020-11-07 00:03:15 +01:00
} )
}
} , [ as , href , isVisible , locale , p , router ] )
2020-11-01 04:37:28 +01:00
2020-07-02 06:53:17 +02:00
const childProps : {
onMouseEnter? : React.MouseEventHandler
onClick : React.MouseEventHandler
href? : string
ref? : any
} = {
2020-11-01 04:37:28 +01:00
ref : setRef ,
2020-07-02 06:53:17 +02:00
onClick : ( e : React.MouseEvent ) = > {
if ( child . props && typeof child . props . onClick === 'function' ) {
child . props . onClick ( e )
}
if ( ! e . defaultPrevented ) {
2020-10-15 10:58:26 +02:00
linkClicked ( e , router , href , as , replace , shallow , scroll , locale )
2020-07-02 06:53:17 +02:00
}
} ,
2017-02-15 15:01:03 +01:00
}
2020-11-06 03:12:22 +01:00
childProps . onMouseEnter = ( e : React.MouseEvent ) = > {
if ( ! isLocalURL ( href ) ) return
if ( child . props && typeof child . props . onMouseEnter === 'function' ) {
child . props . onMouseEnter ( e )
2017-02-03 21:27:12 +01:00
}
2020-11-06 03:12:22 +01:00
prefetch ( router , href , as , { priority : true } )
2020-07-02 06:53:17 +02:00
}
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +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 ( props . passHref || ( child . type === 'a' && ! ( 'href' in child . props ) ) ) {
2020-12-31 09:07:51 +01:00
const curLocale =
typeof locale !== 'undefined' ? locale : router && router . locale
const localeDomain = getDomainLocale (
as ,
curLocale ,
router && router . locales ,
router && router . domainLocales
2020-10-08 13:12:17 +02:00
)
2020-12-31 09:07:51 +01:00
childProps . href =
localeDomain ||
addBasePath ( addLocale ( as , curLocale , router && router . defaultLocale ) )
2020-07-02 06:53:17 +02:00
}
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
return React . cloneElement ( child , childProps )
2016-10-06 09:07:41 +02:00
}
2018-08-07 05:23:28 +02:00
export default Link