mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +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 { useLocation, type Location } from 'react-router-dom';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import './PageTransition.scss';
|
import './PageTransition.scss';
|
||||||
@@ -6,6 +6,7 @@ import './PageTransition.scss';
|
|||||||
interface PageTransitionProps {
|
interface PageTransitionProps {
|
||||||
render: (location: Location) => ReactNode;
|
render: (location: Location) => ReactNode;
|
||||||
getRouteOrder?: (pathname: string) => number | null;
|
getRouteOrder?: (pathname: string) => number | null;
|
||||||
|
scrollContainerRef?: React.RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSITION_DURATION = 0.65;
|
const TRANSITION_DURATION = 0.65;
|
||||||
@@ -20,10 +21,15 @@ type Layer = {
|
|||||||
|
|
||||||
type TransitionDirection = 'forward' | 'backward';
|
type TransitionDirection = 'forward' | 'backward';
|
||||||
|
|
||||||
export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
|
export function PageTransition({
|
||||||
|
render,
|
||||||
|
getRouteOrder,
|
||||||
|
scrollContainerRef,
|
||||||
|
}: PageTransitionProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const currentLayerRef = useRef<HTMLDivElement>(null);
|
const currentLayerRef = useRef<HTMLDivElement>(null);
|
||||||
const exitingLayerRef = useRef<HTMLDivElement>(null);
|
const exitingLayerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const exitScrollOffsetRef = useRef(0);
|
||||||
|
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
const [transitionDirection, setTransitionDirection] = useState<TransitionDirection>('forward');
|
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 currentLayerKey = layers[layers.length - 1]?.key ?? location.key;
|
||||||
const currentLayerPathname = layers[layers.length - 1]?.location.pathname;
|
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(() => {
|
useEffect(() => {
|
||||||
if (isAnimating) return;
|
if (isAnimating) return;
|
||||||
if (location.key === currentLayerKey) return;
|
if (location.key === currentLayerKey) return;
|
||||||
|
const scrollContainer = resolveScrollContainer();
|
||||||
|
exitScrollOffsetRef.current = scrollContainer?.scrollTop ?? 0;
|
||||||
const resolveOrderIndex = (pathname?: string) => {
|
const resolveOrderIndex = (pathname?: string) => {
|
||||||
if (!getRouteOrder || !pathname) return null;
|
if (!getRouteOrder || !pathname) return null;
|
||||||
const index = getRouteOrder(pathname);
|
const index = getRouteOrder(pathname);
|
||||||
@@ -70,6 +84,7 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
|
|||||||
currentLayerKey,
|
currentLayerKey,
|
||||||
currentLayerPathname,
|
currentLayerPathname,
|
||||||
getRouteOrder,
|
getRouteOrder,
|
||||||
|
resolveScrollContainer,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Run GSAP animation when animating starts
|
// Run GSAP animation when animating starts
|
||||||
@@ -78,6 +93,12 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
|
|||||||
|
|
||||||
if (!currentLayerRef.current) return;
|
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({
|
const tl = gsap.timeline({
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
setLayers((prev) => prev.filter((layer) => layer.status !== 'exiting'));
|
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)
|
// Exit animation: fly out to top (slow-to-fast)
|
||||||
if (exitingLayerRef.current) {
|
if (exitingLayerRef.current) {
|
||||||
|
gsap.set(exitingLayerRef.current, { y: scrollOffset ? -scrollOffset : 0 });
|
||||||
tl.fromTo(
|
tl.fromTo(
|
||||||
exitingLayerRef.current,
|
exitingLayerRef.current,
|
||||||
{ y: 0, opacity: 1 },
|
{ yPercent: 0, opacity: 1 },
|
||||||
{
|
{
|
||||||
y: transitionDirection === 'forward' ? '-100%' : '100%',
|
yPercent: transitionDirection === 'forward' ? -100 : 100,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: TRANSITION_DURATION,
|
duration: TRANSITION_DURATION,
|
||||||
ease: 'power3.in', // slow start, fast end
|
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)
|
// Enter animation: slide in from bottom (slow-to-fast)
|
||||||
tl.fromTo(
|
tl.fromTo(
|
||||||
currentLayerRef.current,
|
currentLayerRef.current,
|
||||||
{ y: transitionDirection === 'forward' ? '100%' : '-100%', opacity: 0 },
|
{ yPercent: transitionDirection === 'forward' ? 100 : -100, opacity: 0 },
|
||||||
{
|
{
|
||||||
y: 0,
|
yPercent: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
duration: TRANSITION_DURATION,
|
duration: TRANSITION_DURATION,
|
||||||
ease: 'power2.in', // slow start, fast end
|
ease: 'power2.in', // slow start, fast end
|
||||||
@@ -117,7 +139,7 @@ export function PageTransition({ render, getRouteOrder }: PageTransitionProps) {
|
|||||||
tl.kill();
|
tl.kill();
|
||||||
gsap.killTweensOf([currentLayerRef.current, exitingLayerRef.current]);
|
gsap.killTweensOf([currentLayerRef.current, exitingLayerRef.current]);
|
||||||
};
|
};
|
||||||
}, [isAnimating, transitionDirection]);
|
}, [isAnimating, transitionDirection, resolveScrollContainer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`page-transition${isAnimating ? ' page-transition--animating' : ''}`}>
|
<div className={`page-transition${isAnimating ? ' page-transition--animating' : ''}`}>
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export function MainLayout() {
|
|||||||
const [requestLogDraft, setRequestLogDraft] = useState(false);
|
const [requestLogDraft, setRequestLogDraft] = useState(false);
|
||||||
const [requestLogTouched, setRequestLogTouched] = useState(false);
|
const [requestLogTouched, setRequestLogTouched] = useState(false);
|
||||||
const [requestLogSaving, setRequestLogSaving] = useState(false);
|
const [requestLogSaving, setRequestLogSaving] = useState(false);
|
||||||
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
const brandCollapseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const brandCollapseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const headerRef = useRef<HTMLElement | null>(null);
|
const headerRef = useRef<HTMLElement | null>(null);
|
||||||
const versionTapCount = useRef(0);
|
const versionTapCount = useRef(0);
|
||||||
@@ -343,6 +344,7 @@ export function MainLayout() {
|
|||||||
});
|
});
|
||||||
}, [fetchConfig]);
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
|
||||||
const statusClass =
|
const statusClass =
|
||||||
connectionStatus === 'connected'
|
connectionStatus === 'connected'
|
||||||
? 'success'
|
? 'success'
|
||||||
@@ -522,11 +524,12 @@ export function MainLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className={`content${isLogsPage ? ' content-logs' : ''}`}>
|
<div className={`content${isLogsPage ? ' content-logs' : ''}`} ref={contentRef}>
|
||||||
<main className={`main-content${isLogsPage ? ' main-content-logs' : ''}`}>
|
<main className={`main-content${isLogsPage ? ' main-content-logs' : ''}`}>
|
||||||
<PageTransition
|
<PageTransition
|
||||||
render={(location) => <MainRoutes location={location} />}
|
render={(location) => <MainRoutes location={location} />}
|
||||||
getRouteOrder={getRouteOrder}
|
getRouteOrder={getRouteOrder}
|
||||||
|
scrollContainerRef={contentRef}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user