Skip to content

Commit afdf1e8

Browse files
committed
add onNavigate for link
1 parent 8e2f470 commit afdf1e8

File tree

12 files changed

+407
-8
lines changed

12 files changed

+407
-8
lines changed

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

Lines changed: 34 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,8 @@ type OptionalKeys<T> = {
2627
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
2728
}[keyof T]
2829

30+
type OnNavigateEventHandler = (event: { preventDefault: () => void }) => void
31+
2932
type InternalLinkProps = {
3033
/**
3134
* **Required**. The path or URL to navigate to. It can also be an object (similar to `URL`).
@@ -192,6 +195,11 @@ type InternalLinkProps = {
192195
* Optional event handler for when the `<Link>` is clicked.
193196
*/
194197
onClick?: React.MouseEventHandler<HTMLAnchorElement>
198+
199+
/**
200+
* Optional event handler for when the `<Link>` is navigated.
201+
*/
202+
onNavigate?: OnNavigateEventHandler
195203
}
196204

197205
// TODO-APP: Include the full set of Anchor props
@@ -224,21 +232,40 @@ function linkClicked(
224232
as: string,
225233
replace?: boolean,
226234
shallow?: boolean,
227-
scroll?: boolean
235+
scroll?: boolean,
236+
onNavigate?: OnNavigateEventHandler
228237
): void {
229238
const { nodeName } = e.currentTarget
230239

231240
// anchors inside an svg have a lowercase nodeName
232241
const isAnchorNodeName = nodeName.toUpperCase() === 'A'
233242

234-
if (isAnchorNodeName && isModifiedEvent(e)) {
243+
if (
244+
(isAnchorNodeName && isModifiedEvent(e)) ||
245+
!isLocalURL(href) ||
246+
e.currentTarget.hasAttribute('download')
247+
) {
235248
// ignore click for browser’s default behavior
236249
return
237250
}
238251

239252
e.preventDefault()
240253

241254
const navigate = () => {
255+
if (onNavigate) {
256+
let isDefaultPrevented = false
257+
258+
onNavigate({
259+
preventDefault: () => {
260+
isDefaultPrevented = true
261+
},
262+
})
263+
264+
if (isDefaultPrevented) {
265+
return
266+
}
267+
}
268+
242269
// If the router is an NextRouter instance it will have `beforePopState`
243270
const routerScroll = scroll ?? true
244271
if ('beforePopState' in router) {
@@ -296,6 +323,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
296323
onMouseEnter: onMouseEnterProp,
297324
onTouchStart: onTouchStartProp,
298325
legacyBehavior = false,
326+
onNavigate,
299327
...restProps
300328
} = props
301329

@@ -372,6 +400,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
372400
onMouseEnter: true,
373401
onTouchStart: true,
374402
legacyBehavior: true,
403+
onNavigate: true,
375404
} as const
376405
const optionalProps: LinkPropsOptional[] = Object.keys(
377406
optionalPropsGuard
@@ -390,7 +419,8 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
390419
} else if (
391420
key === 'onClick' ||
392421
key === 'onMouseEnter' ||
393-
key === 'onTouchStart'
422+
key === 'onTouchStart' ||
423+
key === 'onNavigate'
394424
) {
395425
if (props[key] && valType !== 'function') {
396426
throw createPropError({
@@ -562,7 +592,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
562592
return
563593
}
564594

565-
linkClicked(e, router, href, as, replace, shallow, scroll)
595+
linkClicked(e, router, href, as, replace, shallow, scroll, onNavigate)
566596
},
567597
onMouseEnter(e) {
568598
if (!legacyBehavior && typeof onMouseEnterProp === 'function') {

packages/next/src/client/link.tsx

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

30+
type OnNavigateEventHandler = (event: { preventDefault: () => void }) => void
31+
3032
type InternalLinkProps = {
3133
/**
3234
* The path or URL to navigate to. It can also be an object.
@@ -104,6 +106,10 @@ type InternalLinkProps = {
104106
* Optional event handler for when Link is clicked.
105107
*/
106108
onClick?: React.MouseEventHandler<HTMLAnchorElement>
109+
/**
110+
* Optional event handler for when the `<Link>` is navigated.
111+
*/
112+
onNavigate?: OnNavigateEventHandler
107113
}
108114

109115
// TODO-APP: Include the full set of Anchor props
@@ -196,21 +202,40 @@ function linkClicked(
196202
replace?: boolean,
197203
shallow?: boolean,
198204
scroll?: boolean,
199-
locale?: string | false
205+
locale?: string | false,
206+
onNavigate?: OnNavigateEventHandler
200207
): void {
201208
const { nodeName } = e.currentTarget
202209

203210
// anchors inside an svg have a lowercase nodeName
204211
const isAnchorNodeName = nodeName.toUpperCase() === 'A'
205212

206-
if (isAnchorNodeName && (isModifiedEvent(e) || !isLocalURL(href))) {
213+
if (
214+
(isAnchorNodeName && isModifiedEvent(e)) ||
215+
!isLocalURL(href) ||
216+
e.currentTarget.hasAttribute('download')
217+
) {
207218
// ignore click for browser’s default behavior
208219
return
209220
}
210221

211222
e.preventDefault()
212223

213224
const navigate = () => {
225+
if (onNavigate) {
226+
let isDefaultPrevented = false
227+
228+
onNavigate({
229+
preventDefault: () => {
230+
isDefaultPrevented = true
231+
},
232+
})
233+
234+
if (isDefaultPrevented) {
235+
return
236+
}
237+
}
238+
214239
// If the router is an NextRouter instance it will have `beforePopState`
215240
const routerScroll = scroll ?? true
216241
if ('beforePopState' in router) {
@@ -265,6 +290,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
265290
scroll,
266291
locale,
267292
onClick,
293+
onNavigate,
268294
onMouseEnter: onMouseEnterProp,
269295
onTouchStart: onTouchStartProp,
270296
legacyBehavior = false,
@@ -338,6 +364,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
338364
onMouseEnter: true,
339365
onTouchStart: true,
340366
legacyBehavior: true,
367+
onNavigate: true,
341368
} as const
342369
const optionalProps: LinkPropsOptional[] = Object.keys(
343370
optionalPropsGuard
@@ -364,7 +391,8 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
364391
} else if (
365392
key === 'onClick' ||
366393
key === 'onMouseEnter' ||
367-
key === 'onTouchStart'
394+
key === 'onTouchStart' ||
395+
key === 'onNavigate'
368396
) {
369397
if (props[key] && valType !== 'function') {
370398
throw createPropError({
@@ -539,7 +567,17 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
539567
return
540568
}
541569

542-
linkClicked(e, router, href, as, replace, shallow, scroll, locale)
570+
linkClicked(
571+
e,
572+
router,
573+
href,
574+
as,
575+
replace,
576+
shallow,
577+
scroll,
578+
locale,
579+
onNavigate
580+
)
543581
},
544582
onMouseEnter(e) {
545583
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)