Skip to content

feat(joint-react): out-of-the-box touch support (pinch-to-zoom, two-finger pan) - #3454

Draft
samuelgja wants to merge 5 commits into
clientIO:devfrom
samuelgja:feat/mobile-touch-support
Draft

feat(joint-react): out-of-the-box touch support (pinch-to-zoom, two-finger pan)#3454
samuelgja wants to merge 5 commits into
clientIO:devfrom
samuelgja:feat/mobile-touch-support

Conversation

@samuelgja

Copy link
Copy Markdown
Contributor

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:

  • stopInteractionIfGestureDetected subscribed to cell:/blank:pointerdown + pointermove, neutralizing single-pointer interactions via the sanctioned APIs — cellView.preventDefaultInteraction(evt) and paper.pointerup(evt) (the official touches.length > 1 predicate is exported as isMultiTouchEvent). Two deliberate hardenings over the demo: the neutralizing pointerup is propagation-stopped so it never synthesizes a pointerclick (a two-finger press must not select the pressed cell), and a pointermove leg keyed on the recognizer phase — this preset's documentEvents are PointerEvents, which carry no touches list.
  • A dependency-free two-finger recognizer (utils/touch-gestures.ts) replaces the demo's interact.js: it re-emits gestures as the very same paper: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 / onPaperPan props work unchanged). Samples are rAF-coalesced (one transform per frame on 120 Hz screens). Browser page pinch-zoom/scroll is suppressed with selective non-passive preventDefault + touch-action: manipulation instead of interact.js's blanket touch-action: none, so [data-jj-scrollable] regions (and <textarea>s) inside nodes keep native touch scrolling — the same opt-out the wheel guard honors.
  • New zoomOnPinch prop (default true, { min, max } bounds 0.2–5, false to disable): applies pinches as a clamped scaleUniformAtPoint and touch pans as a translation. Self-yielding — any external paper:pinch/paper:pan subscriber (an onPaperPinch prop, 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), zoomOnPinch behavior tests. Verified in Chrome with real CDP touch input: pinch scales exactly, pan deltas exact, visualViewport untouched — 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 (normalizeEvent collapses to changedTouches[0], a second touchstart re-enters pointerdown and resets the drag, and paper:pinch only 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.

- 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.
@samuelgja
samuelgja requested review from kumilingus and a lite review from Copilot August 14, 2026 08:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 paper preset (including scrollable-region opt-out shared with the wheel guard).
  • Introduce built-in pinch-zoom / touch-pan behavior for <Paper> via zoomOnPinch (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.

Comment thread packages/joint-react/src/utils/touch-gestures.ts
@samuelgja
samuelgja marked this pull request as draft August 14, 2026 09:33
@samuelgja
samuelgja force-pushed the feat/mobile-touch-support branch from 96e6630 to a12d642 Compare August 17, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants