fix(react): WebKit trackpad pinch-zoom via gesture events - #35
Merged
Conversation
Safari / WKWebView encode a trackpad pinch as gesturestart/change/end
(GestureEvent, cumulative scale) and do NOT emit the ctrlKey wheel that
Chromium does, so pinch-zoom never worked there — the gesture handlers
only preventDefault'd. Translate the cumulative scale into the same
accumulate-and-flush zoom the wheel path already uses, anchored at the
gesture point, so it goes through the identical clamp/limits and drives
motion-LOD via pulseMotion('zooming').
A gestureActive flag suppresses any stray ctrlKey wheel during a native
pinch as a double-apply guard. Chromium/Firefox never fire these events,
so the path is inert there and the existing wheel+ctrl zoom is unchanged.
Adds a browser test that dispatches synthetic gesture events (chromium
doesn't fire them natively) and asserts zoom magnitude, cumulative-scale
handling (no double-apply), and the zooming interaction mode. Real Safari
still needs a manual pass.
… dedup Follow-up to the WebKit pinch-zoom handlers: - Defer the gesture* path when two touches are active (activeTouches.size >= 2). iOS/iPadOS fire BOTH GestureEvents and touch pointers for one pinch; the pointer pinch path already owns that case, so the gesture path must not also apply zoom (was double-applying on touchscreens). - Self-heal the cumulative-scale base: a gesturechange with no fresh gesturestart (missed, or bailed while editing) now seeds the base and skips a frame instead of dividing by a stale base and snapping zoom. - Drop the gestureActive flag + its onWheel guard. It guarded against an engine firing both gesture events and a ctrlKey wheel for one pinch, which no engine does — while risking a stuck-true state that swallowed wheel input, and suppressing two-finger pan wheels. Removing it fixes both without weakening real coverage. - Extract queueZoom(factor, anchor) so the finite/positive guard + accumulate contract lives in one place (was triplicated across wheel, touch and gesture paths). Adds tests for the touch-defer and stale-base cases. Full react suite (17 browser + unit), repo lint + typecheck all pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two-finger trackpad pinch-zoom works in Chrome but does nothing in Safari and the Dim0 desktop app (Tauri → WKWebView).
Root cause: the library derives zoom only from
wheelevents carryingctrlKey— Chromium's encoding of a trackpad pinch. WebKit doesn't do that: it firesgesturestart/gesturechange/gestureend(GestureEvent, cumulativescale) and emits noctrlKeywheel. The gesture handlers here onlypreventDefault()'d, so on WebKit the pinch was swallowed — no canvas zoom and no native page zoom either.Fix
packages/react/src/internal/use-pan-zoom.ts— translategesturechange'sscaleinto the same accumulate-and-flush zoom pipeline the wheel path already uses (pendingZoomFactor/pendingZoomAnchor→zoomAtScreenPoint+clampZoom), anchored at the gesture point, and drive motion-LOD viapulseMotion('zooming').scaleis cumulative sincegesturestart(=1.0), so we track a base and divide to get the per-event factor — applying it raw would double-apply and over-zoom.gestureActiveflag suppresses any strayctrlKeywheel during a native pinch (double-apply guard).preventDefault()'d to suppress WebKit's native page-magnification.Tests
New
tests/use-pan-zoom.browser.test.tsxdispatches synthetic gesture events (chromium doesn't fire them natively but the handlers are wired viaaddEventListener) and asserts:scale1 → 1.5 produces a 1.5× zoom,scale(1 → 1.2 → 1.44) yields 1.44× net — not double-applied,zoominginteraction mode (motion-LOD).All green: full react unit + browser suites (15/15), repo lint + typecheck pass.
Still needs a human
Chromium can't fire real
GestureEvents, so the automated test covers handler math/wiring, not actual Safari behaviour. Manual pass: desktop Safari, pinch → should zoom toward the cursor; confirm Chrome unchanged; then the Dim0 desktop app once a fixed@canvas-harness/*is published.