feat(joint-react): out-of-the-box touch support (pinch-to-zoom, two-finger pan) - #3454
Draft
samuelgja wants to merge 5 commits into
Draft
feat(joint-react): out-of-the-box touch support (pinch-to-zoom, two-finger pan)#3454samuelgja wants to merge 5 commits into
samuelgja wants to merge 5 commits into
Conversation
- Implemented usePinchZoom hook for handling pinch and pan gestures. - Created tests for usePinchZoom to ensure correct behavior under various scenarios. - Developed touch-gesture recognition logic to support two-finger pinch and pan gestures. - Added integration tests for touch gestures in Paper component. - Introduced example story for demonstrating pinch-to-zoom and pan functionality in Storybook.
There was a problem hiding this comment.
Pull request overview
Adds out-of-the-box touchscreen pinch-to-zoom and two-finger pan support to @joint/react’s <Paper> by recognizing two-finger touch gestures and re-emitting them as the existing paper:pinch / paper:pan events (matching joint-core’s touchpad ctrl/cmd+wheel pipeline), plus an optional built-in zoom/pan consumer via the new zoomOnPinch prop.
Changes:
- Add a dependency-free two-finger touch recognizer and wire it into the
paperpreset (including scrollable-region opt-out shared with the wheel guard). - Introduce built-in pinch-zoom / touch-pan behavior for
<Paper>viazoomOnPinch(with clamped bounds and automatic yielding to external subscribers). - Add Storybook example plus unit/integration tests for recognizer behavior and
<Paper>touch interactions.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/joint-react/stories/examples/touch-pan-zoom/story.tsx | Adds the “Touch Pan & Zoom” Storybook entry. |
| packages/joint-react/stories/examples/touch-pan-zoom/code.tsx | Provides the example <Paper> setup used by the story. |
| packages/joint-react/src/utils/wheel-guard.ts | Extracts reusable scrollable-region detection for wheel/touch parity. |
| packages/joint-react/src/utils/touch-gestures.ts | Implements the two-finger pinch/pan recognizer and isMultiTouchEvent. |
| packages/joint-react/src/utils/tests/touch-gestures.test.ts | Unit-tests the recognizer state machine, coalescing, and helpers. |
| packages/joint-react/src/presets/paper.ts | Wires touch gesture listeners + interaction neutralization into the paper preset and adds teardown via onRemove. |
| packages/joint-react/src/presets/paper.css | Sets touch-action: manipulation on .jj-paper to disable double-tap zoom. |
| packages/joint-react/src/presets/tests/paper-touch.test.tsx | Integration-tests DOM touch dispatch → paper:pinch/paper:pan emission and drag neutralization. |
| packages/joint-react/src/internal.ts | Re-exports isMultiTouchEvent for internal consumption. |
| packages/joint-react/src/index.ts | Exports PinchZoomBounds type. |
| packages/joint-react/src/hooks/use-pinch-zoom.ts | Adds the built-in pinch-zoom / touch-pan behavior with bounds and yielding. |
| packages/joint-react/src/hooks/use-create-portal-paper.tsx | Hooks usePinchZoom into paper creation and plumbs zoomOnPinch. |
| packages/joint-react/src/hooks/tests/use-pinch-zoom.test.tsx | Tests clamping, yielding, and touch-vs-wheel pan filtering. |
| packages/joint-react/src/components/paper/paper.types.ts | Introduces the zoomOnPinch prop (and imports PinchZoomBounds). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… and improve eligibleTouches method
samuelgja
marked this pull request as draft
August 14, 2026 09:33
…ns with unrelated UI elements
samuelgja
force-pushed
the
feat/mobile-touch-support
branch
from
August 17, 2026 08:06
96e6630 to
a12d642
Compare
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.
Description
Proper touch support for
<Paper>, out of the box: on a touchscreen, pinching zooms the canvas around the gesture point (instead of the browser zooming the page), two-finger drags pan it, and a second finger landing mid-drag cleanly stops the single-pointer interaction. On a touchpad, the existing Ctrl/Cmd+wheel pinch now also zooms with zero configuration.Structure follows the official JointJS+ touch demo:
stopInteractionIfGestureDetectedsubscribed tocell:/blank:pointerdown+pointermove, neutralizing single-pointer interactions via the sanctioned APIs —cellView.preventDefaultInteraction(evt)andpaper.pointerup(evt)(the officialtouches.length > 1predicate is exported asisMultiTouchEvent). Two deliberate hardenings over the demo: the neutralizing pointerup is propagation-stopped so it never synthesizes apointerclick(a two-finger press must not select the pressed cell), and apointermoveleg keyed on the recognizer phase — this preset'sdocumentEventsare PointerEvents, which carry notoucheslist.utils/touch-gestures.ts) replaces the demo's interact.js: it re-emits gestures as the very samepaper:pinch(evt, x, y, scale) /paper:pan(evt, deltaX, deltaY) events joint-core fires for touchpad pinches — one consumer pipeline for wheel and touch (onPaperPinch/onPaperPanprops work unchanged). Samples are rAF-coalesced (one transform per frame on 120 Hz screens). Browser page pinch-zoom/scroll is suppressed with selective non-passivepreventDefault+touch-action: manipulationinstead of interact.js's blankettouch-action: none, so[data-jj-scrollable]regions (and<textarea>s) inside nodes keep native touch scrolling — the same opt-out the wheel guard honors.zoomOnPinchprop (defaulttrue,{ min, max }bounds 0.2–5,falseto disable): applies pinches as a clampedscaleUniformAtPointand touch pans as a translation. Self-yielding — any externalpaper:pinch/paper:pansubscriber (anonPaperPinchprop, a paper scroller's interactions) takes over automatically, so it never double-applies.New story:
Examples/Touch Pan & Zoom. Tests: recognizer unit tests,<Paper>integration tests (real DOM touch dispatches: pinch/pan emission, drag neutralization without a click, scrollable opt-out, leftover-finger draining),zoomOnPinchbehavior tests. Verified in Chrome with real CDP touch input: pinch scales exactly, pan deltas exact,visualViewportuntouched — both standalone and through a JointJS+ PaperScroller (which consumes the events with no changes to its pipeline).Ticket: https://github.com/orgs/clientIO/projects/6/views/13?pane=issue&itemId=226891385
Motivation and Context
On mobile, pinching over any JointJS app zoomed the browser page while the paper panned garbled underneath — multi-touch was unmodelled anywhere in the stack (
normalizeEventcollapses tochangedTouches[0], a secondtouchstartre-enterspointerdownand resets the drag, andpaper:pinchonly ever fired from ctrl+wheel). This makes the canvas own the gesture, as a patch in joint-react with no core changes, following the pattern of the official touch demo.