diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 905c17048f..77971816ca 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -692,6 +692,7 @@ class App extends React.Component { lastPointerMoveEvent: PointerEvent | null = null; /** current frame pointer cords */ lastPointerMoveCoords: { x: number; y: number } | null = null; + private lastCompletedCanvasClicks: { x: number; y: number }[] = []; /** previous frame pointer coords */ previousPointerMoveCoords: { x: number; y: number } | null = null; lastViewportPosition = { x: 0, y: 0 }; @@ -2341,6 +2342,7 @@ class App extends React.Component { } handleCanvasRef={this.handleInteractiveCanvasRef} onContextMenu={this.handleCanvasContextMenu} + onClick={this.handleCanvasClick} onPointerMove={this.handleCanvasPointerMove} onPointerUp={this.handleCanvasPointerUp} onPointerCancel={this.removePointer} @@ -3594,10 +3596,14 @@ class App extends React.Component { this.lassoTrail.endPath(); this.deselectElements(); - // @ts-ignore this.handleCanvasDoubleClick({ clientX: touch.clientX, clientY: touch.clientY, + type: "touch", + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, }); } didTapTwice = false; @@ -6220,10 +6226,46 @@ class App extends React.Component { } }; + private shouldHandleBrowserCanvasDoubleClick = (type: string) => { + // TODO remove this once we consolidate double-click logic and handle + // ourselves for all event types together + if (type === "touch") { + return true; + } + if (this.lastCompletedCanvasClicks.length === 0) { + return true; + } + + if (this.lastCompletedCanvasClicks.length < 2) { + return false; + } + + const [firstClick, secondClick] = this.lastCompletedCanvasClicks; + + return ( + pointDistance( + pointFrom(firstClick.x, firstClick.y), + pointFrom(secondClick.x, secondClick.y), + ) <= DOUBLE_TAP_POSITION_THRESHOLD + ); + }; + private handleCanvasDoubleClick = ( - event: React.MouseEvent, + event: Pick< + React.MouseEvent, + | "type" + | "clientX" + | "clientY" + | "altKey" + | "ctrlKey" + | "metaKey" + | "shiftKey" + >, ) => { - if (this.state.editingTextElement) { + if ( + this.state.editingTextElement || + !this.shouldHandleBrowserCanvasDoubleClick(event.type) + ) { return; } // case: double-clicking with arrow/line tool selected would both create @@ -6413,6 +6455,21 @@ class App extends React.Component { } }; + private handleCanvasClick = (event: React.MouseEvent) => { + if (event.button !== POINTER_BUTTON.MAIN) { + this.lastCompletedCanvasClicks = []; + return; + } + + this.lastCompletedCanvasClicks = [ + ...this.lastCompletedCanvasClicks.slice(-1), + { + x: event.clientX, + y: event.clientY, + }, + ]; + }; + private getElementLinkAtPosition = ( scenePointer: Readonly<{ x: number; y: number }>, hitElementMightBeLocked: NonDeletedExcalidrawElement | null, diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx index a30088f30a..2aabddfbab 100644 --- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx +++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx @@ -54,6 +54,7 @@ type InteractiveCanvasProps = { DOMAttributes["onContextMenu"], undefined >; + onClick: Exclude["onClick"], undefined>; onPointerMove: Exclude< DOMAttributes["onPointerMove"], undefined @@ -213,6 +214,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => { height={props.appState.height * props.scale} ref={props.handleCanvasRef} onContextMenu={props.onContextMenu} + onClick={props.onClick} onPointerMove={props.onPointerMove} onPointerUp={props.onPointerUp} onPointerCancel={props.onPointerCancel} diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx index 352b1a1529..de1d08574f 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx @@ -293,6 +293,39 @@ describe("textWysiwyg", () => { expect(await getTextEditor({ waitForEditor: false })).toBe(null); }); + it("should ignore double-click when the second click ends away from where it started", async () => { + UI.clickTool("selection"); + + mouse.downAt(40, 40); + mouse.upAt(40, 40); + fireEvent.click(GlobalTestState.interactiveCanvas, { + button: 0, + clientX: 40, + clientY: 40, + }); + + mouse.downAt(40, 40); + mouse.moveTo(200, 200); + mouse.upAt(200, 200); + fireEvent.click(GlobalTestState.interactiveCanvas, { + button: 0, + clientX: 200, + clientY: 200, + }); + + fireEvent.doubleClick(GlobalTestState.interactiveCanvas, { + button: 0, + clientX: 200, + clientY: 200, + }); + + const editor = await getTextEditor({ waitForEditor: false }); + + expect(editor).toBe(null); + expect(h.state.editingTextElement).toBe(null); + expect(h.elements.length).toBe(0); + }); + // FIXME too flaky. No one knows why. it.skip("should bump the version of a labeled arrow when the label is updated", async () => { const arrow = UI.createElement("arrow", {