1
+ import { ViewTransitionOptions } from "../dom/global" ;
1
2
import type { History , Location , Path , To } from "./history" ;
2
3
import {
3
4
Action as NavigationType ,
@@ -415,6 +416,7 @@ export interface StaticHandler {
415
416
type ViewTransitionOpts = {
416
417
currentLocation : Location ;
417
418
nextLocation : Location ;
419
+ opts ?: ViewTransitionOptions ;
418
420
} ;
419
421
420
422
/**
@@ -464,7 +466,7 @@ type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
464
466
replace ?: boolean ;
465
467
state ?: any ;
466
468
fromRouteId ?: string ;
467
- viewTransition ?: boolean ;
469
+ viewTransition ?: ViewTransitionOptions ;
468
470
} ;
469
471
470
472
// Only allowed for submission navigations
@@ -768,12 +770,19 @@ const defaultMapRouteProperties: MapRoutePropertiesFunction = (route) => ({
768
770
hasErrorBoundary : Boolean ( route . hasErrorBoundary ) ,
769
771
} ) ;
770
772
771
- const TRANSITIONS_STORAGE_KEY = "remix-router-transitions" ;
773
+ export const ROUTER_TRANSITIONS_STORAGE_KEY = "remix-router-transitions" ;
772
774
773
775
// Flag used on new `loaderData` to indicate that we do not want to preserve
774
776
// any prior loader data from the throwing route in `mergeLoaderData`
775
777
const ResetLoaderDataSymbol = Symbol ( "ResetLoaderData" ) ;
776
778
779
+ // The applied view transitions map stores, for each source pathname (string),
780
+ // a mapping from destination pathnames (string) to the view transition option that was used.
781
+ export type AppliedViewTransitionMap = Map <
782
+ string ,
783
+ Map < string , ViewTransitionOptions >
784
+ > ;
785
+
777
786
//#endregion
778
787
779
788
////////////////////////////////////////////////////////////////////////////////
@@ -943,13 +952,14 @@ export function createRouter(init: RouterInit): Router {
943
952
// AbortController for the active navigation
944
953
let pendingNavigationController : AbortController | null ;
945
954
946
- // Should the current navigation enable document.startViewTransition?
947
- let pendingViewTransitionEnabled = false ;
955
+ // Should the current navigation enable document.startViewTransition? (includes custom opts when provided)
956
+ let pendingViewTransition : ViewTransitionOptions = false ;
948
957
949
- // Store applied view transitions so we can apply them on POP
950
- let appliedViewTransitions : Map < string , Set < string > > = new Map <
958
+ // Store, for each "from" pathname, a mapping of "to" pathnames to the viewTransition option.
959
+ // This registry enables us to reapply the appropriate view transition when handling a POP navigation.
960
+ let appliedViewTransitions : AppliedViewTransitionMap = new Map <
951
961
string ,
952
- Set < string >
962
+ Map < string , ViewTransitionOptions >
953
963
> ( ) ;
954
964
955
965
// Cleanup function for persisting applied transitions to sessionStorage
@@ -1261,33 +1271,44 @@ export function createRouter(init: RouterInit): Router {
1261
1271
1262
1272
// On POP, enable transitions if they were enabled on the original navigation
1263
1273
if ( pendingAction === NavigationType . Pop ) {
1264
- // Forward takes precedence so they behave like the original navigation
1265
- let priorPaths = appliedViewTransitions . get ( state . location . pathname ) ;
1266
- if ( priorPaths && priorPaths . has ( location . pathname ) ) {
1274
+ // Try to get the transition mapping from the current (source) location.
1275
+ let vTRegistry = appliedViewTransitions . get ( state . location . pathname ) ;
1276
+ if ( vTRegistry && vTRegistry . has ( location . pathname ) ) {
1277
+ const opts = vTRegistry . get ( location . pathname ) ;
1267
1278
viewTransitionOpts = {
1268
1279
currentLocation : state . location ,
1269
1280
nextLocation : location ,
1281
+ opts,
1270
1282
} ;
1271
1283
} else if ( appliedViewTransitions . has ( location . pathname ) ) {
1272
- // If we don't have a previous forward nav, assume we're popping back to
1273
- // the new location and enable if that location previously enabled
1274
- viewTransitionOpts = {
1275
- currentLocation : location ,
1276
- nextLocation : state . location ,
1277
- } ;
1284
+ // Otherwise, check the reverse mapping from the destination side.
1285
+ let vTRegistry = appliedViewTransitions . get ( location . pathname ) ! ;
1286
+ if ( vTRegistry . has ( state . location . pathname ) ) {
1287
+ const opts = vTRegistry . get ( state . location . pathname ) ;
1288
+ viewTransitionOpts = {
1289
+ currentLocation : location ,
1290
+ nextLocation : state . location ,
1291
+ opts,
1292
+ } ;
1293
+ }
1278
1294
}
1279
- } else if ( pendingViewTransitionEnabled ) {
1280
- // Store the applied transition on PUSH/REPLACE
1281
- let toPaths = appliedViewTransitions . get ( state . location . pathname ) ;
1282
- if ( toPaths ) {
1283
- toPaths . add ( location . pathname ) ;
1284
- } else {
1285
- toPaths = new Set < string > ( [ location . pathname ] ) ;
1286
- appliedViewTransitions . set ( state . location . pathname , toPaths ) ;
1295
+ } else if ( pendingViewTransition ) {
1296
+ // For non-POP navigations ( PUSH/REPLACE) when viewTransition is enabled:
1297
+ // Retrieve the existing transition mapping for the source pathname.
1298
+ let vTRegistry = appliedViewTransitions . get ( state . location . pathname ) ;
1299
+ if ( ! vTRegistry ) {
1300
+ // If no mapping exists, create one.
1301
+ vTRegistry = new Map < string , ViewTransitionOptions > ( ) ;
1302
+ appliedViewTransitions . set ( state . location . pathname , vTRegistry ) ;
1287
1303
}
1304
+ // Record that navigating from the current pathname to the next uses the pending view transition option.
1305
+ vTRegistry . set ( location . pathname , pendingViewTransition ) ;
1306
+
1307
+ // Set the view transition options for the current navigation.
1288
1308
viewTransitionOpts = {
1289
1309
currentLocation : state . location ,
1290
1310
nextLocation : location ,
1311
+ opts : pendingViewTransition , // Retains the full option (boolean or object)
1291
1312
} ;
1292
1313
}
1293
1314
@@ -1317,7 +1338,7 @@ export function createRouter(init: RouterInit): Router {
1317
1338
// Reset stateful navigation vars
1318
1339
pendingAction = NavigationType . Pop ;
1319
1340
pendingPreventScrollReset = false ;
1320
- pendingViewTransitionEnabled = false ;
1341
+ pendingViewTransition = false ;
1321
1342
isUninterruptedRevalidation = false ;
1322
1343
isRevalidationRequired = false ;
1323
1344
pendingRevalidationDfd ?. resolve ( ) ;
@@ -1426,7 +1447,7 @@ export function createRouter(init: RouterInit): Router {
1426
1447
pendingError : error ,
1427
1448
preventScrollReset,
1428
1449
replace : opts && opts . replace ,
1429
- enableViewTransition : opts && opts . viewTransition ,
1450
+ viewTransition : opts && opts . viewTransition ,
1430
1451
flushSync,
1431
1452
} ) ;
1432
1453
}
@@ -1480,7 +1501,7 @@ export function createRouter(init: RouterInit): Router {
1480
1501
{
1481
1502
overrideNavigation : state . navigation ,
1482
1503
// Proxy through any rending view transition
1483
- enableViewTransition : pendingViewTransitionEnabled === true ,
1504
+ viewTransition : pendingViewTransition ,
1484
1505
}
1485
1506
) ;
1486
1507
return promise ;
@@ -1501,7 +1522,7 @@ export function createRouter(init: RouterInit): Router {
1501
1522
startUninterruptedRevalidation ?: boolean ;
1502
1523
preventScrollReset ?: boolean ;
1503
1524
replace ?: boolean ;
1504
- enableViewTransition ?: boolean ;
1525
+ viewTransition ?: ViewTransitionOptions ;
1505
1526
flushSync ?: boolean ;
1506
1527
}
1507
1528
) : Promise < void > {
@@ -1519,7 +1540,8 @@ export function createRouter(init: RouterInit): Router {
1519
1540
saveScrollPosition ( state . location , state . matches ) ;
1520
1541
pendingPreventScrollReset = ( opts && opts . preventScrollReset ) === true ;
1521
1542
1522
- pendingViewTransitionEnabled = ( opts && opts . enableViewTransition ) === true ;
1543
+ pendingViewTransition =
1544
+ opts && opts . viewTransition ? opts . viewTransition : false ;
1523
1545
1524
1546
let routesToUse = inFlightDataRoutes || dataRoutes ;
1525
1547
let loadingNavigation = opts && opts . overrideNavigation ;
@@ -2701,9 +2723,7 @@ export function createRouter(init: RouterInit): Router {
2701
2723
} ,
2702
2724
// Preserve these flags across redirects
2703
2725
preventScrollReset : preventScrollReset || pendingPreventScrollReset ,
2704
- enableViewTransition : isNavigation
2705
- ? pendingViewTransitionEnabled
2706
- : undefined ,
2726
+ viewTransition : isNavigation ? pendingViewTransition : undefined ,
2707
2727
} ) ;
2708
2728
} else {
2709
2729
// If we have a navigation submission, we will preserve it through the
@@ -2718,9 +2738,7 @@ export function createRouter(init: RouterInit): Router {
2718
2738
fetcherSubmission,
2719
2739
// Preserve these flags across redirects
2720
2740
preventScrollReset : preventScrollReset || pendingPreventScrollReset ,
2721
- enableViewTransition : isNavigation
2722
- ? pendingViewTransitionEnabled
2723
- : undefined ,
2741
+ viewTransition : isNavigation ? pendingViewTransition : undefined ,
2724
2742
} ) ;
2725
2743
}
2726
2744
}
@@ -5608,39 +5626,49 @@ function getDoneFetcher(data: Fetcher["data"]): FetcherStates["Idle"] {
5608
5626
return fetcher ;
5609
5627
}
5610
5628
5611
- function restoreAppliedTransitions (
5629
+ export function restoreAppliedTransitions (
5612
5630
_window : Window ,
5613
- transitions : Map < string , Set < string > >
5631
+ transitions : AppliedViewTransitionMap
5614
5632
) {
5615
5633
try {
5616
- let sessionPositions = _window . sessionStorage . getItem (
5617
- TRANSITIONS_STORAGE_KEY
5634
+ const sessionData = _window . sessionStorage . getItem (
5635
+ ROUTER_TRANSITIONS_STORAGE_KEY
5618
5636
) ;
5619
- if ( sessionPositions ) {
5620
- let json = JSON . parse ( sessionPositions ) ;
5621
- for ( let [ k , v ] of Object . entries ( json || { } ) ) {
5622
- if ( v && Array . isArray ( v ) ) {
5623
- transitions . set ( k , new Set ( v || [ ] ) ) ;
5637
+ if ( sessionData ) {
5638
+ // Parse the JSON object into the expected nested structure.
5639
+ const json : Record <
5640
+ string ,
5641
+ Record < string , ViewTransitionOptions >
5642
+ > = JSON . parse ( sessionData ) ;
5643
+ for ( const [ from , toOptsObj ] of Object . entries ( json ) ) {
5644
+ const toOptsMap = new Map < string , ViewTransitionOptions > ( ) ;
5645
+ for ( const [ to , opts ] of Object . entries ( toOptsObj ) ) {
5646
+ toOptsMap . set ( to , opts ) ;
5624
5647
}
5648
+ transitions . set ( from , toOptsMap ) ;
5625
5649
}
5626
5650
}
5627
5651
} catch ( e ) {
5628
- // no-op, use default empty object
5652
+ // On error, simply do nothing.
5629
5653
}
5630
5654
}
5631
5655
5632
- function persistAppliedTransitions (
5656
+ export function persistAppliedTransitions (
5633
5657
_window : Window ,
5634
- transitions : Map < string , Set < string > >
5658
+ transitions : AppliedViewTransitionMap
5635
5659
) {
5636
5660
if ( transitions . size > 0 ) {
5637
- let json : Record < string , string [ ] > = { } ;
5638
- for ( let [ k , v ] of transitions ) {
5639
- json [ k ] = [ ...v ] ;
5661
+ // Convert the nested Map structure into a plain object.
5662
+ const json : Record < string , Record < string , ViewTransitionOptions > > = { } ;
5663
+ for ( const [ from , toOptsMap ] of transitions . entries ( ) ) {
5664
+ json [ from ] = { } ;
5665
+ for ( const [ to , opts ] of toOptsMap . entries ( ) ) {
5666
+ json [ from ] [ to ] = opts ;
5667
+ }
5640
5668
}
5641
5669
try {
5642
5670
_window . sessionStorage . setItem (
5643
- TRANSITIONS_STORAGE_KEY ,
5671
+ ROUTER_TRANSITIONS_STORAGE_KEY ,
5644
5672
JSON . stringify ( json )
5645
5673
) ;
5646
5674
} catch ( error ) {
0 commit comments