diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index 41c6d4fad..7d0f8d073 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -1,4 +1,11 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { + ForwardedRef, + forwardRef, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { Button, Col, @@ -69,7 +76,9 @@ const CLASSNAME_FOOTER_RIGHT = `${CLASSNAME_FOOTER}-right`; const CLASSNAME_DISPLAY = `${CLASSNAME}-display`; const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`; const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`; +const CLASSNAME_SWIPE = `${CLASSNAME_CAROUSEL}-swipe`; const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`; +const CLASSNAME_IMAGE_CONTAINER = `${CLASSNAME_CAROUSEL}-image-container`; const CLASSNAME_NAVBUTTON = `${CLASSNAME}-navbutton`; const CLASSNAME_NAV = `${CLASSNAME}-nav`; const CLASSNAME_NAVIMAGE = `${CLASSNAME_NAV}-image`; @@ -82,6 +91,113 @@ const MIN_ZOOM = 0.1; const SCROLL_ZOOM_TIMEOUT = 250; const ZOOM_NONE_EPSILON = 0.015; +interface ILightboxCarouselProps { + transition: string | null; + currentIndex: number; + images: ILightboxImage[]; + displayMode: GQL.ImageLightboxDisplayMode; + scaleUp: boolean; + scrollMode: GQL.ImageLightboxScrollMode; + resetPosition?: boolean; + zoom: number; + scrollAttemptsBeforeChange: number; + firstScroll: React.MutableRefObject; + inScrollGroup: React.MutableRefObject; + movingLeft: boolean; + updateZoom: (v: number) => void; + debouncedScrollReset: () => void; + handleLeft: () => void; + handleRight: () => void; + overrideTransition: (t: string) => void; +} + +const LightboxCarousel = forwardRef(function ( + { + transition, + currentIndex, + images, + displayMode, + scaleUp, + scrollMode, + resetPosition, + zoom, + scrollAttemptsBeforeChange, + firstScroll, + inScrollGroup, + movingLeft, + updateZoom, + debouncedScrollReset, + handleLeft, + handleRight, + overrideTransition, + }: ILightboxCarouselProps, + carouselRef: ForwardedRef +) { + const [carouselShift, setCarouselShift] = useState(0); + + function handleMoveCarousel(delta: number) { + overrideTransition(CLASSNAME_INSTANT); + setCarouselShift(carouselShift + delta); + } + + function handleReleaseCarousel( + event: React.PointerEvent, + swipeDuration: number, + cancelled: boolean + ): boolean { + const cappedDuration = Math.max(50, Math.min(500, swipeDuration)) / 1000; + const adjustedShift = carouselShift / (2 * cappedDuration); + let changed = false; + if (!cancelled && adjustedShift < -window.innerWidth / 2) { + handleRight(); + changed = true; + } else if (!cancelled && adjustedShift > window.innerWidth / 2) { + handleLeft(); + changed = true; + } + setCarouselShift(0); + overrideTransition(CLASSNAME_SWIPE); + return changed; + } + + return ( +
+ {images.map((image, i) => ( +
+ {i >= currentIndex - 1 && i <= currentIndex + 1 ? ( + + ) : undefined} +
+ ))} +
+ ); +}); + interface IProps { images: ILightboxImage[]; isVisible: boolean; @@ -117,7 +233,6 @@ export const LightboxComponent: React.FC = ({ const [index, setIndex] = useState(null); const [movingLeft, setMovingLeft] = useState(false); const oldIndex = useRef(null); - const [instantTransition, setInstantTransition] = useState(false); const [isSwitchingPage, setIsSwitchingPage] = useState(true); const [isFullscreen, setFullscreen] = useState(false); const [showOptions, setShowOptions] = useState(false); @@ -144,7 +259,6 @@ export const LightboxComponent: React.FC = ({ const containerRef = useRef(null); const overlayTarget = useRef(null); - const carouselRef = useRef(null); const indicatorRef = useRef(null); const navRef = useRef(null); const clearIntervalCallback = useRef<() => void>(); @@ -201,6 +315,11 @@ export const LightboxComponent: React.FC = ({ ); const disableAnimation = config?.interface.imageLightbox.disableAnimation; + const defaultTransition = disableAnimation ? CLASSNAME_INSTANT : null; + + const [transition, setTransition] = useState( + defaultTransition + ); function setSlideshowDelay(v: number) { setLightboxSettings({ slideshowDelay: v }); @@ -249,15 +368,18 @@ export const LightboxComponent: React.FC = ({ } }, [isSwitchingPage, images, index]); - const disableInstantTransition = useDebounce( - () => setInstantTransition(false), + const restoreTransition = useDebounce( + () => setTransition(defaultTransition), 400 ); - const setInstant = useCallback(() => { - setInstantTransition(true); - disableInstantTransition(); - }, [disableInstantTransition]); + const overrideTransition = useCallback( + (t: string) => { + setTransition(t); + restoreTransition(); + }, + [restoreTransition] + ); useEffect(() => { if (images.length < 2) return; @@ -359,10 +481,6 @@ export const LightboxComponent: React.FC = ({ (isUserAction = true) => { if (isSwitchingPage || index === -1) return; - if (disableAnimation) { - setInstant(); - } - setShowChapters(false); setMovingLeft(true); @@ -380,25 +498,13 @@ export const LightboxComponent: React.FC = ({ resetIntervalCallback.current(); } }, - [ - images, - pageCallback, - isSwitchingPage, - resetIntervalCallback, - index, - disableAnimation, - setInstant, - ] + [images, pageCallback, isSwitchingPage, resetIntervalCallback, index] ); const handleRight = useCallback( (isUserAction = true) => { if (isSwitchingPage) return; - if (disableAnimation) { - setInstant(); - } - setMovingLeft(false); setShowChapters(false); @@ -423,8 +529,6 @@ export const LightboxComponent: React.FC = ({ isSwitchingPage, resetIntervalCallback, index, - disableAnimation, - setInstant, ] ); @@ -439,12 +543,12 @@ export const LightboxComponent: React.FC = ({ const handleKey = useCallback( (e: KeyboardEvent) => { if (e.repeat && (e.key === "ArrowRight" || e.key === "ArrowLeft")) - setInstant(); + overrideTransition(CLASSNAME_INSTANT); if (e.key === "ArrowLeft") handleLeft(); else if (e.key === "ArrowRight") handleRight(); else if (e.key === "Escape") close(); }, - [setInstant, handleLeft, handleRight, close] + [overrideTransition, handleLeft, handleRight, close] ); const handleFullScreenChange = () => { if (clearIntervalCallback.current) { @@ -883,42 +987,25 @@ export const LightboxComponent: React.FC = ({ )} - -
- {images.map((image, i) => ( -
- {i >= currentIndex - 1 && i <= currentIndex + 1 ? ( - - ) : undefined} -
- ))} -
- + {allowNavigation && (