mirror of
https://github.com/Afilmory/afilmory
synced 2026-05-28 07:15:21 +00:00
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:
@@ -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(() => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user