@tanstack/react-router 1.127.9 → 1.128.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/link.cjs +98 -67
- package/dist/cjs/link.cjs.map +1 -1
- package/dist/cjs/utils.cjs +4 -10
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +1 -1
- package/dist/esm/link.js +99 -68
- package/dist/esm/link.js.map +1 -1
- package/dist/esm/utils.d.ts +1 -1
- package/dist/esm/utils.js +4 -10
- package/dist/esm/utils.js.map +1 -1
- package/dist/llms/rules/guide.d.ts +1 -1
- package/dist/llms/rules/guide.js +469 -0
- package/dist/llms/rules/routing.d.ts +1 -1
- package/dist/llms/rules/routing.js +34 -0
- package/package.json +2 -2
- package/src/link.tsx +124 -88
- package/src/utils.ts +6 -14
package/src/link.tsx
CHANGED
|
@@ -10,17 +10,12 @@ import {
|
|
|
10
10
|
import { useRouterState } from './useRouterState'
|
|
11
11
|
import { useRouter } from './useRouter'
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
useForwardedRef,
|
|
15
|
-
useIntersectionObserver,
|
|
16
|
-
useLayoutEffect,
|
|
17
|
-
} from './utils'
|
|
13
|
+
import { useForwardedRef, useIntersectionObserver } from './utils'
|
|
18
14
|
|
|
19
15
|
import { useMatch } from './useMatch'
|
|
20
16
|
import type {
|
|
21
17
|
AnyRouter,
|
|
22
18
|
Constrain,
|
|
23
|
-
LinkCurrentTargetElement,
|
|
24
19
|
LinkOptions,
|
|
25
20
|
RegisteredRouter,
|
|
26
21
|
RoutePaths,
|
|
@@ -48,8 +43,8 @@ export function useLinkProps<
|
|
|
48
43
|
|
|
49
44
|
const {
|
|
50
45
|
// custom props
|
|
51
|
-
activeProps
|
|
52
|
-
inactiveProps
|
|
46
|
+
activeProps,
|
|
47
|
+
inactiveProps,
|
|
53
48
|
activeOptions,
|
|
54
49
|
to,
|
|
55
50
|
preload: userPreload,
|
|
@@ -71,10 +66,6 @@ export function useLinkProps<
|
|
|
71
66
|
onMouseLeave,
|
|
72
67
|
onTouchStart,
|
|
73
68
|
ignoreBlocker,
|
|
74
|
-
...rest
|
|
75
|
-
} = options
|
|
76
|
-
|
|
77
|
-
const {
|
|
78
69
|
// prevent these from being returned
|
|
79
70
|
params: _params,
|
|
80
71
|
search: _search,
|
|
@@ -83,8 +74,10 @@ export function useLinkProps<
|
|
|
83
74
|
mask: _mask,
|
|
84
75
|
reloadDocument: _reloadDocument,
|
|
85
76
|
unsafeRelative: _unsafeRelative,
|
|
77
|
+
from: _from,
|
|
78
|
+
_fromLocation,
|
|
86
79
|
...propsSafeToSpread
|
|
87
|
-
} =
|
|
80
|
+
} = options
|
|
88
81
|
|
|
89
82
|
// If this link simply reloads the current route,
|
|
90
83
|
// make sure it has a new key so it will trigger a data refresh
|
|
@@ -94,7 +87,7 @@ export function useLinkProps<
|
|
|
94
87
|
|
|
95
88
|
const type: 'internal' | 'external' = React.useMemo(() => {
|
|
96
89
|
try {
|
|
97
|
-
new URL(
|
|
90
|
+
new URL(to as any)
|
|
98
91
|
return 'external'
|
|
99
92
|
} catch {}
|
|
100
93
|
return 'internal'
|
|
@@ -106,33 +99,41 @@ export function useLinkProps<
|
|
|
106
99
|
structuralSharing: true as any,
|
|
107
100
|
})
|
|
108
101
|
|
|
109
|
-
const
|
|
102
|
+
const from = useMatch({
|
|
110
103
|
strict: false,
|
|
111
|
-
select: (match) => match.fullPath,
|
|
104
|
+
select: (match) => options.from ?? match.fullPath,
|
|
112
105
|
})
|
|
113
106
|
|
|
114
|
-
const from = options.from ?? nearestFrom
|
|
115
|
-
|
|
116
|
-
// Use it as the default `from` location
|
|
117
|
-
options = { ...options, from }
|
|
118
|
-
|
|
119
107
|
const next = React.useMemo(
|
|
120
|
-
() => router.buildLocation(options as any),
|
|
108
|
+
() => router.buildLocation({ ...options, from } as any),
|
|
121
109
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
122
|
-
[
|
|
110
|
+
[
|
|
111
|
+
router,
|
|
112
|
+
currentSearch,
|
|
113
|
+
options._fromLocation,
|
|
114
|
+
from,
|
|
115
|
+
options.hash,
|
|
116
|
+
options.to,
|
|
117
|
+
options.search,
|
|
118
|
+
options.params,
|
|
119
|
+
options.state,
|
|
120
|
+
options.mask,
|
|
121
|
+
options.unsafeRelative,
|
|
122
|
+
],
|
|
123
123
|
)
|
|
124
124
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
const isExternal = type === 'external'
|
|
126
|
+
|
|
127
|
+
const preload =
|
|
128
|
+
options.reloadDocument || isExternal
|
|
129
|
+
? false
|
|
130
|
+
: (userPreload ?? router.options.defaultPreload)
|
|
131
131
|
const preloadDelay =
|
|
132
132
|
userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
|
|
133
133
|
|
|
134
134
|
const isActive = useRouterState({
|
|
135
135
|
select: (s) => {
|
|
136
|
+
if (isExternal) return false
|
|
136
137
|
if (activeOptions?.exact) {
|
|
137
138
|
const testExact = exactPathTest(
|
|
138
139
|
s.location.pathname,
|
|
@@ -146,15 +147,17 @@ export function useLinkProps<
|
|
|
146
147
|
const currentPathSplit = removeTrailingSlash(
|
|
147
148
|
s.location.pathname,
|
|
148
149
|
router.basepath,
|
|
149
|
-
)
|
|
150
|
+
)
|
|
150
151
|
const nextPathSplit = removeTrailingSlash(
|
|
151
152
|
next.pathname,
|
|
152
153
|
router.basepath,
|
|
153
|
-
).split('/')
|
|
154
|
-
|
|
155
|
-
const pathIsFuzzyEqual = nextPathSplit.every(
|
|
156
|
-
(d, i) => d === currentPathSplit[i],
|
|
157
154
|
)
|
|
155
|
+
|
|
156
|
+
const pathIsFuzzyEqual =
|
|
157
|
+
currentPathSplit.startsWith(nextPathSplit) &&
|
|
158
|
+
(currentPathSplit.length === nextPathSplit.length ||
|
|
159
|
+
currentPathSplit[nextPathSplit.length] === '/')
|
|
160
|
+
|
|
158
161
|
if (!pathIsFuzzyEqual) {
|
|
159
162
|
return false
|
|
160
163
|
}
|
|
@@ -177,12 +180,34 @@ export function useLinkProps<
|
|
|
177
180
|
},
|
|
178
181
|
})
|
|
179
182
|
|
|
180
|
-
const doPreload = React.useCallback(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
const doPreload = React.useCallback(
|
|
184
|
+
() => {
|
|
185
|
+
router.preloadRoute({ ...options, from } as any).catch((err) => {
|
|
186
|
+
console.warn(err)
|
|
187
|
+
console.warn(preloadWarning)
|
|
188
|
+
})
|
|
189
|
+
},
|
|
190
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
191
|
+
[
|
|
192
|
+
router,
|
|
193
|
+
options.to,
|
|
194
|
+
options._fromLocation,
|
|
195
|
+
from,
|
|
196
|
+
options.search,
|
|
197
|
+
options.hash,
|
|
198
|
+
options.params,
|
|
199
|
+
options.state,
|
|
200
|
+
options.mask,
|
|
201
|
+
options.unsafeRelative,
|
|
202
|
+
options.hashScrollIntoView,
|
|
203
|
+
options.href,
|
|
204
|
+
options.ignoreBlocker,
|
|
205
|
+
options.reloadDocument,
|
|
206
|
+
options.replace,
|
|
207
|
+
options.resetScroll,
|
|
208
|
+
options.viewTransition,
|
|
209
|
+
],
|
|
210
|
+
)
|
|
186
211
|
|
|
187
212
|
const preloadViewportIoCallback = React.useCallback(
|
|
188
213
|
(entry: IntersectionObserverEntry | undefined) => {
|
|
@@ -196,11 +221,11 @@ export function useLinkProps<
|
|
|
196
221
|
useIntersectionObserver(
|
|
197
222
|
innerRef,
|
|
198
223
|
preloadViewportIoCallback,
|
|
199
|
-
|
|
224
|
+
intersectionObserverOptions,
|
|
200
225
|
{ disabled: !!disabled || !(preload === 'viewport') },
|
|
201
226
|
)
|
|
202
227
|
|
|
203
|
-
|
|
228
|
+
React.useEffect(() => {
|
|
204
229
|
if (hasRenderFetched.current) {
|
|
205
230
|
return
|
|
206
231
|
}
|
|
@@ -210,7 +235,7 @@ export function useLinkProps<
|
|
|
210
235
|
}
|
|
211
236
|
}, [disabled, doPreload, preload])
|
|
212
237
|
|
|
213
|
-
if (
|
|
238
|
+
if (isExternal) {
|
|
214
239
|
return {
|
|
215
240
|
...propsSafeToSpread,
|
|
216
241
|
ref: innerRef as React.ComponentPropsWithRef<'a'>['ref'],
|
|
@@ -230,7 +255,7 @@ export function useLinkProps<
|
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
// The click handler
|
|
233
|
-
const handleClick = (e: MouseEvent) => {
|
|
258
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
234
259
|
if (
|
|
235
260
|
!disabled &&
|
|
236
261
|
!isCtrlEvent(e) &&
|
|
@@ -253,6 +278,7 @@ export function useLinkProps<
|
|
|
253
278
|
// N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing
|
|
254
279
|
return router.navigate({
|
|
255
280
|
...options,
|
|
281
|
+
from,
|
|
256
282
|
replace,
|
|
257
283
|
resetScroll,
|
|
258
284
|
hashScrollIntoView,
|
|
@@ -264,7 +290,7 @@ export function useLinkProps<
|
|
|
264
290
|
}
|
|
265
291
|
|
|
266
292
|
// The click handler
|
|
267
|
-
const handleFocus = (_: MouseEvent) => {
|
|
293
|
+
const handleFocus = (_: React.MouseEvent) => {
|
|
268
294
|
if (disabled) return
|
|
269
295
|
if (preload) {
|
|
270
296
|
doPreload()
|
|
@@ -273,54 +299,44 @@ export function useLinkProps<
|
|
|
273
299
|
|
|
274
300
|
const handleTouchStart = handleFocus
|
|
275
301
|
|
|
276
|
-
const handleEnter = (e: MouseEvent) => {
|
|
277
|
-
if (disabled) return
|
|
278
|
-
const eventTarget = (e.target || {}) as LinkCurrentTargetElement
|
|
302
|
+
const handleEnter = (e: React.MouseEvent) => {
|
|
303
|
+
if (disabled || !preload) return
|
|
279
304
|
|
|
280
|
-
if (
|
|
281
|
-
|
|
305
|
+
if (!preloadDelay) {
|
|
306
|
+
doPreload()
|
|
307
|
+
} else {
|
|
308
|
+
const eventTarget = e.target
|
|
309
|
+
if (timeoutMap.has(eventTarget)) {
|
|
282
310
|
return
|
|
283
311
|
}
|
|
284
|
-
|
|
285
|
-
|
|
312
|
+
const id = setTimeout(() => {
|
|
313
|
+
timeoutMap.delete(eventTarget)
|
|
286
314
|
doPreload()
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
eventTarget.preloadTimeout = null
|
|
290
|
-
doPreload()
|
|
291
|
-
}, preloadDelay)
|
|
292
|
-
}
|
|
315
|
+
}, preloadDelay)
|
|
316
|
+
timeoutMap.set(eventTarget, id)
|
|
293
317
|
}
|
|
294
318
|
}
|
|
295
319
|
|
|
296
|
-
const handleLeave = (e: MouseEvent) => {
|
|
297
|
-
if (disabled) return
|
|
298
|
-
const eventTarget =
|
|
299
|
-
|
|
300
|
-
if (
|
|
301
|
-
clearTimeout(
|
|
302
|
-
eventTarget
|
|
320
|
+
const handleLeave = (e: React.MouseEvent) => {
|
|
321
|
+
if (disabled || !preload || !preloadDelay) return
|
|
322
|
+
const eventTarget = e.target
|
|
323
|
+
const id = timeoutMap.get(eventTarget)
|
|
324
|
+
if (id) {
|
|
325
|
+
clearTimeout(id)
|
|
326
|
+
timeoutMap.delete(eventTarget)
|
|
303
327
|
}
|
|
304
328
|
}
|
|
305
329
|
|
|
306
|
-
const composeHandlers =
|
|
307
|
-
(handlers: Array<undefined | ((e: any) => void)>) =>
|
|
308
|
-
(e: { persist?: () => void; defaultPrevented: boolean }) => {
|
|
309
|
-
e.persist?.()
|
|
310
|
-
handlers.filter(Boolean).forEach((handler) => {
|
|
311
|
-
if (e.defaultPrevented) return
|
|
312
|
-
handler!(e)
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
|
|
316
330
|
// Get the active props
|
|
317
331
|
const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
|
|
318
|
-
? (functionalUpdate(activeProps as any, {}) ??
|
|
319
|
-
:
|
|
332
|
+
? (functionalUpdate(activeProps as any, {}) ?? STATIC_ACTIVE_OBJECT)
|
|
333
|
+
: STATIC_EMPTY_OBJECT
|
|
320
334
|
|
|
321
335
|
// Get the inactive props
|
|
322
336
|
const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
|
|
323
|
-
isActive
|
|
337
|
+
isActive
|
|
338
|
+
? STATIC_EMPTY_OBJECT
|
|
339
|
+
: (functionalUpdate(inactiveProps, {}) ?? STATIC_EMPTY_OBJECT)
|
|
324
340
|
|
|
325
341
|
const resolvedClassName = [
|
|
326
342
|
className,
|
|
@@ -330,7 +346,9 @@ export function useLinkProps<
|
|
|
330
346
|
.filter(Boolean)
|
|
331
347
|
.join(' ')
|
|
332
348
|
|
|
333
|
-
const resolvedStyle =
|
|
349
|
+
const resolvedStyle = (style ||
|
|
350
|
+
resolvedActiveProps.style ||
|
|
351
|
+
resolvedInactiveProps.style) && {
|
|
334
352
|
...style,
|
|
335
353
|
...resolvedActiveProps.style,
|
|
336
354
|
...resolvedInactiveProps.style,
|
|
@@ -353,17 +371,35 @@ export function useLinkProps<
|
|
|
353
371
|
onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
|
|
354
372
|
disabled: !!disabled,
|
|
355
373
|
target,
|
|
356
|
-
...(
|
|
374
|
+
...(resolvedStyle && { style: resolvedStyle }),
|
|
357
375
|
...(resolvedClassName && { className: resolvedClassName }),
|
|
358
|
-
...(disabled &&
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}),
|
|
362
|
-
...(isActive && { 'data-status': 'active', 'aria-current': 'page' }),
|
|
363
|
-
...(isTransitioning && { 'data-transitioning': 'transitioning' }),
|
|
376
|
+
...(disabled && STATIC_DISABLED_PROPS),
|
|
377
|
+
...(isActive && STATIC_ACTIVE_PROPS),
|
|
378
|
+
...(isTransitioning && STATIC_TRANSITIONING_PROPS),
|
|
364
379
|
}
|
|
365
380
|
}
|
|
366
381
|
|
|
382
|
+
const STATIC_EMPTY_OBJECT = {}
|
|
383
|
+
const STATIC_ACTIVE_OBJECT = { className: 'active' }
|
|
384
|
+
const STATIC_DISABLED_PROPS = { role: 'link', 'aria-disabled': true }
|
|
385
|
+
const STATIC_ACTIVE_PROPS = { 'data-status': 'active', 'aria-current': 'page' }
|
|
386
|
+
const STATIC_TRANSITIONING_PROPS = { 'data-transitioning': 'transitioning' }
|
|
387
|
+
|
|
388
|
+
const timeoutMap = new WeakMap<EventTarget, ReturnType<typeof setTimeout>>()
|
|
389
|
+
|
|
390
|
+
const intersectionObserverOptions: IntersectionObserverInit = {
|
|
391
|
+
rootMargin: '100px',
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const composeHandlers =
|
|
395
|
+
(handlers: Array<undefined | React.EventHandler<any>>) =>
|
|
396
|
+
(e: React.SyntheticEvent) => {
|
|
397
|
+
handlers.filter(Boolean).forEach((handler) => {
|
|
398
|
+
if (e.defaultPrevented) return
|
|
399
|
+
handler!(e)
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
367
403
|
type UseLinkReactProps<TComp> = TComp extends keyof React.JSX.IntrinsicElements
|
|
368
404
|
? React.JSX.IntrinsicElements[TComp]
|
|
369
405
|
: React.PropsWithoutRef<
|
|
@@ -516,7 +552,7 @@ export const Link: LinkComponent<'a'> = React.forwardRef<Element, any>(
|
|
|
516
552
|
})
|
|
517
553
|
: rest.children
|
|
518
554
|
|
|
519
|
-
if (
|
|
555
|
+
if (_asChild === undefined) {
|
|
520
556
|
// the ReturnType of useLinkProps returns the correct type for a <a> element, not a general component that has a disabled prop
|
|
521
557
|
// @ts-expect-error
|
|
522
558
|
delete linkProps.disabled
|
|
@@ -533,7 +569,7 @@ export const Link: LinkComponent<'a'> = React.forwardRef<Element, any>(
|
|
|
533
569
|
},
|
|
534
570
|
) as any
|
|
535
571
|
|
|
536
|
-
function isCtrlEvent(e: MouseEvent) {
|
|
572
|
+
function isCtrlEvent(e: React.MouseEvent) {
|
|
537
573
|
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
|
|
538
574
|
}
|
|
539
575
|
|
package/src/utils.ts
CHANGED
|
@@ -69,34 +69,26 @@ export function useIntersectionObserver<T extends Element>(
|
|
|
69
69
|
callback: (entry: IntersectionObserverEntry | undefined) => void,
|
|
70
70
|
intersectionObserverOptions: IntersectionObserverInit = {},
|
|
71
71
|
options: { disabled?: boolean } = {},
|
|
72
|
-
)
|
|
73
|
-
const isIntersectionObserverAvailable = React.useRef(
|
|
74
|
-
typeof IntersectionObserver === 'function',
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
const observerRef = React.useRef<IntersectionObserver | null>(null)
|
|
78
|
-
|
|
72
|
+
) {
|
|
79
73
|
React.useEffect(() => {
|
|
80
74
|
if (
|
|
81
75
|
!ref.current ||
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
options.disabled ||
|
|
77
|
+
typeof IntersectionObserver !== 'function'
|
|
84
78
|
) {
|
|
85
79
|
return
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
89
83
|
callback(entry)
|
|
90
84
|
}, intersectionObserverOptions)
|
|
91
85
|
|
|
92
|
-
|
|
86
|
+
observer.observe(ref.current)
|
|
93
87
|
|
|
94
88
|
return () => {
|
|
95
|
-
|
|
89
|
+
observer.disconnect()
|
|
96
90
|
}
|
|
97
91
|
}, [callback, intersectionObserverOptions, options.disabled, ref])
|
|
98
|
-
|
|
99
|
-
return observerRef.current
|
|
100
92
|
}
|
|
101
93
|
|
|
102
94
|
/**
|