Skip to content

feat(dia.Paper): add two-finger pinch and pan touch gestures - #3506

Open
samuelgja wants to merge 5 commits into
clientIO:devfrom
samuelgja:feat/new-touch-gestures
Open

samuelgja wants to merge 5 commits into
clientIO:devfrom
samuelgja:feat/new-touch-gestures

Conversation

@samuelgja

@samuelgja samuelgja commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

Two-finger touch gestures (pinch-to-zoom, two-finger pan) for dia.Paper, with the <Paper /> integration in @joint/react. Most of the change is in @joint/core - please review it as a core feature.

@joint/core

  • dia.Paper - paper:pinch / paper:pan from a touchscreen. A new internal recognizer (src/dia/PaperTouchGestures.mjs, not exported) fires the existing touchpad events for two fingers, once per animation frame: paper:pan with wheel-sign deltas, paper:pinch with the local midpoint and the relative scale. A gesture starts no cell or blank interaction, and the content under the fingers hears none of their pointer events (a finger already announced as a press only gets its matching pointerup).
  • Opt-in: only while paper:pinch has a listener. This mirrors the touchpad pinch (ctrl + wheel), which the paper consumes only with a paper:pinch listener. A paper:pan listener alone - the wheel's pan - does not opt in. A paper without a paper:pinch listener keeps every touch and pointer event exactly as before (covered by tests).
  • Delayed touch press (opted-in papers only). To tell a press from the first finger of a gesture, a touch press is announced once the finger is held for TOUCH_PRESS_DELAY (150 ms), travels TOUCH_PRESS_THRESHOLD (20 px), or lifts. The native pointerdown of content under the finger is withheld and replayed when the press is announced. Both constants are on the paper prototype and typed in dia.d.ts.
  • Cancelled presses are cancels, for every paper:
    • dia.ElementView - a drag ended by touchcancel / pointercancel is reverted (position, parent, z-indices) instead of committed (dragCancel()).
    • dia.LinkView - an arrowhead drag ended by a cancel is undone the way a disallowed connection is.
    • dia.Paper - a cancel no longer triggers pointerclick or element:magnet:pointerclick.

@joint/react

  • <Paper /> - onPaperPinch / onPaperPan receive the touch gestures (onPaperPinch opts the paper in, as above).
  • Native touch scrolling stays inside data-jj-scrollable regions and <textarea>s with overflow (a touch guard next to the existing wheel guard), including the compatibility mousedown the browser sends after such a tap.
  • touch-action: manipulation on the paper stops the browser's double-tap zoom, so the second tap reaches the paper's own dbltap.
  • Story: touch-pan-zoom.

Motivation and Context

On a touchscreen a pinch over the paper zoomed the page and a second finger started a second press on whatever was under it. Apps had to write their own recognizer on top of pointer events, competing with the paper's own handling. The paper now owns two-finger gestures when asked to, through the same events a touchpad already uses.

Tests

  • @joint/core: test/jointjs/dia/Paper.touch.js (single finger, two fingers, native pointer events, cancelled pointers, an app-owned pinch detector left alone). yarn test (2168 QUnit tests plus the geometry, vectorizer and TypeScript checks) and yarn lint pass.
  • @joint/react: presets/__tests__/paper-touch.test.tsx, utils/__tests__/wheel-guard.test.ts. yarn test (typecheck, knip, duplicity check, Jest on React 19 and React 18) passes.
  • The branch merges into the current dev without conflicts; the touch tests also pass under dev's @joint/mock-svg Jest setup.
  • Manual, Safari on the iOS 26.1 simulator (iPhone 17 Pro), with touch input injected by the simulator. Safari sends pointerdown before touchstart, the order the recognizer relies on.
    • Vanilla JointJS, with a paper:pinch listener: a tap announces one press, release and click; a double tap fires element:pointerdblclick; a drag moves the element with no click; a pinch over the element zooms at the fingers (spread x2.5, scale 2.48) with no press; a two-finger pan fires paper:pan with no press; a second finger during a drag puts the element back, with no click, and starts a gesture.
    • Vanilla JointJS, with only a paper:pan listener: touches press immediately, with no replay and no drag dead zone.
    • @joint/react touch-pan-zoom story: pinch over a node (exactly 200%), two-finger pan, node drag, and a second finger during a drag putting the node back.
    • @joint/react test page with a data-jj-scrollable region and a <textarea>: a tap on the region reaches its onClick while the paper ignores Safari's compatibility mousedown, with no drag left behind; a swipe scrolls the region natively; two fingers landing on the region are left to the browser, with both pointerdowns delivered; a tap focuses the <textarea>; a drag from elsewhere on the node still moves it.

Known limits

  • Not yet run on a physical iOS or Android device, or in Android browsers. The Apple Pencil path is untested.
  • With gestures on, a drag that passes the threshold before TOUCH_PRESS_DELAY ends starts at the finger's position then, so the element moves about TOUCH_PRESS_THRESHOLD less than the finger travelled.
  • @joint/react: a mouse press on the same DOM node within a second of a guarded touch landing on it is also treated as that touch's compatibility mousedown, because no portable flag tells them apart.
  • Tool drags (linkTools, elementTools) run on their own document events and are not cancelled by a second finger.
  • A cancelled label drag or whole-link move still commits (only arrowhead drags are undone).
  • Every paper binds two capture listeners (pointerdown, touchstart) that return immediately without a paper:pinch listener.
  • The size (~2.7k added lines, about two thirds of them tests) is one cohesive feature: the recognizer, the cancel semantics it relies on and the React integration.

- Introduced `Paper.touch.js` to handle touch events for pinch and pan gestures.
- Updated `ElementView` to include `dragCancel` method for handling canceled drags.
- Enhanced `Paper` class with new properties for touch gesture thresholds.
- Implemented tests for touch gestures in `paper-touch.test.tsx`.
- Modified event handling in `paper-events.ts` to support new touch events.
- Updated CSS to prevent double-tap zoom on touch devices.
- Added examples for touch pan and zoom functionality in stories.
@samuelgja
samuelgja changed the base branch from master to dev September 15, 2026 12:39
@samuelgja samuelgja changed the title Feat/new touch gestures feat: new touch gestures (draft) Sep 15, 2026
@kumilingus
kumilingus self-requested a review September 22, 2026 13:28
@kumilingus kumilingus added this to the v4.4 milestone Sep 28, 2026
@samuelgja samuelgja self-assigned this Nov 3, 2026
…ding label drags, no magnet click on cancel

- a paper:pan listener alone (the wheel's pan) no longer delays touch presses
- a browser pointercancel ends a label drag that went live while the press was pending
- element:magnet:pointerclick is not triggered by a cancelled press
- the tapped pointer is no longer left in the tracked pointers
- <Paper /> guards the compatibility mousedown that follows a guarded touch
@samuelgja samuelgja changed the title feat: new touch gestures (draft) feat(dia.Paper): add two-finger pinch and pan touch gestures Nov 3, 2026
@samuelgja
samuelgja marked this pull request as ready for review November 3, 2026 12:09
@samuelgja
samuelgja requested a balanced review from Copilot November 3, 2026 12:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved touch-gesture defects cause drag jumps, bypass guarded native scrolling, emit spurious double-clicks, and mishandle active-pointer cleanup.

4 open issues
What changed in this PR

Adds opt-in two-finger touch gestures to dia.Paper, reusing the existing pan and pinch events and exposing them through @joint/react.

Changes:

  • Adds a touch recognizer with delayed presses and frame-coalesced gesture events.
  • Reverts cancelled element and arrowhead drags and suppresses cancel-triggered clicks.
  • Integrates React scroll guards, event documentation, regression tests, and a touch pan/zoom example.
File Description
packages/​joint-react/​stories/​examples/​touch-pan-zoom/​story.tsx Registers the touchscreen example.
packages/​joint-react/​stories/​examples/​touch-pan-zoom/​code.tsx Demonstrates gesture-driven viewport controls.
packages/​joint-react/​src/​utils/​wheel-guard.ts Adds touch and compatibility-mouse guards.
packages/​joint-react/​src/​utils/​__tests__/​wheel-guard.test.ts Tests touch guard behavior.
packages/​joint-react/​src/​presets/​paper.ts Integrates touch guards and cancellation cleanup.
packages/​joint-react/​src/​presets/​paper.css Disables browser double-tap zoom.
packages/​joint-react/​src/​presets/​paper-events.ts Documents touchscreen event payloads and opt-in behavior.
packages/​joint-react/​src/​presets/​__tests__/​paper-touch.test.tsx Tests React touch interactions and cancellation.
packages/​joint-core/​types/​dia.d.ts Declares touch constants and drag cancellation.
packages/​joint-core/​test/​jointjs/​index.html Loads the touch test suite.
packages/​joint-core/​test/​jointjs/​dia/​Paper.touch.js Covers gesture recognition and pointer behavior.
packages/​joint-core/​src/​dia/​PaperTouchGestures.mjs Implements the internal touch recognizer.
packages/​joint-core/​src/​dia/​Paper.mjs Connects recognition to paper interactions.
packages/​joint-core/​src/​dia/​LinkView.mjs Undoes cancelled arrowhead drags.
packages/​joint-core/​src/​dia/​ElementView.mjs Reverts cancelled drags and suppresses magnet clicks.
.changeset/​tidy-taps-reach.md Records double-tap handling changes.
.changeset/​still-presses-wait.md Records delayed touch presses.
.changeset/​soft-regions-scroll.md Records native touch scrolling support.
.changeset/​quiet-cancels-click.md Records cancellation click fixes.
.changeset/​plain-drags-revert.md Records element drag rollback.
.changeset/​lucky-links-undo.md Records arrowhead drag rollback.
.changeset/​calm-fingers-pinch.md Records core two-finger gesture support.
.changeset/​brisk-fingers-zoom.md Records React gesture integration.

Review effort: Balanced

…andings, keep a moved press's coordinates

- a lift while a gesture is live is never paired into a `dbltap`
- both fingers landing in one `touchstart` honor `guard()`; every withheld `pointerdown` is handed back
- pointers the content heard are not tracked after a sequence ends; a finger on content next to the SVG hands the first finger's `pointerdown` back
- the magnet and custom-event wrappers of a withheld press use its resolved coordinates
@samuelgja
samuelgja requested a balanced review from Copilot November 3, 2026 12:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

This branch has not been deployed

No deployments
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.

3 participants