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:
2026-06-02 22:19:14 +08:00
parent ec9204437d
commit e555209516
2 changed files with 3 additions and 47 deletions
+3 -27
View File
@@ -6,7 +6,6 @@ interface PageTransitionProps {
}
const EXIT_DURATION_MS = 180;
const ENTER_DURATION_MS = 220;
const NAV_ORDER: string[] = [
"home",
@@ -40,7 +39,7 @@ function getNavIndex(key: string): number {
export default function PageTransition({ viewKey, children }: PageTransitionProps) {
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 prevKeyRef = useRef(viewKey);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
@@ -73,38 +72,15 @@ export default function PageTransition({ viewKey, children }: PageTransitionProp
setPhase("exit");
timerRef.current = setTimeout(() => {
setDisplayedChildren(children);
setPhase("enter");
setPhase("idle");
}, EXIT_DURATION_MS);
return () => clearTimeout(timerRef.current);
}, [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" : "";
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 (
<div className="page-transition-wrap">
<div className={phase === "exit" ? `page-transition-wrap page-motion--exit${dirClass}` : "page-transition-wrap"}>
{displayedChildren}
</div>
);