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
This commit is contained in:
Claude
2026-03-29 12:16:09 +00:00
parent 67a52ebe3d
commit ff292bbc28
3 changed files with 34 additions and 5 deletions

View File

@@ -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(() => {

View File

@@ -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<HTMLDivElement | null>(null)
const cachedTriggerRef = useRef<HTMLElement | null>(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)

View File

@@ -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<string | null>(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 <NotFound />
}
const isOpen = photoViewer.isOpen && !isClosing
return (
<RootPortal>
<RootPortalProvider value={rootPortalValue}>
@@ -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')}
>
<PhotoViewer
photos={photos}
currentIndex={photoViewer.currentIndex}
isOpen={photoViewer.isOpen}
isOpen={isOpen}
triggerElement={photoViewer.triggerElement}
onClose={photoViewer.closeViewer}
onClose={handleClose}
onIndexChange={photoViewer.goToIndex}
onExitComplete={handleExitComplete}
/>
</RemoveScroll>
</RootPortalProvider>