From ff292bbc281176befb5c872b1c8ea9fac80dab2f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 12:16:09 +0000 Subject: [PATCH] fix: restore preview close animation by deferring navigation until exit transition completes When closing the photo viewer, the route component was immediately unmounting due to navigation, preventing the exit transition animation from playing. Now the close triggers a local "closing" state first, allowing the exit animation to complete before navigating away. https://claude.ai/code/session_01GE9xCcB59MnZiERpgBXxo8 --- apps/web/src/modules/viewer/PhotoViewer.tsx | 3 +++ .../animations/usePhotoViewerTransitions.ts | 11 +++++++- .../pages/(main)/photos/[photoId]/index.tsx | 25 ++++++++++++++++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/apps/web/src/modules/viewer/PhotoViewer.tsx b/apps/web/src/modules/viewer/PhotoViewer.tsx index d0d98665..5aa6928f 100644 --- a/apps/web/src/modules/viewer/PhotoViewer.tsx +++ b/apps/web/src/modules/viewer/PhotoViewer.tsx @@ -32,6 +32,7 @@ interface PhotoViewerProps { onClose: () => void onIndexChange: (index: number) => void triggerElement: HTMLElement | null + onExitComplete?: () => void } export const PhotoViewer = ({ @@ -41,6 +42,7 @@ export const PhotoViewer = ({ onClose, onIndexChange, triggerElement, + onExitComplete, }: PhotoViewerProps) => { const { t } = useTranslation() const isMobile = useMobile() @@ -68,6 +70,7 @@ export const PhotoViewer = ({ currentPhoto, currentBlobSrc, isMobile, + onExitComplete, }) useEffect(() => { diff --git a/apps/web/src/modules/viewer/animations/usePhotoViewerTransitions.ts b/apps/web/src/modules/viewer/animations/usePhotoViewerTransitions.ts index afcd7649..47fa3d02 100644 --- a/apps/web/src/modules/viewer/animations/usePhotoViewerTransitions.ts +++ b/apps/web/src/modules/viewer/animations/usePhotoViewerTransitions.ts @@ -12,6 +12,7 @@ interface UsePhotoViewerTransitionsParams { currentPhoto: PhotoManifest | undefined currentBlobSrc: string | null isMobile: boolean + onExitComplete?: () => void } interface UsePhotoViewerTransitionsResult { @@ -33,6 +34,7 @@ export const usePhotoViewerTransitions = ({ currentPhoto, currentBlobSrc, isMobile, + onExitComplete, }: UsePhotoViewerTransitionsParams): UsePhotoViewerTransitionsResult => { const containerRef = useRef(null) const cachedTriggerRef = useRef(triggerElement) @@ -208,6 +210,7 @@ export const usePhotoViewerTransitions = ({ if (!wasOpenRef.current || !currentPhoto) { wasOpenRef.current = false restoreTriggerElementVisibility() + onExitComplete?.() return } @@ -217,6 +220,7 @@ export const usePhotoViewerTransitions = ({ wasOpenRef.current = false restoreTriggerElementVisibility() setExitTransition(null) + onExitComplete?.() return } @@ -225,6 +229,7 @@ export const usePhotoViewerTransitions = ({ wasOpenRef.current = false restoreTriggerElementVisibility() setExitTransition(null) + onExitComplete?.() return } @@ -242,6 +247,7 @@ export const usePhotoViewerTransitions = ({ wasOpenRef.current = false restoreTriggerElementVisibility() setExitTransition(null) + onExitComplete?.() return } @@ -257,6 +263,7 @@ export const usePhotoViewerTransitions = ({ wasOpenRef.current = false restoreTriggerElementVisibility() setExitTransition(null) + onExitComplete?.() return } @@ -294,6 +301,7 @@ export const usePhotoViewerTransitions = ({ resolveTriggerElement, restoreTriggerElementVisibility, hideTriggerElement, + onExitComplete, ]) useLayoutEffect(() => { @@ -321,7 +329,8 @@ export const usePhotoViewerTransitions = ({ const handleExitAnimationComplete = useCallback(() => { restoreTriggerElementVisibility() setExitTransition(null) - }, [restoreTriggerElementVisibility]) + onExitComplete?.() + }, [restoreTriggerElementVisibility, onExitComplete]) const isEntryAnimating = Boolean(entryTransition) const shouldRenderBackdrop = isOpen || Boolean(exitTransition) || Boolean(entryTransition) diff --git a/apps/web/src/pages/(main)/photos/[photoId]/index.tsx b/apps/web/src/pages/(main)/photos/[photoId]/index.tsx index 52424e36..9dc665c4 100644 --- a/apps/web/src/pages/(main)/photos/[photoId]/index.tsx +++ b/apps/web/src/pages/(main)/photos/[photoId]/index.tsx @@ -1,6 +1,6 @@ import { RootPortal, RootPortalProvider } from '@afilmory/ui' import clsx from 'clsx' -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { RemoveScroll } from 'react-remove-scroll' import { NotFound } from '~/components/common/NotFound' @@ -24,6 +24,20 @@ export const Component = () => { const [accentColor, setAccentColor] = useState(null) + // Track closing state to allow exit animation before navigation + const [isClosing, setIsClosing] = useState(false) + const closeViewerRef = useRef(photoViewer.closeViewer) + closeViewerRef.current = photoViewer.closeViewer + + const handleClose = useCallback(() => { + setIsClosing(true) + }, []) + + const handleExitComplete = useCallback(() => { + setIsClosing(false) + closeViewerRef.current() + }, []) + useEffect(() => { const current = photos[photoViewer.currentIndex] if (!current) return @@ -65,6 +79,8 @@ export const Component = () => { return } + const isOpen = photoViewer.isOpen && !isClosing + return ( @@ -75,15 +91,16 @@ export const Component = () => { } as React.CSSProperties } ref={setRef} - className={clsx(photoViewer.isOpen ? 'fixed inset-0 z-9999' : 'pointer-events-none fixed inset-0 z-40')} + className={clsx(isOpen ? 'fixed inset-0 z-9999' : 'pointer-events-none fixed inset-0 z-40')} >