Skip to content

Commit 5fd71fb

Browse files
authored
Reply animation refactor (codesandbox#3813)
* massive imperative refactor * its done, dont ask me * ugh -1 not 0
1 parent 69be02a commit 5fd71fb

File tree

1 file changed

+149
-31
lines changed
  • packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog

1 file changed

+149
-31
lines changed

packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/index.tsx

Lines changed: 149 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const CommentDialog = props =>
2828

2929
const DIALOG_WIDTH = 420;
3030
const DIALOG_TRANSITION_DURATION = 0.25;
31-
const REPLY_TRANSITION_DELAY = 0.25;
31+
const REPLY_TRANSITION_DELAY = 0.5;
3232

3333
export const Dialog: React.FC = () => {
3434
const { state } = useOvermind();
@@ -135,12 +135,11 @@ export const Dialog: React.FC = () => {
135135
setEditing={setEditing}
136136
hasReplies={replies.length}
137137
/>
138-
{replies.length ? (
139-
<Replies
140-
replies={replies}
141-
repliesRenderedCallback={() => setRepliesRendered(true)}
142-
/>
143-
) : null}
138+
139+
<Replies
140+
replies={replies}
141+
repliesRenderedCallback={() => setRepliesRendered(true)}
142+
/>
144143
</Element>
145144
<AddReply
146145
comment={comment}
@@ -365,43 +364,162 @@ const CommentBody = ({ comment, editing, setEditing, hasReplies }) => {
365364
};
366365

367366
const Replies = ({ replies, repliesRenderedCallback }) => {
368-
const [isAnimating, setAnimating] = React.useState(true);
369-
const repliesLoaded = !!replies[0];
367+
/**
368+
* Loading animations:
369+
* 0. Wait for the dialog to have animated in view and scaled up.
370+
* 1. If replies have not loaded yet - show skeleton with 146px height,
371+
* when the comments load, replace skeleton with replies and
372+
* transition to height auto
373+
* 2. If replies are already there - show replies with 0px height,
374+
* transition to height: auto
375+
*
376+
*/
377+
378+
const skeletonController = useAnimation();
379+
const repliesController = useAnimation();
370380

371381
/** Wait another <delay>ms after the dialog has transitioned into view */
372382
const delay = DIALOG_TRANSITION_DURATION + REPLY_TRANSITION_DELAY;
373-
const REPLY_TRANSITION_DURATION = 0.25;
383+
const REPLY_TRANSITION_DURATION = Math.max(replies.length * 0.15, 0.5);
384+
const SKELETON_FADE_DURATION = 0.25;
374385
const SKELETON_HEIGHT = 146;
375386

387+
// initial status of replies -
388+
// this is false when it's the first time this specific comment is opened
389+
// after that it will be true because we cache replies in state
390+
const repliesAlreadyLoadedOnFirstRender = React.useRef(!!replies[0]);
391+
392+
// current status of replies-
393+
const repliesLoaded = !!replies[0];
394+
395+
/** Welcome to the imperative world of timeline animations
396+
*
397+
* -------------------------------------------------------
398+
* | | | | |
399+
* t=0 t=R1 t=1 t=R2 t=2
400+
*
401+
* Legend:
402+
* t=0 DOM has rendered, animations can be started
403+
* t=1 Dialog's enter animation has completed, replies animations can start
404+
* t=2 Replies animation have started
405+
* t=R1 Replies have loaded before t=1
406+
* t=R2 Replies have loaded after t=1
407+
*
408+
*/
409+
410+
const [T, setStepInTimeline] = React.useState(-1);
411+
412+
/*
413+
* T = 0 (DOM has rendered, animations can be started)
414+
* If there are no replies, skip all of the animations
415+
* If replies aren't loaded, show skeleton
416+
* If replies are loaded, do nothing and wait for next animation
417+
* */
418+
React.useEffect(() => {
419+
if (!replies.length) {
420+
// If the dialog is already open without any replies,
421+
// just skip all of the animations for opening transitions
422+
repliesController.set({ opacity: 1, height: 'auto' });
423+
setStepInTimeline(2);
424+
} else if (!repliesAlreadyLoadedOnFirstRender.current && T === -1) {
425+
skeletonController.set({ height: SKELETON_HEIGHT, opacity: 1 });
426+
setStepInTimeline(0);
427+
}
428+
}, [skeletonController, repliesController, replies.length, T]);
429+
430+
/**
431+
* T = 1 (Dialog's enter animation has completed, hence the delay)
432+
* If replies have loaded, remove skeleton, transition the replies
433+
* If replies have not loaded, do nothing and wait for next animation
434+
*/
435+
React.useEffect(() => {
436+
const timeout = window.setTimeout(() => {
437+
if (T >= 1) return; // can't go back in time
438+
439+
if (repliesLoaded) {
440+
skeletonController.set({ position: 'absolute' });
441+
skeletonController.start({
442+
height: 0,
443+
opacity: 0,
444+
transition: { duration: SKELETON_FADE_DURATION },
445+
});
446+
repliesController.set({ opacity: 1 });
447+
repliesController.start({
448+
height: 'auto',
449+
transition: { duration: REPLY_TRANSITION_DURATION },
450+
});
451+
452+
setStepInTimeline(2);
453+
} else {
454+
setStepInTimeline(1);
455+
}
456+
}, delay * 1000);
457+
return () => window.clearTimeout(timeout);
458+
}, [
459+
skeletonController,
460+
repliesController,
461+
repliesLoaded,
462+
delay,
463+
REPLY_TRANSITION_DURATION,
464+
T,
465+
]);
466+
467+
/**
468+
* T = R1 or R2 (Replies have now loaded)
469+
* this is a parralel async process and can happen before or after t=1
470+
* If it's before T=1, do nothing, wait for T=1
471+
* If it's after T=1, start replies transition now!
472+
*/
376473
React.useEffect(() => {
377-
if (repliesLoaded && !isAnimating) repliesRenderedCallback();
378-
}, [repliesLoaded, isAnimating, repliesRenderedCallback]);
474+
if (!repliesLoaded) {
475+
// do nothing, wait for T=1
476+
} else if (T === 1) {
477+
skeletonController.start({
478+
height: 0,
479+
opacity: 0,
480+
transition: { duration: REPLY_TRANSITION_DURATION },
481+
});
482+
repliesController.set({ opacity: 1 });
483+
repliesController.start({
484+
height: 'auto',
485+
transition: { duration: REPLY_TRANSITION_DURATION },
486+
});
487+
setStepInTimeline(2);
488+
}
489+
}, [
490+
T,
491+
repliesLoaded,
492+
REPLY_TRANSITION_DURATION,
493+
skeletonController,
494+
repliesController,
495+
]);
379496

380497
return (
381-
<motion.ul
382-
initial={{ height: repliesLoaded ? 0 : SKELETON_HEIGHT }}
383-
animate={{ height: 'auto' }}
384-
transition={{
385-
delay,
386-
duration: REPLY_TRANSITION_DURATION,
387-
}}
388-
style={{
389-
minHeight: repliesLoaded ? 0 : SKELETON_HEIGHT,
390-
overflow: 'visible',
391-
paddingLeft: 0,
392-
}}
393-
onAnimationComplete={() => setAnimating(false)}
394-
>
395-
{repliesLoaded ? (
498+
<>
499+
<motion.div
500+
initial={{ height: 0, opacity: 0, overflow: 'hidden' }}
501+
animate={skeletonController}
502+
>
503+
<SkeletonReply />
504+
</motion.div>
505+
506+
<motion.ul
507+
initial={{ height: 0, opacity: 0 }}
508+
animate={repliesController}
509+
style={{
510+
overflow: 'visible',
511+
paddingLeft: 0,
512+
margin: 0,
513+
listStyle: 'none',
514+
}}
515+
>
396516
<>
397517
{replies.map(
398518
reply => reply && <Reply reply={reply} key={reply.id} />
399519
)}
400520
</>
401-
) : (
402-
<SkeletonReply />
403-
)}
404-
</motion.ul>
521+
</motion.ul>
522+
</>
405523
);
406524
};
407525

0 commit comments

Comments
 (0)