From 025af1eee023fe5c22d398f88a948558a972d094 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 7 May 2026 17:14:07 +1000 Subject: [PATCH] fix: enable vertical touch scrolling in mobile diff view setPointerCapture was called immediately on pointer down, which stole all touch events from the browser before the gesture direction was known. Combined with overflow:hidden on all diff containers, this completely blocked vertical scrolling on mobile. Now the gesture direction is detected first (8px dead zone), and setPointerCapture is only called when horizontal drag intent is confirmed. For vertical intent, touch deltas are translated into scrollController.scrollBy calls via a new bindable scrollApi prop exposed by DiffViewer. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Matt Toohey --- .../src/lib/features/diff/DiffModal.svelte | 37 +++++++++++++++++-- .../src/lib/components/DiffViewer.svelte | 9 +++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/apps/staged/src/lib/features/diff/DiffModal.svelte b/apps/staged/src/lib/features/diff/DiffModal.svelte index 49aa8e3e..5d20488f 100644 --- a/apps/staged/src/lib/features/diff/DiffModal.svelte +++ b/apps/staged/src/lib/features/diff/DiffModal.svelte @@ -301,6 +301,10 @@ let mobileDiffStartY = 0; let mobileDiffStartDragX = 0; let mobileDiffIsDragging = $state(false); + let mobileDiffIsScrolling = false; + let mobileDiffLastY = 0; + let diffViewerScrollApi: { scrollBy: (side: 'before' | 'after', deltaY: number) => void } | null = + $state(null); let mobileDiffStyle = $derived( `--mobile-diff-drag-x: ${mobileDiffDragX}px;` + `--mobile-diff-rest-offset: ${MOBILE_DIFF_REST_OFFSET}px;` + @@ -1010,6 +1014,7 @@ function resetMobileDiffDrag() { mobileDiffPointerId = null; mobileDiffIsDragging = false; + mobileDiffIsScrolling = false; mobileDiffDragX = 0; } @@ -1030,9 +1035,10 @@ mobileDiffPointerId = event.pointerId; mobileDiffStartX = event.clientX; mobileDiffStartY = event.clientY; + mobileDiffLastY = event.clientY; mobileDiffStartDragX = mobileDiffDragX; mobileDiffIsDragging = false; - diffViewerContainerEl?.setPointerCapture(event.pointerId); + mobileDiffIsScrolling = false; } function handleMobileDiffPointerMove(event: PointerEvent) { @@ -1040,10 +1046,32 @@ const deltaX = event.clientX - mobileDiffStartX; const deltaY = event.clientY - mobileDiffStartY; + + // Already in vertical scroll mode — translate touch delta to scroll + if (mobileDiffIsScrolling) { + event.preventDefault(); + const moveDeltaY = event.clientY - mobileDiffLastY; + mobileDiffLastY = event.clientY; + diffViewerScrollApi?.scrollBy('after', -moveDeltaY); + return; + } + if (!mobileDiffIsDragging) { - const horizontalIntent = Math.abs(deltaX) > 8 && Math.abs(deltaX) > Math.abs(deltaY); - if (!horizontalIntent) return; - mobileDiffIsDragging = true; + // Determine gesture direction once past dead zone + if (Math.abs(deltaX) > 8 && Math.abs(deltaX) > Math.abs(deltaY)) { + // Horizontal intent — capture pointer and start drag + mobileDiffIsDragging = true; + diffViewerContainerEl?.setPointerCapture(event.pointerId); + } else if (Math.abs(deltaY) > 8 && Math.abs(deltaY) > Math.abs(deltaX)) { + // Vertical intent — start scroll mode + mobileDiffIsScrolling = true; + mobileDiffLastY = event.clientY; + event.preventDefault(); + diffViewerScrollApi?.scrollBy('after', -deltaY); + return; + } else { + return; + } } event.preventDefault(); @@ -1273,6 +1301,7 @@ onCommentCommit={readonly ? undefined : handleNewCommit} onCommentGithub={readonly || !hasPr ? undefined : handleSendToGithub} commentGithubState={readonly || !hasPr ? undefined : getCommentGithubState} + bind:scrollApi={diffViewerScrollApi} /> diff --git a/packages/diff-viewer/src/lib/components/DiffViewer.svelte b/packages/diff-viewer/src/lib/components/DiffViewer.svelte index aecc2a7f..46690a6a 100644 --- a/packages/diff-viewer/src/lib/components/DiffViewer.svelte +++ b/packages/diff-viewer/src/lib/components/DiffViewer.svelte @@ -119,6 +119,9 @@ onCommentGithub?: (comment: Comment) => void; /** Returns the GitHub button state for a given comment. */ commentGithubState?: (comment: Comment) => GithubButtonState; + + /** Bindable API object exposing scroll control for external callers (e.g. mobile touch scroll). */ + scrollApi?: { scrollBy: (side: 'before' | 'after', deltaY: number) => void } | null; } let { @@ -142,6 +145,7 @@ onCommentCommit, onCommentGithub, commentGithubState, + scrollApi = $bindable(null), }: Props = $props(); // ========================================================================== @@ -359,6 +363,11 @@ const scrollController = createScrollController(); + // Expose scroll API for external callers (e.g. mobile touch scroll in DiffModal) + scrollApi = { + scrollBy: (side, deltaY) => scrollController.scrollBy(side, deltaY), + }; + // Update scroll controller with active alignments $effect(() => { const filePath = diff ? getFilePath(diff) : null;