fix: page transition UI jitter — remove enter phase to prevent double animation
The three-phase exit→enter→idle flow caused a visible "double refresh" jitter. During the enter phase (220ms), the wrapper animated from opacity:0 while cancelling child .page-motion with animation:none !important. When phase switched to idle, the !important rule was removed and child .page-motion re-triggered, creating a second entrance animation — the jitter. Fix: remove the enter phase entirely. After exit animation (180ms), phase goes directly to idle. The child page's own .page-motion class handles entrance naturally via React's fresh DOM mount. No wrapper animation on enter, no double-animation conflict. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,6 @@ interface PageTransitionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EXIT_DURATION_MS = 180;
|
const EXIT_DURATION_MS = 180;
|
||||||
const ENTER_DURATION_MS = 220;
|
|
||||||
|
|
||||||
const NAV_ORDER: string[] = [
|
const NAV_ORDER: string[] = [
|
||||||
"home",
|
"home",
|
||||||
@@ -40,7 +39,7 @@ function getNavIndex(key: string): number {
|
|||||||
|
|
||||||
export default function PageTransition({ viewKey, children }: PageTransitionProps) {
|
export default function PageTransition({ viewKey, children }: PageTransitionProps) {
|
||||||
const [displayedChildren, setDisplayedChildren] = useState(children);
|
const [displayedChildren, setDisplayedChildren] = useState(children);
|
||||||
const [phase, setPhase] = useState<"idle" | "exit" | "enter">("idle");
|
const [phase, setPhase] = useState<"idle" | "exit">("idle");
|
||||||
const [exitDirection, setExitDirection] = useState<"forward" | "backward" | "neutral">("neutral");
|
const [exitDirection, setExitDirection] = useState<"forward" | "backward" | "neutral">("neutral");
|
||||||
const prevKeyRef = useRef(viewKey);
|
const prevKeyRef = useRef(viewKey);
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout>>();
|
const timerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
@@ -73,38 +72,15 @@ export default function PageTransition({ viewKey, children }: PageTransitionProp
|
|||||||
setPhase("exit");
|
setPhase("exit");
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
setDisplayedChildren(children);
|
setDisplayedChildren(children);
|
||||||
setPhase("enter");
|
setPhase("idle");
|
||||||
}, EXIT_DURATION_MS);
|
}, EXIT_DURATION_MS);
|
||||||
return () => clearTimeout(timerRef.current);
|
return () => clearTimeout(timerRef.current);
|
||||||
}, [viewKey, children]);
|
}, [viewKey, children]);
|
||||||
|
|
||||||
// After enter animation completes, go back to idle
|
|
||||||
useEffect(() => {
|
|
||||||
if (phase !== "enter") return;
|
|
||||||
const timer = setTimeout(() => setPhase("idle"), ENTER_DURATION_MS);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [phase]);
|
|
||||||
|
|
||||||
const dirClass = exitDirection === "forward" ? " is-forward" : exitDirection === "backward" ? " is-backward" : "";
|
const dirClass = exitDirection === "forward" ? " is-forward" : exitDirection === "backward" ? " is-backward" : "";
|
||||||
|
|
||||||
if (phase === "exit") {
|
|
||||||
return (
|
|
||||||
<div className={`page-transition-wrap page-motion--exit${dirClass}`}>
|
|
||||||
{displayedChildren}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (phase === "enter") {
|
|
||||||
return (
|
|
||||||
<div className="page-transition-wrap page-motion--enter">
|
|
||||||
{displayedChildren}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-transition-wrap">
|
<div className={phase === "exit" ? `page-transition-wrap page-motion--exit${dirClass}` : "page-transition-wrap"}>
|
||||||
{displayedChildren}
|
{displayedChildren}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,23 +54,3 @@
|
|||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enter: explicit wrapper entrance animation overrides child page-motion */
|
|
||||||
.page-motion--enter {
|
|
||||||
animation: page-enter-fade 220ms ease both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-motion--enter .page-motion {
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes page-enter-fade {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(6px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user