Skip to content
Source
Sign inSign up
Open History to see the last commit

useDraftHeroLayoutTransition.ts

70 lines2.8 KB

1import { useStableCallback } from "../../hooks/useStableCallback";
2import { useLayoutEffect, useRef } from "react";
3import {
4 DRAFT_HERO_TRANSITION_ANIMATION_ID,
5 DRAFT_HERO_TRANSITION_EASING,
6} from "./draftHeroTransition";
7​
8export function useDraftHeroLayoutTransition(
9 isDraftHeroState: boolean,
10 animationsActive: boolean,
11 animationDurationMs: number,
12) {
13 const transitionGroupRef = useRef<HTMLDivElement | null>(null);
14 const composerAnchorRef = useRef<HTMLDivElement | null>(null);
15 const previousStateRef = useRef(isDraftHeroState);
16 const previousComposerRectRef = useRef<DOMRect | null>(null);
17 const animationRef = useRef<Animation | null>(null);
18 const captureComposerRect = useStableCallback(() => {
19 previousComposerRectRef.current = composerAnchorRef.current?.getBoundingClientRect() ?? null;
20 });
21​
22 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- measures the committed composer rect and starts a FLIP Web Animation before paint
23 useLayoutEffect(() => {
24 const transitionGroup = transitionGroupRef.current;
25 const nextComposerRect = composerAnchorRef.current?.getBoundingClientRect() ?? null;
26 const stateChanged = previousStateRef.current !== isDraftHeroState;
27 const mobileComposerTransitionActive =
28 typeof document !== "undefined" &&
29 document.documentElement.dataset.mobileComposerRouteTransition === "true";
30​
31 animationRef.current?.cancel();
32 animationRef.current = null;
33 const previousComposerRect = previousComposerRectRef.current;
34 if (
35 stateChanged &&
36 animationsActive &&
37 !mobileComposerTransitionActive &&
38 transitionGroup &&
39 previousComposerRect &&
40 nextComposerRect &&
41 typeof transitionGroup.animate === "function"
42 ) {
43 const translateX = previousComposerRect.left - nextComposerRect.left;
44 const translateY = previousComposerRect.top - nextComposerRect.top;
45 if (Math.abs(translateX) >= 0.5 || Math.abs(translateY) >= 0.5) {
46 const animation = transitionGroup.animate(
47 [
48 { transform: `translate3d(${translateX}px, ${translateY}px, 0)` },
49 { transform: "translate3d(0, 0, 0)" },
50 ],
51 {
52 duration: animationDurationMs,
53 easing: DRAFT_HERO_TRANSITION_EASING,
54 },
55 );
56 animation.id = DRAFT_HERO_TRANSITION_ANIMATION_ID;
57 animationRef.current = animation;
58 void animation.finished
59 .catch(() => undefined)
60 .then(() => {
61 if (animationRef.current === animation) animationRef.current = null;
62 });
63 }
64 }
65 previousStateRef.current = isDraftHeroState;
66 previousComposerRectRef.current = nextComposerRect;
67 }, [animationDurationMs, animationsActive, isDraftHeroState]);
68​
69 return { transitionGroupRef, composerAnchorRef, captureComposerRect } as const;