From 5c3777ab467cfc634e66abefdc58a68121efb0cf Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Thu, 12 Mar 2026 10:37:29 -0400 Subject: [PATCH] fix(web): fix zoom touch event handling (#26866) fix(web): fix zoom touch event handling and add clarifying comments - Suppress Safari's synthetic dblclick on double-tap which conflicts with zoom-image's touchstart-based zoom - Add comment explaining pointer-events-none on zoom transform wrapper - Add comments for touchAction and overflow style overrides --- web/src/lib/actions/zoom-image.ts | 20 ++++++++++++++++++++ web/src/lib/components/AdaptiveImage.svelte | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/web/src/lib/actions/zoom-image.ts b/web/src/lib/actions/zoom-image.ts index 66659997d2..35c3d3a106 100644 --- a/web/src/lib/actions/zoom-image.ts +++ b/web/src/lib/actions/zoom-image.ts @@ -23,7 +23,25 @@ export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolea node.addEventListener('wheel', onInteractionStart, { capture: true }); node.addEventListener('pointerdown', onInteractionStart, { capture: true }); + // Suppress Safari's synthetic dblclick on double-tap. Without this, zoom-image's touchstart + // handler zooms to maxZoom (10x), then Safari's synthetic dblclick triggers photo-viewer's + // handler which conflicts. Chrome does not fire synthetic dblclick on touch. + let lastPointerWasTouch = false; + const trackPointerType = (event: PointerEvent) => { + lastPointerWasTouch = event.pointerType === 'touch'; + }; + const suppressTouchDblClick = (event: MouseEvent) => { + if (lastPointerWasTouch) { + event.stopImmediatePropagation(); + } + }; + node.addEventListener('pointerdown', trackPointerType, { capture: true }); + node.addEventListener('dblclick', suppressTouchDblClick, { capture: true }); + + // Allow zoomed content to render outside the container bounds node.style.overflow = 'visible'; + // Prevent browser handling of touch gestures so zoom-image can manage them + node.style.touchAction = 'none'; return { update(newOptions?: { disabled?: boolean }) { options = newOptions; @@ -34,6 +52,8 @@ export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolea } node.removeEventListener('wheel', onInteractionStart, { capture: true }); node.removeEventListener('pointerdown', onInteractionStart, { capture: true }); + node.removeEventListener('pointerdown', trackPointerType, { capture: true }); + node.removeEventListener('dblclick', suppressTouchDblClick, { capture: true }); zoomInstance.cleanup(); }, }; diff --git a/web/src/lib/components/AdaptiveImage.svelte b/web/src/lib/components/AdaptiveImage.svelte index 92e3fad2d3..fad4d49d1b 100644 --- a/web/src/lib/components/AdaptiveImage.svelte +++ b/web/src/lib/components/AdaptiveImage.svelte @@ -162,8 +162,9 @@
{@render backdrop?.()} +