Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add startViewTransition support #10916

Merged
merged 43 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0d5dd29
POC support for startViewTransition
brophdawg11 Mar 9, 2023
069b77f
Add example using startViwTransition
brophdawg11 Jul 14, 2023
9d6f330
Update dep
brophdawg11 Aug 22, 2023
cbd6aad
Update approach and add future flags
brophdawg11 Aug 29, 2023
b774db3
Rename future flag
brophdawg11 Aug 29, 2023
dbac4b1
Updates
brophdawg11 Aug 30, 2023
560a894
Switch to useViewTransition hook
brophdawg11 Sep 19, 2023
d4087e9
Add more updates - proxy through Link
brophdawg11 Sep 20, 2023
c383e8f
Add unstable prefix
brophdawg11 Sep 20, 2023
48303d0
Revert "Add unstable prefix"
brophdawg11 Sep 20, 2023
449d51e
Remove viewTransition API from router.navigate
brophdawg11 Sep 20, 2023
c1dee37
Rename useViewTransitions
brophdawg11 Sep 20, 2023
f516ded
Allow return false
brophdawg11 Sep 20, 2023
f800892
POC
brophdawg11 Sep 22, 2023
adf0b78
Get POP navigations working
brophdawg11 Sep 25, 2023
d8bafcd
Lift viewTransition function execution up to router
brophdawg11 Sep 25, 2023
18cc33b
Handle forward pops
brophdawg11 Sep 25, 2023
27f65c1
Updates
brophdawg11 Sep 26, 2023
9fcf8df
Update approch to use prop and hook together
brophdawg11 Sep 28, 2023
7d6a724
Remove currentLocation from ViewTransitionContext
brophdawg11 Sep 28, 2023
5768b76
Fix image freeze on detail->home
brophdawg11 Sep 28, 2023
e43453e
Remove defer value param
brophdawg11 Sep 28, 2023
f456698
Remove usage of fallbackOnDeferRevalidation
brophdawg11 Sep 28, 2023
2359232
Remove fallbackOnDeferRevalidation flag
brophdawg11 Sep 28, 2023
360206f
Remove debug logs
brophdawg11 Sep 28, 2023
15961e1
Clean ups
brophdawg11 Sep 28, 2023
2f716d3
Revert "Remove debug logs"
brophdawg11 Sep 28, 2023
dda9693
Add interruption handling
brophdawg11 Sep 29, 2023
7263d4a
Remove debug logs
brophdawg11 Sep 29, 2023
098efb7
Remove unused flushSync
brophdawg11 Sep 29, 2023
b9f0c8a
Persist applied transitions to sessionStorage
brophdawg11 Sep 29, 2023
3912731
Add docs
brophdawg11 Oct 2, 2023
4a8a492
Handle basename and PUSH navs that reverse a transition
brophdawg11 Oct 4, 2023
7383054
Udpate docs with example
brophdawg11 Oct 4, 2023
d5e1a89
Udpate docs with example
brophdawg11 Oct 4, 2023
35c4680
Add changeset
brophdawg11 Oct 4, 2023
1e8dcaa
Only leverage useViewTransitionState in DataRouter NavLink usages
brophdawg11 Oct 5, 2023
6ddc868
Fix tests
brophdawg11 Oct 11, 2023
04857aa
Unit tests
brophdawg11 Oct 11, 2023
e687cac
Bump bundle
brophdawg11 Oct 11, 2023
34ace81
Bump bundle
brophdawg11 Oct 11, 2023
54298be
Update changeset
brophdawg11 Oct 11, 2023
f3db06f
Rename viewTransitionOpts -> unstable_viewTransitionOpts
brophdawg11 Oct 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Lift viewTransition function execution up to router
  • Loading branch information
brophdawg11 committed Sep 28, 2023
commit d8bafcdcf3d32577204c8d6cd9c18a30166947eb
12 changes: 8 additions & 4 deletions examples/view-transitions/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
contain: layout;
}

.image-list a.transitioning img {
view-transition-name: image-expand;
}

.image-detail img {
max-width: 100%;
view-transition-name: image-expand;
Expand All @@ -73,11 +77,11 @@
width: fit-content;
}

.image-detail h1 {
.image-list a.transitioning p {
view-transition-name: image-title;
width: fit-content;
}

::view-transition-old(image-title),
::view-transition-new(image-title) {
.image-detail h1 {
view-transition-name: image-title;
width: fit-content;
}
119 changes: 3 additions & 116 deletions examples/view-transitions/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
Outlet,
RouterProvider,
unstable_useViewTransition,
unstable_useViewTransitions,
useLoaderData,
useNavigation,
useParams,
Expand Down Expand Up @@ -143,40 +142,6 @@ const router = createBrowserRouter(
{
path: "images",
Component() {
// Animate list image into detail image
// unstable_useViewTransitions(({ nextLocation }) => {
// // TODO: Should we be able to return false here to opt out of
// // calling startViewTransition?
// if (!/^\/images\/\d+$/.test(nextLocation.pathname)) {
// return false;
// }

// LOG("before dom update");
// let anchor = document.querySelector(
// `.image-list a[href="${nextLocation.pathname}"]`
// );
// let title = anchor?.previousElementSibling!;
// let img = anchor?.firstElementChild!;
// // @ts-ignore
// title.style.viewTransitionName = "image-title";
// // @ts-ignore
// img.style.viewTransitionName = "image-expand";

// return (transition) => {
// LOG("after dom update", transition);
// transition.ready.finally(() => {
// LOG("transition ready");
// });
// transition.finished.finally(() => {
// LOG("transition finished");
// // @ts-ignore
// title.style.viewTransitionName = "";
// // @ts-ignore
// img.style.viewTransitionName = "";
// });
// };
// });

return (
<div className="image-list">
<h1>Image List</h1>
Expand All @@ -187,28 +152,8 @@ const router = createBrowserRouter(
to={`/images/${idx}`}
unstable_viewTransition
>
{({ isTransitioning }) => (
<>
<p
style={{
viewTransitionName: isTransitioning
? "image-title"
: "",
}}
>
Image Number {idx}
</p>
<img
src={src}
alt={`Img ${idx}`}
style={{
viewTransitionName: isTransitioning
? "image-expand"
: "",
}}
/>
</>
)}
<p>Image Number {idx}</p>
<img src={src} alt={`Img ${idx}`} />
</NavLink>
))}
</div>
Expand All @@ -220,41 +165,6 @@ const router = createBrowserRouter(
path: "images/:id",
Component() {
let params = useParams();

// // Animate detail image into list image
// unstable_useViewTransitions(({ currentLocation, nextLocation }) => {
// // TODO: Should we be able to return false here to opt out of
// // calling startViewTransition?
// if (!/^\/images$/.test(nextLocation.pathname)) {
// return false;
// }

// LOG("before dom update");

// return (transition) => {
// LOG("after dom update", transition);
// let anchor = document.querySelector(
// `.image-list a[href="${currentLocation.pathname}"]`
// );
// let title = anchor?.previousElementSibling!;
// let img = anchor?.firstElementChild!;
// // @ts-ignore
// title.style.viewTransitionName = "image-title";
// // @ts-ignore
// img.style.viewTransitionName = "image-expand";
// transition.ready.finally(() => {
// LOG("transition ready");
// });
// transition.finished.finally(() => {
// LOG("transition finished");
// // @ts-ignore
// title.style.viewTransitionName = "";
// // @ts-ignore
// img.style.viewTransitionName = "";
// });
// };
// });

return (
<div className="image-detail">
<h1>Image Number {params.id}</h1>
Expand All @@ -270,34 +180,11 @@ const router = createBrowserRouter(
future: {
// Prevent react router from await-ing defer() promises for revalidating
// loaders, which includes changing search params on the active route
v7_fallbackOnDeferRevalidation: true,
unstable_fallbackOnDeferRevalidation: true,
},
}
);

function NavImage({ src, idx }: { src: string; idx: number }) {
let href = `/images/${idx}`;
let vt = unstable_useViewTransition(href);
return (
<div>
<p
style={{ viewTransitionName: vt.isTransitioning ? "image-title" : "" }}
>
Image Number {idx}
</p>
<Link to={href}>
<img
src={src}
alt={`Img ${idx}`}
style={{
viewTransitionName: vt.isTransitioning ? "image-expand" : "",
}}
/>
</Link>
</div>
);
}

const rootElement = document.getElementById("root") as HTMLElement;
ReactDOMClient.createRoot(rootElement).render(
<React.StrictMode>
Expand Down
2 changes: 0 additions & 2 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1532,8 +1532,6 @@ function useViewTransitions(
}, [router, viewTransition]);
}

export { useViewTransitions as unstable_useViewTransitions };

/**
* Localized version of useViewTransitions to enable view transitions for a
* specific destination href. Returns an isTransitioning value that you can
Expand Down
108 changes: 44 additions & 64 deletions packages/react-router/lib/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ export interface RouterProviderProps {
*/
const START_TRANSITION = "startTransition";
const startTransitionImpl = React[START_TRANSITION];
const FLUSH_SYNC = "flushSync";
const flushSyncImpl = ReactDOM[FLUSH_SYNC];

function flushSyncSafe(cb: () => void) {
if (flushSyncImpl) {
flushSyncImpl(cb);
} else {
cb();
}
}

function startTransitionSafe(cb: () => void) {
if (startTransitionImpl) {
startTransitionImpl(cb);
} else {
cb();
}
}

// FIXME: Remove
function LOG(...args: any[]) {
Expand All @@ -119,95 +137,57 @@ export function RouterProvider({
});
let { v7_startTransition } = future || {};
let setState = React.useCallback<RouterSubscriber>(
(newState: RouterState, { completedNavigation }) => {
function maybeStartTransition(cb: () => void) {
if (v7_startTransition && startTransitionImpl) {
startTransitionImpl(cb);
} else {
cb();
}
}

(newState: RouterState, { viewTransitionOpts }) => {
// Simple update for non-completeNavigation/non-startViewTransition navs
if (
!completedNavigation ||
completedNavigation.viewTransitions == null ||
typeof document.startViewTransition !== "function" ||
completedNavigation.viewTransitions.length === 0 ||
completedNavigation.viewTransitions.every((vt) => vt === false)
!viewTransitionOpts ||
typeof document.startViewTransition !== "function"
) {
LOG("no transitions to apply");
maybeStartTransition(() => setStateImpl(newState));
return;
}

// TODO: Can the execution of these lift up into the router? We should
// have better control over which ones to execute there I think and can
// hopefully do better POP-key/location matching

// Run user-provided functions, short circuiting on false
LOG(
"calling pre-dom-update functions. Num:",
completedNavigation.viewTransitions.length
);
let callbacks = completedNavigation.viewTransitions.map((vt) =>
typeof vt === "function"
? vt({
historyAction: newState.historyAction,
currentLocation: completedNavigation?.prevLocation,
nextLocation: newState.location,
})
: vt
);

if (callbacks.every((cb) => cb === false)) {
LOG("user opted out of startViewTransition with returned false");
maybeStartTransition(() => setStateImpl(newState));
LOG("updating state without startViewTransition");
if (v7_startTransition) {
startTransitionSafe(() => setStateImpl(newState));
} else {
setStateImpl(newState);
}
return;
}

// FIXME: Make this defensive for React 17!
// Flush a ViewTransitionContext update to apply classes/styles to the
// DOM prior to startViewTransition()
LOG("flushSync(() => setVtContext({ isTransitioning: true }))");
ReactDOM.flushSync(() => {
flushSyncSafe(() => {
setVtContext({
isTransitioning: true,
historyAction: newState.historyAction,
currentLocation: completedNavigation.prevLocation,
currentLocation: viewTransitionOpts.prevLocation,
nextLocation: newState.location,
});
});

// Synchronously update the DOM for the new route inside startViewTransition()
// Note: startViewTransition/startTransition/flushSync don't
// play nicely together so we don't call startTransition when view
// transitions are enabled. document.startViewTransition needs
// React.flushSync for more advanced animations, and React.flushSync
// breaks React.startTransition "freezing" behavior if the destination
// route suspends without a boundary.
// TODO: We should add something to the docs to indicate this
LOG("calling document.startViewTransition()");
let transition = document.startViewTransition(() => {
// Note: startViewTransition/startTransition/flushSync don't
// play nicely together so we don't call startTransition when view
// transitions are enabled.
// - document.startViewTransition needs React.flushSync for more advanced animations)
// - React.flushSync breaks React.startTransition "freezing" behavior if the
// destination route suspends without a boundary
//
// TODO: We should add something to the docs to indicate this

// Synchronously update the DOM with the new route
// FIXME: Make this defensive for React 17!
ReactDOM.flushSync(() => {
LOG("calling setState()");
setStateImpl(newState);
});
// Update DOM
LOG("flushSync(() => setStateImpl(newState))");
flushSyncSafe(() => setStateImpl(newState));

// Run the post-DOM-update callbacks with the transition
callbacks.forEach(
viewTransitionOpts.callbacks.forEach(
(cb) => typeof cb === "function" && cb && cb(transition)
);

// Cleanup DOM after the transitions completes
LOG("waiting on transition.finished to unset isTransitioning");
transition.finished.finally(() => {
// FIXME: Make this defensive for React 17!
LOG("flushSync(() => setVtContext({ isTransitioning: false }))");
ReactDOM.flushSync(() => {
setVtContext({ isTransitioning: false });
});
flushSyncSafe(() => setVtContext({ isTransitioning: false }));
});
});
},
Expand Down
Loading