fix(transition): preserve scroll position during page animations

This commit is contained in:
LTbinglingfeng
2026-01-02 00:29:42 +08:00
parent 946ed36af0
commit 7ce97a616f
2 changed files with 33 additions and 8 deletions

View File

@@ -1,4 +1,4 @@
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useLocation, type Location } from 'react-router-dom';
import gsap from 'gsap';
import './PageTransition.scss';
@@ -6,6 +6,7 @@ import './PageTransition.scss';
interface PageTransitionProps {
render: (location: Location) => ReactNode;
getRouteOrder?: (pathname: string) => number | null;
scrollContainerRef?: React.RefObject<HTMLElement | null>;
}
const TRANSITION_DURATION = 0.65;
@@ -20,10 +21,15 @@ type Layer = {
type TransitionDirection = 'forward' | 'backward';
export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
export function PageTransition({
render,
getRouteOrder,
scrollContainerRef,
}: PageTransitionProps) {
const location = useLocation();
const currentLayerRef = useRef<HTMLDivElement>(null);
const exitingLayerRef = useRef<HTMLDivElement>(null);
const exitScrollOffsetRef = useRef(0);
const [isAnimating, setIsAnimating] = useState(false);
const [transitionDirection, setTransitionDirection] = useState<TransitionDirection>('forward');
@@ -37,9 +43,17 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
const currentLayerKey = layers[layers.length - 1]?.key ?? location.key;
const currentLayerPathname = layers[layers.length - 1]?.location.pathname;
const resolveScrollContainer = useCallback(() => {
if (scrollContainerRef?.current) return scrollContainerRef.current;
if (typeof document === 'undefined') return null;
return document.scrollingElement as HTMLElement | null;
}, [scrollContainerRef]);
useEffect(() => {
if (isAnimating) return;
if (location.key === currentLayerKey) return;
const scrollContainer = resolveScrollContainer();
exitScrollOffsetRef.current = scrollContainer?.scrollTop ?? 0;
const resolveOrderIndex = (pathname?: string) => {
if (!getRouteOrder || !pathname) return null;
const index = getRouteOrder(pathname);
@@ -70,6 +84,7 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
currentLayerKey,
currentLayerPathname,
getRouteOrder,
resolveScrollContainer,
]);
// Run GSAP animation when animating starts
@@ -78,6 +93,12 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
if (!currentLayerRef.current) return;
const scrollContainer = resolveScrollContainer();
const scrollOffset = exitScrollOffsetRef.current;
if (scrollContainer && scrollOffset > 0) {
scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'auto' });
}
const tl = gsap.timeline({
onComplete: () => {
setLayers((prev) => prev.filter((layer) => layer.status !== 'exiting'));
@@ -87,11 +108,12 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
// Exit animation: fly out to top (slow-to-fast)
if (exitingLayerRef.current) {
gsap.set(exitingLayerRef.current, { y: scrollOffset ? -scrollOffset : 0 });
tl.fromTo(
exitingLayerRef.current,
{ y: 0, opacity: 1 },
{ yPercent: 0, opacity: 1 },
{
y: transitionDirection === 'forward' ? '-100%' : '100%',
yPercent: transitionDirection === 'forward' ? -100 : 100,
opacity: 0,
duration: TRANSITION_DURATION,
ease: 'power3.in', // slow start, fast end
@@ -103,9 +125,9 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
// Enter animation: slide in from bottom (slow-to-fast)
tl.fromTo(
currentLayerRef.current,
{ y: transitionDirection === 'forward' ? '100%' : '-100%', opacity: 0 },
{ yPercent: transitionDirection === 'forward' ? 100 : -100, opacity: 0 },
{
y: 0,
yPercent: 0,
opacity: 1,
duration: TRANSITION_DURATION,
ease: 'power2.in', // slow start, fast end
@@ -117,7 +139,7 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
tl.kill();
gsap.killTweensOf([currentLayerRef.current, exitingLayerRef.current]);
};
}, [isAnimating, transitionDirection]);
}, [isAnimating, transitionDirection, resolveScrollContainer]);
return (
<div className={`page-transition${isAnimating ? ' page-transition--animating' : ''}`}>