@remix-run/router 1.9.0 → 1.10.0-pre.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/CHANGELOG.md +50 -0
- package/dist/router.cjs.js +103 -12
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +15 -1
- package/dist/router.js +96 -6
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +103 -12
- package/dist/router.umd.js.map +1 -1
- package/dist/router.umd.min.js +2 -2
- package/dist/router.umd.min.js.map +1 -1
- package/dist/utils.d.ts +12 -4
- package/package.json +1 -1
- package/router.ts +168 -23
- package/utils.ts +13 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# `@remix-run/router`
|
|
2
2
|
|
|
3
|
+
## 1.10.0-pre.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add support for the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) via `document.startViewTransition` to enable CSS animated transitions on SPA navigations in your application. ([#10916](https://github.com/remix-run/react-router/pull/10916))
|
|
8
|
+
|
|
9
|
+
The simplest approach to enabling a View Transition in your React Router app is via the new `<Link unstable_viewTransition>` prop. This will cause the navigation DOM update to be wrapped in `document.startViewTransition` which will enable transitions for the DOM update. Without any additional CSS styles, you'll get a basic cross-fade animation for your page.
|
|
10
|
+
|
|
11
|
+
If you need to apply more fine-grained styles for your animations, you can leverage the `unstable_useViewTransitionState` hook which will tell you when a transition is in progress and you can use that to apply classes or styles:
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
function ImageLink(to, src, alt) {
|
|
15
|
+
let isTransitioning = unstable_useViewTransitionState(to);
|
|
16
|
+
return (
|
|
17
|
+
<Link to={to} unstable_viewTransition>
|
|
18
|
+
<img
|
|
19
|
+
src={src}
|
|
20
|
+
alt={alt}
|
|
21
|
+
style={{
|
|
22
|
+
viewTransitionName: isTransitioning ? "image-expand" : "",
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
</Link>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
You can also use the `<NavLink unstable_viewTransition>` shorthand which will manage the hook usage for you and automatically add a `transitioning` class to the `<a>` during the transition:
|
|
31
|
+
|
|
32
|
+
```css
|
|
33
|
+
a.transitioning img {
|
|
34
|
+
view-transition-name: "image-expand";
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```jsx
|
|
39
|
+
<NavLink to={to} unstable_viewTransition>
|
|
40
|
+
<img src={src} alt={alt} />
|
|
41
|
+
</NavLink>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For an example usage of View Transitions with React Router, check out [our fork](https://github.com/brophdawg11/react-router-records) of the [Astro Records](https://github.com/Charca/astro-records) demo.
|
|
45
|
+
|
|
46
|
+
For more information on using the View Transitions API, please refer to the [Smooth and simple transitions with the View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions/) guide from the Google Chrome team.
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- Allow 404 detection to leverage root route error boundary if path contains a URL segment ([#10852](https://github.com/remix-run/react-router/pull/10852))
|
|
51
|
+
- Fix `ErrorResponse` type to avoid leaking internal field ([#10876](https://github.com/remix-run/react-router/pull/10876))
|
|
52
|
+
|
|
3
53
|
## 1.9.0
|
|
4
54
|
|
|
5
55
|
### Minor Changes
|
package/dist/router.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @remix-run/router v1.
|
|
2
|
+
* @remix-run/router v1.10.0-pre.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Remix Software Inc.
|
|
5
5
|
*
|
|
@@ -574,8 +574,8 @@ let ResultType = /*#__PURE__*/function (ResultType) {
|
|
|
574
574
|
*/
|
|
575
575
|
|
|
576
576
|
/**
|
|
577
|
-
* Users can specify either lowercase or uppercase form methods on
|
|
578
|
-
* useSubmit(),
|
|
577
|
+
* Users can specify either lowercase or uppercase form methods on `<Form>`,
|
|
578
|
+
* useSubmit(), `<fetcher.Form>`, etc.
|
|
579
579
|
*/
|
|
580
580
|
|
|
581
581
|
/**
|
|
@@ -1468,10 +1468,13 @@ const redirectDocument = (url, init) => {
|
|
|
1468
1468
|
response.headers.set("X-Remix-Reload-Document", "true");
|
|
1469
1469
|
return response;
|
|
1470
1470
|
};
|
|
1471
|
-
|
|
1472
1471
|
/**
|
|
1473
1472
|
* @private
|
|
1474
1473
|
* Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
|
|
1474
|
+
*
|
|
1475
|
+
* We don't export the class for public use since it's an implementation
|
|
1476
|
+
* detail, but we export the interface above so folks can build their own
|
|
1477
|
+
* abstractions around instances via isRouteErrorResponse()
|
|
1475
1478
|
*/
|
|
1476
1479
|
class ErrorResponseImpl {
|
|
1477
1480
|
constructor(status, statusText, data, internal) {
|
|
@@ -1490,9 +1493,6 @@ class ErrorResponseImpl {
|
|
|
1490
1493
|
}
|
|
1491
1494
|
}
|
|
1492
1495
|
|
|
1493
|
-
// We don't want the class exported since usage of it at runtime is an
|
|
1494
|
-
// implementation detail, but we do want to export the shape so folks can
|
|
1495
|
-
// build their own abstractions around instances via isRouteErrorResponse()
|
|
1496
1496
|
/**
|
|
1497
1497
|
* Check if the given error is an ErrorResponse generated from a 4xx/5xx
|
|
1498
1498
|
* Response thrown from an action/loader
|
|
@@ -1612,6 +1612,7 @@ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
|
1612
1612
|
const defaultMapRouteProperties = route => ({
|
|
1613
1613
|
hasErrorBoundary: Boolean(route.hasErrorBoundary)
|
|
1614
1614
|
});
|
|
1615
|
+
const TRANSITIONS_STORAGE_KEY = "remix-router-transitions";
|
|
1615
1616
|
|
|
1616
1617
|
//#endregion
|
|
1617
1618
|
|
|
@@ -1720,6 +1721,15 @@ function createRouter(init) {
|
|
|
1720
1721
|
// AbortController for the active navigation
|
|
1721
1722
|
let pendingNavigationController;
|
|
1722
1723
|
|
|
1724
|
+
// Should the current navigation enable document.startViewTransition?
|
|
1725
|
+
let pendingViewTransitionEnabled = false;
|
|
1726
|
+
|
|
1727
|
+
// Store applied view transitions so we can apply them on POP
|
|
1728
|
+
let appliedViewTransitions = new Map();
|
|
1729
|
+
|
|
1730
|
+
// Cleanup function for persisting applied transitions to sessionStorage
|
|
1731
|
+
let removePageHideEventListener = null;
|
|
1732
|
+
|
|
1723
1733
|
// We use this to avoid touching history in completeNavigation if a
|
|
1724
1734
|
// revalidation is entirely uninterrupted
|
|
1725
1735
|
let isUninterruptedRevalidation = false;
|
|
@@ -1827,6 +1837,14 @@ function createRouter(init) {
|
|
|
1827
1837
|
}
|
|
1828
1838
|
return startNavigation(historyAction, location);
|
|
1829
1839
|
});
|
|
1840
|
+
if (isBrowser) {
|
|
1841
|
+
// FIXME: This feels gross. How can we cleanup the lines between
|
|
1842
|
+
// scrollRestoration/appliedTransitions persistance?
|
|
1843
|
+
restoreAppliedTransitions(routerWindow, appliedViewTransitions);
|
|
1844
|
+
let _saveAppliedTransitions = () => persistAppliedTransitions(routerWindow, appliedViewTransitions);
|
|
1845
|
+
routerWindow.addEventListener("pagehide", _saveAppliedTransitions);
|
|
1846
|
+
removePageHideEventListener = () => routerWindow.removeEventListener("pagehide", _saveAppliedTransitions);
|
|
1847
|
+
}
|
|
1830
1848
|
|
|
1831
1849
|
// Kick off initial data load if needed. Use Pop to avoid modifying history
|
|
1832
1850
|
// Note we don't do any handling of lazy here. For SPA's it'll get handled
|
|
@@ -1844,6 +1862,9 @@ function createRouter(init) {
|
|
|
1844
1862
|
if (unlistenHistory) {
|
|
1845
1863
|
unlistenHistory();
|
|
1846
1864
|
}
|
|
1865
|
+
if (removePageHideEventListener) {
|
|
1866
|
+
removePageHideEventListener();
|
|
1867
|
+
}
|
|
1847
1868
|
subscribers.clear();
|
|
1848
1869
|
pendingNavigationController && pendingNavigationController.abort();
|
|
1849
1870
|
state.fetchers.forEach((_, key) => deleteFetcher(key));
|
|
@@ -1857,9 +1878,11 @@ function createRouter(init) {
|
|
|
1857
1878
|
}
|
|
1858
1879
|
|
|
1859
1880
|
// Update our state and notify the calling context of the change
|
|
1860
|
-
function updateState(newState) {
|
|
1881
|
+
function updateState(newState, viewTransitionOpts) {
|
|
1861
1882
|
state = _extends({}, state, newState);
|
|
1862
|
-
subscribers.forEach(subscriber => subscriber(state
|
|
1883
|
+
subscribers.forEach(subscriber => subscriber(state, {
|
|
1884
|
+
unstable_viewTransitionOpts: viewTransitionOpts
|
|
1885
|
+
}));
|
|
1863
1886
|
}
|
|
1864
1887
|
|
|
1865
1888
|
// Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
|
|
@@ -1914,6 +1937,39 @@ function createRouter(init) {
|
|
|
1914
1937
|
} else if (pendingAction === Action.Replace) {
|
|
1915
1938
|
init.history.replace(location, location.state);
|
|
1916
1939
|
}
|
|
1940
|
+
let viewTransitionOpts;
|
|
1941
|
+
|
|
1942
|
+
// On POP, enable transitions if they were enabled on the original navigation
|
|
1943
|
+
if (pendingAction === Action.Pop) {
|
|
1944
|
+
// Forward takes precedence so they behave like the original navigation
|
|
1945
|
+
let priorPaths = appliedViewTransitions.get(state.location.pathname);
|
|
1946
|
+
if (priorPaths && priorPaths.has(location.pathname)) {
|
|
1947
|
+
viewTransitionOpts = {
|
|
1948
|
+
currentLocation: state.location,
|
|
1949
|
+
nextLocation: location
|
|
1950
|
+
};
|
|
1951
|
+
} else if (appliedViewTransitions.has(location.pathname)) {
|
|
1952
|
+
// If we don't have a previous forward nav, assume we're popping back to
|
|
1953
|
+
// the new location and enable if that location previously enabled
|
|
1954
|
+
viewTransitionOpts = {
|
|
1955
|
+
currentLocation: location,
|
|
1956
|
+
nextLocation: state.location
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
} else if (pendingViewTransitionEnabled) {
|
|
1960
|
+
// Store the applied transition on PUSH/REPLACE
|
|
1961
|
+
let toPaths = appliedViewTransitions.get(state.location.pathname);
|
|
1962
|
+
if (toPaths) {
|
|
1963
|
+
toPaths.add(location.pathname);
|
|
1964
|
+
} else {
|
|
1965
|
+
toPaths = new Set([location.pathname]);
|
|
1966
|
+
appliedViewTransitions.set(state.location.pathname, toPaths);
|
|
1967
|
+
}
|
|
1968
|
+
viewTransitionOpts = {
|
|
1969
|
+
currentLocation: state.location,
|
|
1970
|
+
nextLocation: location
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1917
1973
|
updateState(_extends({}, newState, {
|
|
1918
1974
|
// matches, errors, fetchers go through as-is
|
|
1919
1975
|
actionData,
|
|
@@ -1926,11 +1982,12 @@ function createRouter(init) {
|
|
|
1926
1982
|
restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
|
|
1927
1983
|
preventScrollReset,
|
|
1928
1984
|
blockers
|
|
1929
|
-
}));
|
|
1985
|
+
}), viewTransitionOpts);
|
|
1930
1986
|
|
|
1931
1987
|
// Reset stateful navigation vars
|
|
1932
1988
|
pendingAction = Action.Pop;
|
|
1933
1989
|
pendingPreventScrollReset = false;
|
|
1990
|
+
pendingViewTransitionEnabled = false;
|
|
1934
1991
|
isUninterruptedRevalidation = false;
|
|
1935
1992
|
isRevalidationRequired = false;
|
|
1936
1993
|
cancelledDeferredRoutes = [];
|
|
@@ -2007,7 +2064,8 @@ function createRouter(init) {
|
|
|
2007
2064
|
// render at the right error boundary after we match routes
|
|
2008
2065
|
pendingError: error,
|
|
2009
2066
|
preventScrollReset,
|
|
2010
|
-
replace: opts && opts.replace
|
|
2067
|
+
replace: opts && opts.replace,
|
|
2068
|
+
enableViewTransition: opts && opts.unstable_viewTransition
|
|
2011
2069
|
});
|
|
2012
2070
|
}
|
|
2013
2071
|
|
|
@@ -2060,6 +2118,7 @@ function createRouter(init) {
|
|
|
2060
2118
|
// and track whether we should reset scroll on completion
|
|
2061
2119
|
saveScrollPosition(state.location, state.matches);
|
|
2062
2120
|
pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
|
|
2121
|
+
pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
|
|
2063
2122
|
let routesToUse = inFlightDataRoutes || dataRoutes;
|
|
2064
2123
|
let loadingNavigation = opts && opts.overrideNavigation;
|
|
2065
2124
|
let matches = matchRoutes(routesToUse, location, basename);
|
|
@@ -3011,6 +3070,9 @@ function createRouter(init) {
|
|
|
3011
3070
|
get routes() {
|
|
3012
3071
|
return dataRoutes;
|
|
3013
3072
|
},
|
|
3073
|
+
get window() {
|
|
3074
|
+
return routerWindow;
|
|
3075
|
+
},
|
|
3014
3076
|
initialize,
|
|
3015
3077
|
subscribe,
|
|
3016
3078
|
enableScrollRestoration,
|
|
@@ -4207,7 +4269,7 @@ function findNearestBoundary(matches, routeId) {
|
|
|
4207
4269
|
}
|
|
4208
4270
|
function getShortCircuitMatches(routes) {
|
|
4209
4271
|
// Prefer a root layout route if present, otherwise shim in a route object
|
|
4210
|
-
let route = routes.find(r => r.index || !r.path || r.path === "/") || {
|
|
4272
|
+
let route = routes.length === 1 ? routes[0] : routes.find(r => r.index || !r.path || r.path === "/") || {
|
|
4211
4273
|
id: "__shim-error-route__"
|
|
4212
4274
|
};
|
|
4213
4275
|
return {
|
|
@@ -4526,6 +4588,35 @@ function getDoneFetcher(data) {
|
|
|
4526
4588
|
};
|
|
4527
4589
|
return fetcher;
|
|
4528
4590
|
}
|
|
4591
|
+
function restoreAppliedTransitions(_window, transitions) {
|
|
4592
|
+
try {
|
|
4593
|
+
let sessionPositions = _window.sessionStorage.getItem(TRANSITIONS_STORAGE_KEY);
|
|
4594
|
+
if (sessionPositions) {
|
|
4595
|
+
let json = JSON.parse(sessionPositions);
|
|
4596
|
+
for (let [k, v] of Object.entries(json || {})) {
|
|
4597
|
+
if (v && Array.isArray(v)) {
|
|
4598
|
+
transitions.set(k, new Set(v || []));
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
} catch (e) {
|
|
4603
|
+
// no-op, use default empty object
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
function persistAppliedTransitions(_window, transitions) {
|
|
4607
|
+
if (transitions.size > 0) {
|
|
4608
|
+
let json = {};
|
|
4609
|
+
for (let [k, v] of transitions) {
|
|
4610
|
+
json[k] = [...v];
|
|
4611
|
+
}
|
|
4612
|
+
try {
|
|
4613
|
+
_window.sessionStorage.setItem(TRANSITIONS_STORAGE_KEY, JSON.stringify(json));
|
|
4614
|
+
} catch (error) {
|
|
4615
|
+
warning(false, "Failed to save applied view transitions in sessionStorage (" + error + ").");
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4529
4620
|
//#endregion
|
|
4530
4621
|
|
|
4531
4622
|
exports.AbortedDeferredError = AbortedDeferredError;
|