From dcc825c2a6bdd4c3f3f45da2b6cae33f411eaacd Mon Sep 17 00:00:00 2001 From: sezzim <174854242+sezzim@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:22:22 -0700 Subject: [PATCH] Add swipe navigation to lightbox --- ui/v2.5/src/hooks/Lightbox/Lightbox.tsx | 62 +++++++++++----- ui/v2.5/src/hooks/Lightbox/LightboxImage.tsx | 76 ++++++++++++++++---- ui/v2.5/src/hooks/Lightbox/lightbox.scss | 5 ++ 3 files changed, 110 insertions(+), 33 deletions(-) diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index 23552f745..b40d7ad17 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -76,6 +76,7 @@ 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`; @@ -91,7 +92,7 @@ const SCROLL_ZOOM_TIMEOUT = 250; const ZOOM_NONE_EPSILON = 0.015; interface ILightboxCarouselProps { - instantTransition: boolean; + transition: string | null; currentIndex: number; images: ILightboxImage[]; displayMode: GQL.ImageLightboxDisplayMode; @@ -106,11 +107,12 @@ interface ILightboxCarouselProps { debouncedScrollReset: () => void; handleLeft: () => void; handleRight: () => void; + overrideTransition: (t: string) => void; } const LightboxCarousel = forwardRef(function ( { - instantTransition, + transition, currentIndex, images, displayMode, @@ -125,15 +127,36 @@ const LightboxCarousel = forwardRef(function ( 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.TouchEvent, + swipeDuration: number + ) { + const cappedDuration = Math.max(50, Math.min(500, swipeDuration)) / 1000; + const adjustedShift = carouselShift / (2 * cappedDuration); + if (adjustedShift < -window.innerWidth / 2) { + handleRight(); + } else if (adjustedShift > window.innerWidth / 2) { + handleLeft(); + } + setCarouselShift(0); + overrideTransition(CLASSNAME_SWIPE); + } + return (
{images.map((image, i) => ( @@ -160,6 +183,8 @@ const LightboxCarousel = forwardRef(function ( onLeft={handleLeft} onRight={handleRight} isVideo={isVideo(image.visual_files?.[0] ?? {})} + moveCarousel={handleMoveCarousel} + releaseCarousel={handleReleaseCarousel} /> ) : undefined}
@@ -203,7 +228,7 @@ 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 [transition, setTransition] = useState(null); const [isSwitchingPage, setIsSwitchingPage] = useState(true); const [isFullscreen, setFullscreen] = useState(false); const [showOptions, setShowOptions] = useState(false); @@ -315,15 +340,15 @@ export const LightboxComponent: React.FC = ({ } }, [isSwitchingPage, images, index]); - const disableInstantTransition = useDebounce( - () => setInstantTransition(false), - 400 - ); + const restoreTransition = useDebounce(() => setTransition(null), 400); - const setInstant = useCallback(() => { - setInstantTransition(true); - disableInstantTransition(); - }, [disableInstantTransition]); + const overrideTransition = useCallback( + (t: string) => { + setTransition(t); + restoreTransition(); + }, + [restoreTransition] + ); useEffect(() => { if (images.length < 2) return; @@ -487,12 +512,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) { @@ -934,7 +959,7 @@ export const LightboxComponent: React.FC = ({ )} = ({ debouncedScrollReset={debouncedScrollReset} handleLeft={handleLeft} handleRight={handleRight} + overrideTransition={overrideTransition} /> {allowNavigation && (