From c77c9ce65ab843e5d01b6637fe19c5d0bea045e4 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 3 Mar 2021 14:04:02 +0100 Subject: [PATCH] fix: cursor being leaked outside of canvas (#3161) --- src/actions/actionFinalize.tsx | 4 +- src/components/Actions.tsx | 4 +- src/components/App.tsx | 77 ++++++++++++++++------------- src/components/LayerUI.tsx | 1 + src/components/MobileMenu.tsx | 1 + src/components/TopErrorBoundary.tsx | 2 - src/element/resizeElements.ts | 8 --- src/utils.ts | 24 +++++++-- 8 files changed, 69 insertions(+), 52 deletions(-) diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 449e2b15711c..30db774f97f9 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -18,7 +18,7 @@ import { isBindingElement } from "../element/typeChecks"; export const actionFinalize = register({ name: "finalize", - perform: (elements, appState) => { + perform: (elements, appState, _, { canvas }) => { if (appState.editingLinearElement) { const { elementId, @@ -126,7 +126,7 @@ export const actionFinalize = register({ (!appState.elementLocked && appState.elementType !== "draw") || !multiPointElement ) { - resetCursor(); + resetCursor(canvas); } return { elements: newElements, diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 2798caccd65b..03ecd51a8ae1 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -151,10 +151,12 @@ const LIBRARY_ICON = ( ); export const ShapesSwitcher = ({ + canvas, elementType, setAppState, isLibraryOpen, }: { + canvas: HTMLCanvasElement | null; elementType: ExcalidrawElement["type"]; setAppState: React.Component["setState"]; isLibraryOpen: boolean; @@ -185,7 +187,7 @@ export const ShapesSwitcher = ({ multiElement: null, selectedElementIds: {}, }); - setCursorForShape(value); + setCursorForShape(canvas, value); setAppState({}); }} /> diff --git a/src/components/App.tsx b/src/components/App.tsx index b670ca1a2fa0..ddaa552efdb9 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -172,6 +172,7 @@ import { ResolvablePromise, resolvablePromise, sceneCoordsToViewportCoords, + setCursor, setCursorForShape, tupleToCoors, viewportCoordsToSceneCoords, @@ -1440,16 +1441,16 @@ class App extends React.Component { } if (event.key === KEYS.SPACE && gesture.pointers.size === 0) { isHoldingSpace = true; - document.documentElement.style.cursor = CURSOR_TYPE.GRABBING; + setCursor(this.canvas, CURSOR_TYPE.GRABBING); } }); private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => { if (event.key === KEYS.SPACE) { if (this.state.elementType === "selection") { - resetCursor(); + resetCursor(this.canvas); } else { - setCursorForShape(this.state.elementType); + setCursorForShape(this.canvas, this.state.elementType); this.setState({ selectedElementIds: {}, selectedGroupIds: {}, @@ -1475,7 +1476,7 @@ class App extends React.Component { private selectShapeTool(elementType: AppState["elementType"]) { if (!isHoldingSpace) { - setCursorForShape(elementType); + setCursorForShape(this.canvas, elementType); } if (isToolIcon(document.activeElement)) { document.activeElement.blur(); @@ -1601,7 +1602,7 @@ class App extends React.Component { editingElement: null, }); if (this.state.elementLocked) { - setCursorForShape(this.state.elementType); + setCursorForShape(this.canvas, this.state.elementType); } }), element, @@ -1782,7 +1783,7 @@ class App extends React.Component { return; } - resetCursor(); + resetCursor(this.canvas); const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( event, @@ -1814,7 +1815,7 @@ class App extends React.Component { } } - resetCursor(); + resetCursor(this.canvas); if (!event[KEYS.CTRL_OR_CMD]) { this.startTextEditing({ @@ -1880,9 +1881,9 @@ class App extends React.Component { const isOverScrollBar = isPointerOverScrollBars.isOverEither; if (!this.state.draggingElement && !this.state.multiElement) { if (isOverScrollBar) { - resetCursor(); + resetCursor(this.canvas); } else { - setCursorForShape(this.state.elementType); + setCursorForShape(this.canvas, this.state.elementType); } } @@ -1933,7 +1934,7 @@ class App extends React.Component { const { points, lastCommittedPoint } = multiElement; const lastPoint = points[points.length - 1]; - setCursorForShape(this.state.elementType); + setCursorForShape(this.canvas, this.state.elementType); if (lastPoint === lastCommittedPoint) { // if we haven't yet created a temp point and we're beyond commit-zone @@ -1950,7 +1951,7 @@ class App extends React.Component { points: [...points, [scenePointerX - rx, scenePointerY - ry]], }); } else { - document.documentElement.style.cursor = CURSOR_TYPE.POINTER; + setCursor(this.canvas, CURSOR_TYPE.POINTER); // in this branch, we're inside the commit zone, and no uncommitted // point exists. Thus do nothing (don't add/remove points). } @@ -1964,13 +1965,13 @@ class App extends React.Component { lastCommittedPoint[1], ) < LINE_CONFIRM_THRESHOLD ) { - document.documentElement.style.cursor = CURSOR_TYPE.POINTER; + setCursor(this.canvas, CURSOR_TYPE.POINTER); mutateElement(multiElement, { points: points.slice(0, -1), }); } else { if (isPathALoop(points, this.state.zoom.value)) { - document.documentElement.style.cursor = CURSOR_TYPE.POINTER; + setCursor(this.canvas, CURSOR_TYPE.POINTER); } // update last uncommitted point mutateElement(multiElement, { @@ -2013,8 +2014,9 @@ class App extends React.Component { elementWithTransformHandleType && elementWithTransformHandleType.transformHandleType ) { - document.documentElement.style.cursor = getCursorForResizingElement( - elementWithTransformHandleType, + setCursor( + this.canvas, + getCursorForResizingElement(elementWithTransformHandleType), ); return; } @@ -2027,9 +2029,12 @@ class App extends React.Component { event.pointerType, ); if (transformHandleType) { - document.documentElement.style.cursor = getCursorForResizingElement({ - transformHandleType, - }); + setCursor( + this.canvas, + getCursorForResizingElement({ + transformHandleType, + }), + ); return; } } @@ -2039,11 +2044,12 @@ class App extends React.Component { scenePointer.y, ); if (this.state.elementType === "text") { - document.documentElement.style.cursor = isTextElement(hitElement) - ? CURSOR_TYPE.TEXT - : CURSOR_TYPE.CROSSHAIR; + setCursor( + this.canvas, + isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR, + ); } else if (isOverScrollBar) { - document.documentElement.style.cursor = CURSOR_TYPE.AUTO; + setCursor(this.canvas, CURSOR_TYPE.AUTO); } else if ( hitElement || this.isHittingCommonBoundingBoxOfSelectedElements( @@ -2051,9 +2057,9 @@ class App extends React.Component { selectedElements, ) ) { - document.documentElement.style.cursor = CURSOR_TYPE.MOVE; + setCursor(this.canvas, CURSOR_TYPE.MOVE); } else { - document.documentElement.style.cursor = CURSOR_TYPE.AUTO; + setCursor(this.canvas, CURSOR_TYPE.AUTO); } }; @@ -2226,7 +2232,7 @@ class App extends React.Component { let nextPastePrevented = false; const isLinux = /Linux/.test(window.navigator.platform); - document.documentElement.style.cursor = CURSOR_TYPE.GRABBING; + setCursor(this.canvas, CURSOR_TYPE.GRABBING); let { clientX: lastX, clientY: lastY } = event; const onPointerMove = withBatchedUpdates((event: PointerEvent) => { const deltaX = lastX - event.clientX; @@ -2278,7 +2284,7 @@ class App extends React.Component { lastPointerUp = null; isPanning = false; if (!isHoldingSpace) { - setCursorForShape(this.state.elementType); + setCursorForShape(this.canvas, this.state.elementType); } this.setState({ cursorButton: "up", @@ -2394,7 +2400,7 @@ class App extends React.Component { const onPointerUp = withBatchedUpdates(() => { isDraggingScrollBar = false; - setCursorForShape(this.state.elementType); + setCursorForShape(this.canvas, this.state.elementType); lastPointerUp = null; this.setState({ cursorButton: "up", @@ -2457,9 +2463,12 @@ class App extends React.Component { ); } if (pointerDownState.resize.handleType) { - document.documentElement.style.cursor = getCursorForResizingElement({ - transformHandleType: pointerDownState.resize.handleType, - }); + setCursor( + this.canvas, + getCursorForResizingElement({ + transformHandleType: pointerDownState.resize.handleType, + }), + ); pointerDownState.resize.isResizing = true; pointerDownState.resize.offset = tupleToCoors( getResizeOffsetXY( @@ -2624,7 +2633,7 @@ class App extends React.Component { insertAtParentCenter: !event.altKey, }); - resetCursor(); + resetCursor(this.canvas); if (!this.state.elementLocked) { this.setState({ elementType: "selection", @@ -2681,7 +2690,7 @@ class App extends React.Component { mutateElement(multiElement, { lastCommittedPoint: multiElement.points[multiElement.points.length - 1], }); - document.documentElement.style.cursor = CURSOR_TYPE.POINTER; + setCursor(this.canvas, CURSOR_TYPE.POINTER); } else { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, @@ -3216,7 +3225,7 @@ class App extends React.Component { } this.setState({ suggestedBindings: [], startBoundElement: null }); if (!elementLocked && elementType !== "draw") { - resetCursor(); + resetCursor(this.canvas); this.setState((prevState) => ({ draggingElement: null, elementType: "selection", @@ -3387,7 +3396,7 @@ class App extends React.Component { } if (!elementLocked && elementType !== "draw") { - resetCursor(); + resetCursor(this.canvas); this.setState({ draggingElement: null, suggestedBindings: [], diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 103b0ef846c4..5c8adcf727f3 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -516,6 +516,7 @@ const LayerUI = ({ {heading} 1) { if (transformHandleType === "rotation") { diff --git a/src/utils.ts b/src/utils.ts index 2532aefcca21..cd6bc00ce684 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -160,15 +160,29 @@ export const removeSelection = () => { export const distance = (x: number, y: number) => Math.abs(x - y); -export const resetCursor = () => { - document.documentElement.style.cursor = ""; +export const resetCursor = (canvas: HTMLCanvasElement | null) => { + if (canvas) { + canvas.style.cursor = ""; + } +}; + +export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => { + if (canvas) { + canvas.style.cursor = cursor; + } }; -export const setCursorForShape = (shape: string) => { +export const setCursorForShape = ( + canvas: HTMLCanvasElement | null, + shape: string, +) => { + if (!canvas) { + return; + } if (shape === "selection") { - resetCursor(); + resetCursor(canvas); } else { - document.documentElement.style.cursor = CURSOR_TYPE.CROSSHAIR; + canvas.style.cursor = CURSOR_TYPE.CROSSHAIR; } };