mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 19:30:51 +08:00
fix(transition): preserve scroll position during page animations
This commit is contained in:
@@ -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' : ''}`}>
|
||||
|
||||
Reference in New Issue
Block a user