Repository navigation
Conversation
- 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.
…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
…, as under @joint/mock-svg
…hecks twice; document the pinch-only opt-in
There was a problem hiding this comment.
🟡 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
There was a problem hiding this comment.
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.

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/coredia.Paper-paper:pinch/paper:panfrom 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:panwith wheel-sign deltas,paper:pinchwith 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 matchingpointerup).paper:pinchhas a listener. This mirrors the touchpad pinch (ctrl+ wheel), which the paper consumes only with apaper:pinchlistener. Apaper:panlistener alone - the wheel's pan - does not opt in. A paper without apaper:pinchlistener keeps every touch and pointer event exactly as before (covered by tests).TOUCH_PRESS_DELAY(150 ms), travelsTOUCH_PRESS_THRESHOLD(20 px), or lifts. The nativepointerdownof content under the finger is withheld and replayed when the press is announced. Both constants are on the paper prototype and typed india.d.ts.dia.ElementView- a drag ended bytouchcancel/pointercancelis 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 triggerspointerclickorelement:magnet:pointerclick.@joint/react<Paper />-onPaperPinch/onPaperPanreceive the touch gestures (onPaperPinchopts the paper in, as above).data-jj-scrollableregions and<textarea>s with overflow (a touch guard next to the existing wheel guard), including the compatibilitymousedownthe browser sends after such a tap.touch-action: manipulationon the paper stops the browser's double-tap zoom, so the second tap reaches the paper's owndbltap.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) andyarn lintpass.@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.devwithout conflicts; the touch tests also pass underdev's@joint/mock-svgJest setup.pointerdownbeforetouchstart, the order the recognizer relies on.paper:pinchlistener: a tap announces one press, release and click; a double tap fireselement: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 firespaper:panwith no press; a second finger during a drag puts the element back, with no click, and starts a gesture.paper:panlistener: touches press immediately, with no replay and no drag dead zone.@joint/reacttouch-pan-zoomstory: pinch over a node (exactly 200%), two-finger pan, node drag, and a second finger during a drag putting the node back.@joint/reacttest page with adata-jj-scrollableregion and a<textarea>: a tap on the region reaches itsonClickwhile the paper ignores Safari's compatibilitymousedown, with no drag left behind; a swipe scrolls the region natively; two fingers landing on the region are left to the browser, with bothpointerdowns delivered; a tap focuses the<textarea>; a drag from elsewhere on the node still moves it.Known limits
TOUCH_PRESS_DELAYends starts at the finger's position then, so the element moves aboutTOUCH_PRESS_THRESHOLDless 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 compatibilitymousedown, because no portable flag tells them apart.linkTools,elementTools) run on their own document events and are not cancelled by a second finger.pointerdown,touchstart) that return immediately without apaper:pinchlistener.