diff --git a/src/components/App.tsx b/src/components/App.tsx index 76352cbd..35b84fee 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -125,6 +125,7 @@ import { INITAL_SCENE_UPDATE_TIMEOUT, TAP_TWICE_TIMEOUT, SYNC_FULL_SCENE_INTERVAL_MS, + TOUCH_CTX_MENU_TIMEOUT, } from "../time_constants"; import LayerUI from "./LayerUI"; @@ -172,6 +173,8 @@ let isHoldingSpace: boolean = false; let isPanning: boolean = false; let isDraggingScrollBar: boolean = false; let currentScrollBars: ScrollBars = { horizontal: null, vertical: null }; +let touchTimeout = 0; +let touchMoving = false; let lastPointerUp: ((event: any) => void) | null = null; const gesture: Gesture = { @@ -256,6 +259,7 @@ class App extends React.Component { onPointerMove={this.handleCanvasPointerMove} onPointerUp={this.removePointer} onPointerCancel={this.removePointer} + onTouchMove={this.handleTouchMove} onDrop={this.handleCanvasOnDrop} > {t("labels.drawingCanvas")} @@ -409,6 +413,8 @@ class App extends React.Component { this.unmounted = true; this.removeSceneCallback!(); this.removeEventListeners(); + + clearTimeout(touchTimeout); } private onResize = withBatchedUpdates(() => { @@ -818,6 +824,12 @@ class App extends React.Component { }; removePointer = (event: React.PointerEvent) => { + // remove touch handler for context menu on touch devices + if (event.pointerType === "touch" && touchTimeout) { + clearTimeout(touchTimeout); + touchMoving = false; + } + gesture.pointers.delete(event.pointerId); }; @@ -1802,11 +1814,32 @@ class App extends React.Component { } }; + // set touch moving for mobile context menu + private handleTouchMove = (event: React.TouchEvent) => { + touchMoving = true; + }; + private handleCanvasPointerDown = ( event: React.PointerEvent, ) => { event.persist(); + // deal with opening context menu on touch devices + if (event.pointerType === "touch") { + touchMoving = false; + + // open the context menu with the first touch's clientX and clientY + // if the touch is not moving + touchTimeout = window.setTimeout(() => { + if (!touchMoving) { + this.openContextMenu({ + clientX: event.clientX, + clientY: event.clientY, + }); + } + }, TOUCH_CTX_MENU_TIMEOUT); + } + if (lastPointerUp !== null) { // Unfortunately, sometimes we don't get a pointerup after a pointerdown, // this can happen when a contextual menu or alert is triggered. In order to avoid @@ -2847,9 +2880,18 @@ class App extends React.Component { event: React.PointerEvent, ) => { event.preventDefault(); + this.openContextMenu(event); + }; + private openContextMenu = ({ + clientX, + clientY, + }: { + clientX: number; + clientY: number; + }) => { const { x, y } = viewportCoordsToSceneCoords( - event, + { clientX, clientY }, this.state, this.canvas, window.devicePixelRatio, @@ -2888,8 +2930,8 @@ class App extends React.Component { action: this.toggleGridMode, }, ], - top: event.clientY, - left: event.clientX, + top: clientY, + left: clientX, }); return; } @@ -2920,8 +2962,8 @@ class App extends React.Component { (action) => !CANVAS_ONLY_ACTIONS.includes(action.name), ), ], - top: event.clientY, - left: event.clientX, + top: clientY, + left: clientX, }); }; diff --git a/src/time_constants.ts b/src/time_constants.ts index 739dbf95..96400d61 100644 --- a/src/time_constants.ts +++ b/src/time_constants.ts @@ -2,3 +2,4 @@ export const TAP_TWICE_TIMEOUT = 300; export const INITAL_SCENE_UPDATE_TIMEOUT = 5000; export const SYNC_FULL_SCENE_INTERVAL_MS = 20000; +export const TOUCH_CTX_MENU_TIMEOUT = 500;