-
-
Notifications
You must be signed in to change notification settings - Fork 868
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Track appear effect handover per element due to Suspense (#2810)
* Remove `scheduleHandoffComplete` due to Suspense With nested Suspense boundaries, it might be that any children mount at a later timing than `queueMicrotask` guarantees. While we could switch to another timing-based method, it doesn't reliably ensure our call happens later than the mounting of the optimized appear effect. There seems to be no reliable way of telling when those nested boundaries have mounted (after completing) hydration, so removing the functionality seems to be the most sensible way. We remove optimized appear animations from the appearAnimationStore anyway after running, so this was just a minor optimization. The trade-off might be, appear effects re-run when changing breakpoints, but to me that sounds ok, as barely any user ever changes the breakpoint. * store handover state per appear element Id * add test --------- Co-authored-by: Matt Perry <mattgperry@gmail.com>
- Loading branch information
1 parent
25b0e5a
commit 7eb6728
Showing
6 changed files
with
215 additions
and
20 deletions.
There are no files selected for viewing
184 changes: 184 additions & 0 deletions
184
dev/html/public/optimized-appear/defer-handoff-suspense.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
<html> | ||
<head> | ||
<style> | ||
body { | ||
padding: 100px; | ||
margin: 0; | ||
} | ||
|
||
#box1, | ||
#box2 { | ||
width: 100px; | ||
height: 100px; | ||
background-color: #0077ff; | ||
} | ||
|
||
[data-layout-correct="false"] { | ||
background: #dd1144 !important; | ||
opacity: 1 !important; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/imports/optimized-appear.js"></script> | ||
<script type="module" src="/src/imports/script-assert.js"></script> | ||
|
||
<script type="module"> | ||
const { | ||
motion, | ||
animateStyle, | ||
animate, | ||
startOptimizedAppearAnimation, | ||
optimizedAppearDataAttribute, | ||
motionValue, | ||
frame, | ||
} = window.Motion | ||
const { matchViewportBox } = window.Assert | ||
const root = document.getElementById("root") | ||
|
||
const duration = 2 | ||
const x1 = motionValue(0) | ||
const x2 = motionValue(0) | ||
|
||
let suspendPromise = false | ||
const SuspendingComponent = ({ children }) => { | ||
if (suspendPromise === undefined) { | ||
suspendPromise = new Promise((resolve) => { | ||
setTimeout(() => { | ||
suspendPromise = true | ||
resolve() | ||
}, 10) | ||
}) | ||
throw suspendPromise | ||
} | ||
return children | ||
} | ||
|
||
const getDiv = (i, children) => | ||
React.createElement(motion.div, { | ||
id: "box" + i, | ||
initial: { x: 0, opacity: 0 }, | ||
animate: { x: 100, opacity: 1 }, | ||
transition: { duration, ease: "linear" }, | ||
style: { x: i === 1 ? x1 : x2 }, | ||
/** | ||
* On animation start, check the values we expect to see here | ||
*/ | ||
onAnimationStart: () => { | ||
console.log("start", i) | ||
const box = document.getElementById("box" + i) | ||
|
||
box.style.backgroundColor = i === 1 ? "green" : "blue" | ||
|
||
setTimeout(() => { | ||
/** | ||
* This animation interrupts the optimised animation. Notably, we are animating | ||
* x in the optimised transform animation and only scale here. This ensures | ||
* that any transform can force the cancellation of the optimised animation on transform, | ||
* not just those involved in the original animation. | ||
*/ | ||
animate( | ||
box, | ||
{ scale: 2, opacity: 0.1 }, | ||
{ duration: 0.3, ease: "linear" } | ||
).then(() => { | ||
frame.postRender(() => { | ||
const style = getComputedStyle(box) | ||
if (style.opacity !== "0.1") { | ||
showError( | ||
box, | ||
`${i} opacity animation didn't interrupt optimised animation. Opacity was ${style.opacity} instead of 0.1.` | ||
) | ||
} | ||
|
||
const { width, left } = | ||
box.getBoundingClientRect() | ||
// the width scales with the parent, e.g. if the parent scales 2x, the child scales scales 2x on top | ||
if (Math.round(width) !== 200 * i) { | ||
showError( | ||
box, | ||
`${i} scale animation didn't interrupt optimised animation. Width was ${width}px instead of ${ | ||
200 * i | ||
}px.` | ||
) | ||
} | ||
|
||
if (left <= 100) { | ||
showError( | ||
box, | ||
`${i} scale animation incorrectly interrupted optimised animation. Left was ${left}px instead of 100px.` | ||
) | ||
} | ||
}) | ||
}) | ||
}, 100) | ||
}, | ||
[optimizedAppearDataAttribute]: "a" + i, | ||
children: children ?? "Content", | ||
}) | ||
|
||
// This is the tree to be rendered "server" and client-side. | ||
const Component = getDiv( | ||
1, | ||
React.createElement( | ||
React.Suspense, | ||
{}, | ||
React.createElement(SuspendingComponent, {}, getDiv(2)) | ||
) | ||
) | ||
|
||
// Emulate server rendering of element | ||
root.innerHTML = ReactDOMServer.renderToString(Component) | ||
suspendPromise = undefined | ||
|
||
// Start optimised opacity animation | ||
startOptimizedAppearAnimation( | ||
document.getElementById("box1"), | ||
"opacity", | ||
[0, 1], | ||
{ | ||
duration: duration * 1000, | ||
ease: "linear", | ||
} | ||
) | ||
|
||
// Start WAAPI animation | ||
startOptimizedAppearAnimation( | ||
document.getElementById("box1"), | ||
"transform", | ||
["translateX(0px)", "translateX(100px)"], | ||
{ | ||
duration: duration * 1000, | ||
ease: "linear", | ||
}, | ||
(animation) => {} | ||
) | ||
|
||
// Start optimised opacity animation | ||
startOptimizedAppearAnimation( | ||
document.getElementById("box2"), | ||
"opacity", | ||
[0, 1], | ||
{ | ||
duration: duration * 1000, | ||
ease: "linear", | ||
} | ||
) | ||
startOptimizedAppearAnimation( | ||
document.getElementById("box2"), | ||
"transform", | ||
["translateX(0px)", "translateX(100px)"], | ||
{ | ||
duration: duration * 1000, | ||
ease: "linear", | ||
}, | ||
(animation) => { | ||
setTimeout(() => { | ||
ReactDOMClient.hydrateRoot(root, Component) | ||
}, (duration * 1000) / 2) | ||
} | ||
) | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters