Browse Source

fix: improve line creation ux on touch screens (#9740)

* fix: awkward point adding and removing on touch device

* feat: move finalize to next to last point

* feat: on touch screen, click would create a default line/arrow

* fix: make default adaptive to zoom

* fix: increase padding to avoid cutoffs

* refactor: simplify

* fix: only use bigger padding when needed

* center arrow horizontally on pointer

* increase min drag distance before we start 2-point-arrow-drag-creating

* do not render 0-width arrow while creating

* dead code

* fix tests

* fix: remove redundant code

* do not enter line editor on creation

---------

Co-authored-by: dwelle <[email protected]>
Ryan Di 1 month ago
parent
commit
e5e07260c6

+ 1 - 0
packages/common/src/constants.ts

@@ -36,6 +36,7 @@ export const APP_NAME = "Excalidraw";
 // (happens a lot with fast clicks with the text tool)
 export const TEXT_AUTOWRAP_THRESHOLD = 36; // px
 export const DRAGGING_THRESHOLD = 10; // px
+export const MINIMUM_ARROW_SIZE = 20; // px
 export const LINE_CONFIRM_THRESHOLD = 8; // px
 export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
 export const ELEMENT_TRANSLATE_AMOUNT = 1;

+ 5 - 0
packages/element/src/renderElement.ts

@@ -106,6 +106,11 @@ const getCanvasPadding = (element: ExcalidrawElement) => {
       return element.strokeWidth * 12;
     case "text":
       return element.fontSize / 2;
+    case "arrow":
+      if (element.endArrowhead || element.endArrowhead) {
+        return 40;
+      }
+      return 20;
     default:
       return 20;
   }

+ 1 - 5
packages/excalidraw/actions/actionFinalize.tsx

@@ -154,11 +154,7 @@ export const actionFinalize = register({
 
     if (element) {
       // pen and mouse have hover
-      if (
-        appState.multiElement &&
-        element.type !== "freedraw" &&
-        appState.lastPointerDownWith !== "touch"
-      ) {
+      if (appState.multiElement && element.type !== "freedraw") {
         const { points, lastCommittedPoint } = element;
         if (
           !lastCommittedPoint ||

+ 0 - 12
packages/excalidraw/components/Actions.tsx

@@ -505,15 +505,3 @@ export const ExitZenModeAction = ({
     {t("buttons.exitZenMode")}
   </button>
 );
-
-export const FinalizeAction = ({
-  renderAction,
-  className,
-}: {
-  renderAction: ActionManager["renderAction"];
-  className?: string;
-}) => (
-  <div className={`finalize-button ${className}`}>
-    {renderAction("finalize", { size: "small" })}
-  </div>
-);

+ 51 - 19
packages/excalidraw/components/App.tsx

@@ -100,6 +100,7 @@ import {
   randomInteger,
   CLASSES,
   Emitter,
+  MINIMUM_ARROW_SIZE,
 } from "@excalidraw/common";
 
 import {
@@ -8162,7 +8163,9 @@ class App extends React.Component<AppProps, AppState> {
           pointDistance(
             pointFrom(pointerCoords.x, pointerCoords.y),
             pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
-          ) < DRAGGING_THRESHOLD
+          ) *
+            this.state.zoom.value <
+          MINIMUM_ARROW_SIZE
         ) {
           return;
         }
@@ -9113,25 +9116,54 @@ class App extends React.Component<AppProps, AppState> {
           this.state,
         );
 
-        if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
-          this.scene.mutateElement(
-            newElement,
-            {
-              points: [
-                ...newElement.points,
-                pointFrom<LocalPoint>(
-                  pointerCoords.x - newElement.x,
-                  pointerCoords.y - newElement.y,
-                ),
-              ],
-            },
-            { informMutation: false, isDragging: false },
-          );
+        const dragDistance =
+          pointDistance(
+            pointFrom(pointerCoords.x, pointerCoords.y),
+            pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
+          ) * this.state.zoom.value;
 
-          this.setState({
-            multiElement: newElement,
-            newElement,
-          });
+        if (
+          (!pointerDownState.drag.hasOccurred ||
+            dragDistance < MINIMUM_ARROW_SIZE) &&
+          newElement &&
+          !multiElement
+        ) {
+          if (this.device.isTouchScreen) {
+            const FIXED_DELTA_X = Math.min(
+              (this.state.width * 0.7) / this.state.zoom.value,
+              100,
+            );
+
+            this.scene.mutateElement(
+              newElement,
+              {
+                x: newElement.x - FIXED_DELTA_X / 2,
+                points: [
+                  pointFrom<LocalPoint>(0, 0),
+                  pointFrom<LocalPoint>(FIXED_DELTA_X, 0),
+                ],
+              },
+              { informMutation: false, isDragging: false },
+            );
+
+            this.actionManager.executeAction(actionFinalize);
+          } else {
+            const dx = pointerCoords.x - newElement.x;
+            const dy = pointerCoords.y - newElement.y;
+
+            this.scene.mutateElement(
+              newElement,
+              {
+                points: [...newElement.points, pointFrom<LocalPoint>(dx, dy)],
+              },
+              { informMutation: false, isDragging: false },
+            );
+
+            this.setState({
+              multiElement: newElement,
+              newElement,
+            });
+          }
         } else if (pointerDownState.drag.hasOccurred && !multiElement) {
           if (
             isBindingEnabled(this.state) &&

+ 1 - 20
packages/excalidraw/components/footer/Footer.tsx

@@ -2,13 +2,7 @@ import clsx from "clsx";
 
 import { actionShortcuts } from "../../actions";
 import { useTunnels } from "../../context/tunnels";
-import {
-  ExitZenModeAction,
-  FinalizeAction,
-  UndoRedoActions,
-  ZoomActions,
-} from "../Actions";
-import { useDevice } from "../App";
+import { ExitZenModeAction, UndoRedoActions, ZoomActions } from "../Actions";
 import { HelpButton } from "../HelpButton";
 import { Section } from "../Section";
 import Stack from "../Stack";
@@ -29,10 +23,6 @@ const Footer = ({
 }) => {
   const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels();
 
-  const device = useDevice();
-  const showFinalize =
-    !appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
-
   return (
     <footer
       role="contentinfo"
@@ -60,15 +50,6 @@ const Footer = ({
                 })}
               />
             )}
-            {showFinalize && (
-              <FinalizeAction
-                renderAction={actionManager.renderAction}
-                className={clsx("zen-mode-transition", {
-                  "layer-ui__wrapper__footer-left--transition-left":
-                    appState.zenModeEnabled,
-                })}
-              />
-            )}
           </Section>
         </Stack.Col>
       </div>

+ 9 - 1
packages/excalidraw/renderer/renderNewElementScene.ts

@@ -1,6 +1,6 @@
 import { throttleRAF } from "@excalidraw/common";
 
-import { renderElement } from "@excalidraw/element";
+import { isInvisiblySmallElement, renderElement } from "@excalidraw/element";
 
 import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
 
@@ -34,6 +34,14 @@ const _renderNewElementScene = ({
     context.scale(appState.zoom.value, appState.zoom.value);
 
     if (newElement && newElement.type !== "selection") {
+      // e.g. when creating arrows and we're still below the arrow drag distance
+      // threshold
+      // (for now we skip render only with elements while we're creating to be
+      // safe)
+      if (isInvisiblySmallElement(newElement)) {
+        return;
+      }
+
       renderElement(
         newElement,
         elementsMap,

+ 44 - 44
packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -8190,7 +8190,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "link": null,
@@ -8203,7 +8203,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st
             "strokeWidth": 2,
             "type": "rectangle",
             "version": 3,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -8369,7 +8369,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "link": null,
@@ -8382,7 +8382,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac
             "strokeWidth": 2,
             "type": "diamond",
             "version": 3,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -8548,7 +8548,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "link": null,
@@ -8561,7 +8561,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac
             "strokeWidth": 2,
             "type": "ellipse",
             "version": 3,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -8758,7 +8758,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "lastCommittedPoint": null,
@@ -8771,8 +8771,8 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
                 0,
               ],
               [
-                10,
-                10,
+                30,
+                30,
               ],
             ],
             "roughness": 1,
@@ -8786,7 +8786,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
             "strokeWidth": 2,
             "type": "arrow",
             "version": 4,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -8982,7 +8982,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "lastCommittedPoint": null,
@@ -8995,8 +8995,8 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
                 0,
               ],
               [
-                10,
-                10,
+                30,
+                30,
               ],
             ],
             "polygon": false,
@@ -9009,7 +9009,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
             "strokeWidth": 2,
             "type": "line",
             "version": 4,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -9167,12 +9167,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "lastCommittedPoint": [
-              10,
-              10,
+              30,
+              30,
             ],
             "link": null,
             "locked": false,
@@ -9183,12 +9183,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
                 0,
               ],
               [
-                10,
-                10,
+                30,
+                30,
               ],
               [
-                10,
-                10,
+                30,
+                30,
               ],
             ],
             "pressures": [
@@ -9204,7 +9204,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
             "strokeWidth": 2,
             "type": "freedraw",
             "version": 4,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -9401,7 +9401,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "lastCommittedPoint": null,
@@ -9414,8 +9414,8 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
                 0,
               ],
               [
-                10,
-                10,
+                30,
+                30,
               ],
             ],
             "roughness": 1,
@@ -9429,7 +9429,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
             "strokeWidth": 2,
             "type": "arrow",
             "version": 4,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -9595,7 +9595,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "link": null,
@@ -9608,7 +9608,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac
             "strokeWidth": 2,
             "type": "diamond",
             "version": 3,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -9804,7 +9804,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "lastCommittedPoint": null,
@@ -9817,8 +9817,8 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
                 0,
               ],
               [
-                10,
-                10,
+                30,
+                30,
               ],
             ],
             "polygon": false,
@@ -9831,7 +9831,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
             "strokeWidth": 2,
             "type": "line",
             "version": 4,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -9997,7 +9997,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "link": null,
@@ -10010,7 +10010,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac
             "strokeWidth": 2,
             "type": "ellipse",
             "version": 3,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -10168,12 +10168,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "lastCommittedPoint": [
-              10,
-              10,
+              30,
+              30,
             ],
             "link": null,
             "locked": false,
@@ -10184,12 +10184,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
                 0,
               ],
               [
-                10,
-                10,
+                30,
+                30,
               ],
               [
-                10,
-                10,
+                30,
+                30,
               ],
             ],
             "pressures": [
@@ -10205,7 +10205,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
             "strokeWidth": 2,
             "type": "freedraw",
             "version": 4,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },
@@ -10371,7 +10371,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st
             "fillStyle": "solid",
             "frameId": null,
             "groupIds": [],
-            "height": 10,
+            "height": 30,
             "index": "a0",
             "isDeleted": false,
             "link": null,
@@ -10384,7 +10384,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st
             "strokeWidth": 2,
             "type": "rectangle",
             "version": 3,
-            "width": 10,
+            "width": 30,
             "x": 10,
             "y": 10,
           },

+ 1 - 1
packages/excalidraw/tests/regressionTests.test.tsx

@@ -150,7 +150,7 @@ describe("regression tests", () => {
         expect(h.state.activeTool.type).toBe(shape);
 
         mouse.down(10, 10);
-        mouse.up(10, 10);
+        mouse.up(30, 30);
 
         if (shouldSelect) {
           expect(API.getSelectedElement().type).toBe(shape);