Skip to content

Commit be5ec35

Browse files
committed
add onNavigate for link
1 parent 3c2670d commit be5ec35

File tree

12 files changed

+412
-8
lines changed

12 files changed

+412
-8
lines changed

packages/next/src/client/app-dir/link.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
onNavigationIntent,
1818
unmountLinkInstance,
1919
} from '../components/links'
20+
import { isLocalURL } from '../../shared/lib/router/utils/is-local-url'
2021

2122
type Url = string | UrlObject
2223
type RequiredKeys<T> = {
@@ -26,6 +27,10 @@ type OptionalKeys<T> = {
2627
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
2728
}[keyof T]
2829

30+
type LinkOnNavigateEventHandler = (event: {
31+
preventDefault: () => void
32+
}) => void
33+
2934
type InternalLinkProps = {
3035
/**
3136
* **Required**. The path or URL to navigate to. It can also be an object (similar to `URL`).
@@ -192,6 +197,11 @@ type InternalLinkProps = {
192197
* Optional event handler for when the `<Link>` is clicked.
193198
*/
194199
onClick?: React.MouseEventHandler<HTMLAnchorElement>
200+
201+
/**
202+
* Optional event handler for when the `<Link>` is navigated.
203+
*/
204+
onNavigate?: LinkOnNavigateEventHandler
195205
}
196206

197207
// TODO-APP: Include the full set of Anchor props
@@ -224,21 +234,36 @@ function linkClicked(
224234
as: string,
225235
replace?: boolean,
226236
shallow?: boolean,
227-
scroll?: boolean
237+
scroll?: boolean,
238+
onNavigate?: LinkOnNavigateEventHandler
228239
): void {
229240
const { nodeName } = e.currentTarget
230241

231242
// anchors inside an svg have a lowercase nodeName
232243
const isAnchorNodeName = nodeName.toUpperCase() === 'A'
233244

234-
if (isAnchorNodeName && isModifiedEvent(e)) {
245+
if ((isAnchorNodeName && isModifiedEvent(e)) || !isLocalURL(href)) {
235246
// ignore click for browser’s default behavior
236247
return
237248
}
238249

239250
e.preventDefault()
240251

241252
const navigate = () => {
253+
if (onNavigate) {
254+
let isDefaultPrevented = false
255+
256+
onNavigate({
257+
preventDefault: () => {
258+
isDefaultPrevented = true
259+
},
260+
})
261+
262+
if (isDefaultPrevented) {
263+
return
264+
}
265+
}
266+
242267
// If the router is an NextRouter instance it will have `beforePopState`
243268
const routerScroll = scroll ?? true
244269
if ('beforePopState' in router) {
@@ -296,6 +321,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
296321
onMouseEnter: onMouseEnterProp,
297322
onTouchStart: onTouchStartProp,
298323
legacyBehavior = false,
324+
onNavigate,
299325
...restProps
300326
} = props
301327

@@ -372,6 +398,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
372398
onMouseEnter: true,
373399
onTouchStart: true,
374400
legacyBehavior: true,
401+
onNavigate: true,
375402
} as const
376403
const optionalProps: LinkPropsOptional[] = Object.keys(
377404
optionalPropsGuard
@@ -390,7 +417,8 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
390417
} else if (
391418
key === 'onClick' ||
392419
key === 'onMouseEnter' ||
393-
key === 'onTouchStart'
420+
key === 'onTouchStart' ||
421+
key === 'onNavigate'
394422
) {
395423
if (props[key] && valType !== 'function') {
396424
throw createPropError({
@@ -562,7 +590,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
562590
return
563591
}
564592

565-
linkClicked(e, router, href, as, replace, shallow, scroll)
593+
linkClicked(e, router, href, as, replace, shallow, scroll, onNavigate)
566594
},
567595
onMouseEnter(e) {
568596
if (!legacyBehavior && typeof onMouseEnterProp === 'function') {

packages/next/src/client/link.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ type OptionalKeys<T> = {
2727
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
2828
}[keyof T]
2929

30+
type LinkOnNavigateEventHandler = (event: {
31+
preventDefault: () => void
32+
}) => void
33+
3034
type InternalLinkProps = {
3135
/**
3236
* The path or URL to navigate to. It can also be an object.
@@ -104,6 +108,10 @@ type InternalLinkProps = {
104108
* Optional event handler for when Link is clicked.
105109
*/
106110
onClick?: React.MouseEventHandler<HTMLAnchorElement>
111+
/**
112+
* Optional event handler for when the `<Link>` is navigated.
113+
*/
114+
onNavigate?: LinkOnNavigateEventHandler
107115
}
108116

109117
// TODO-APP: Include the full set of Anchor props
@@ -196,7 +204,8 @@ function linkClicked(
196204
replace?: boolean,
197205
shallow?: boolean,
198206
scroll?: boolean,
199-
locale?: string | false
207+
locale?: string | false,
208+
onNavigate?: LinkOnNavigateEventHandler
200209
): void {
201210
const { nodeName } = e.currentTarget
202211

@@ -211,6 +220,20 @@ function linkClicked(
211220
e.preventDefault()
212221

213222
const navigate = () => {
223+
if (onNavigate) {
224+
let isDefaultPrevented = false
225+
226+
onNavigate({
227+
preventDefault: () => {
228+
isDefaultPrevented = true
229+
},
230+
})
231+
232+
if (isDefaultPrevented) {
233+
return
234+
}
235+
}
236+
214237
// If the router is an NextRouter instance it will have `beforePopState`
215238
const routerScroll = scroll ?? true
216239
if ('beforePopState' in router) {
@@ -226,7 +249,7 @@ function linkClicked(
226249
}
227250
}
228251

229-
navigate()
252+
React.startTransition(navigate)
230253
}
231254

232255
type LinkPropsReal = React.PropsWithChildren<
@@ -265,6 +288,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
265288
scroll,
266289
locale,
267290
onClick,
291+
onNavigate,
268292
onMouseEnter: onMouseEnterProp,
269293
onTouchStart: onTouchStartProp,
270294
legacyBehavior = false,
@@ -338,6 +362,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
338362
onMouseEnter: true,
339363
onTouchStart: true,
340364
legacyBehavior: true,
365+
onNavigate: true,
341366
} as const
342367
const optionalProps: LinkPropsOptional[] = Object.keys(
343368
optionalPropsGuard
@@ -364,7 +389,8 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
364389
} else if (
365390
key === 'onClick' ||
366391
key === 'onMouseEnter' ||
367-
key === 'onTouchStart'
392+
key === 'onTouchStart' ||
393+
key === 'onNavigate'
368394
) {
369395
if (props[key] && valType !== 'function') {
370396
throw createPropError({
@@ -539,7 +565,17 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
539565
return
540566
}
541567

542-
linkClicked(e, router, href, as, replace, shallow, scroll, locale)
568+
linkClicked(
569+
e,
570+
router,
571+
href,
572+
as,
573+
replace,
574+
shallow,
575+
scroll,
576+
locale,
577+
onNavigate
578+
)
543579
},
544580
onMouseEnter(e) {
545581
if (!legacyBehavior && typeof onMouseEnterProp === 'function') {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use client'
2+
3+
import React from 'react'
4+
import OnNavigate from '../../shared/OnNavigate'
5+
6+
export default function Layout({ children }: { children: React.ReactNode }) {
7+
return <OnNavigate rootPath="/app-router">{children}</OnNavigate>
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react'
2+
3+
export default function Home() {
4+
return <div>Home</div>
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react'
2+
3+
export default function Subpage() {
4+
return <div>Subpage</div>
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
3+
export default function RootLayout({
4+
children,
5+
}: {
6+
children: React.ReactNode
7+
}) {
8+
return (
9+
<html>
10+
<body>{children}</body>
11+
</html>
12+
)
13+
}

0 commit comments

Comments
 (0)