From df7d1e8d32a575591f877fe4959305b69f7e02a3 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Wed, 3 Jun 2026 19:24:25 -0400 Subject: [PATCH] Fix web carousel horizontal swipe triggering browser back/forward On web, a horizontal trackpad swipe over the in-feed image carousel could trigger the browser's back/forward navigation instead of scrolling the carousel. - Add overscroll-behavior-x: contain to the carousel scroller, which stops Chrome and Firefox from chaining horizontal overscroll into history nav. - Safari ignores overscroll-behavior, so add a non-passive wheel handler that intercepts horizontal-dominant wheel events, scrolls the carousel manually, and preventDefaults the history-nav gesture. Vertical-dominant events are left untouched so normal page scroll still works. APP-2287 --- src/components/images/Gallery/index.tsx | 5 ++ .../images/Gallery/usePointerHandlers.web.ts | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 9ae677c2d2..532d9f6de3 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -340,6 +340,11 @@ export function Gallery({ marginLeft: -insetLeft, width, }, + // Prevent horizontal trackpad/wheel swipes from triggering the + // browser's back/forward overscroll-navigation gesture. Handles + // Chrome and Firefox; Safari is handled via the wheel listener in + // usePointerHandlers.web.ts since it ignores overscroll-behavior. + web({overscrollBehaviorX: 'contain'}), ]} contentContainerStyle={{ gap: ITEM_GAP, diff --git a/src/components/images/Gallery/usePointerHandlers.web.ts b/src/components/images/Gallery/usePointerHandlers.web.ts index 25bebfccbd..326d6dd194 100644 --- a/src/components/images/Gallery/usePointerHandlers.web.ts +++ b/src/components/images/Gallery/usePointerHandlers.web.ts @@ -246,12 +246,63 @@ export function usePointerHandlers({ } } + /* + * Safari does not support `overscroll-behavior`, so a horizontal trackpad + * swipe over the carousel can trigger the browser's back/forward + * navigation gesture. We intercept predominantly-horizontal wheel events + * and apply the scroll ourselves, calling preventDefault to suppress the + * history-nav gesture. Vertical-dominant wheel events are left untouched so + * normal page scroll still works. Chrome/Firefox are covered by the + * `overscrollBehaviorX: 'contain'` style on the FlatList. + * + * Listener must be non-passive so preventDefault is honored. + */ + const onWheel = (e: WheelEvent) => { + // Only act on predominantly-horizontal scrolls. Vertical-dominant events + // are page scroll and must not be swallowed. + if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return + + e.preventDefault() + + // Cancel any in-progress settle tween so manual scrolling feels direct. + if (stopTween) { + stopTween() + stopTween = null + } + if (overscrollX !== 0) clearOverscroll() + + const maxScroll = el.scrollWidth - el.clientWidth + const next = Math.max(0, Math.min(el.scrollLeft + e.deltaX, maxScroll)) + scrollTo(next) + + // Keep the active index in sync so keyboard/lightbox stay correct, but + // only settle when it actually changes - onSettle moves focus, which we + // don't want to thrash on every wheel tick. + let accumulated = 0 + let index = 0 + for (let i = 0; i < imageCount; i++) { + const w = (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP + if (next < accumulated + w / 2) { + index = i + break + } + accumulated += w + if (i === imageCount - 1) index = i + } + if (index !== localIndex) { + localIndex = index + onSettle(index) + } + } + el.addEventListener('mousedown', onMouseDown) + el.addEventListener('wheel', onWheel, {passive: false}) window.addEventListener('mousemove', onMouseMove) window.addEventListener('mouseup', onMouseUp) return () => { el.removeEventListener('mousedown', onMouseDown) + el.removeEventListener('wheel', onWheel) window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mouseup', onMouseUp) if (stopTween) stopTween()