Просмотр исходного кода

feat: Scroll using PageUp and PageDown (#6038)

* feat: Scroll using PageUp and PageDown

* support x-axis via `shift` & enable in viewMode

* tweak test

Co-authored-by: dwelle <luzar.david@gmail.com>
DanielJGeiger 2 лет назад
Родитель
Сommit
fdd8552637
5 измененных файлов с 92 добавлено и 7 удалено
  1. 20 6
      src/components/App.tsx
  2. 8 0
      src/components/HelpDialog.tsx
  3. 2 0
      src/keys.ts
  4. 3 1
      src/locales/en.json
  5. 59 0
      src/tests/scroll.test.tsx

+ 20 - 6
src/components/App.tsx

@@ -2008,6 +2008,20 @@ class App extends React.Component<AppProps, AppState> {
         return;
       }
 
+      if (event.key === KEYS.PAGE_UP || event.key === KEYS.PAGE_DOWN) {
+        let offset =
+          (event.shiftKey ? this.state.width : this.state.height) /
+          this.state.zoom.value;
+        if (event.key === KEYS.PAGE_DOWN) {
+          offset = -offset;
+        }
+        if (event.shiftKey) {
+          this.setState((state) => ({ scrollX: state.scrollX + offset }));
+        } else {
+          this.setState((state) => ({ scrollY: state.scrollY + offset }));
+        }
+      }
+
       if (this.actionManager.handleKeyDown(event)) {
         return;
       }
@@ -2030,12 +2044,6 @@ class App extends React.Component<AppProps, AppState> {
             ? ELEMENT_SHIFT_TRANSLATE_AMOUNT
             : ELEMENT_TRANSLATE_AMOUNT);
 
-        const selectedElements = getSelectedElements(
-          this.scene.getNonDeletedElements(),
-          this.state,
-          true,
-        );
-
         let offsetX = 0;
         let offsetY = 0;
 
@@ -2049,6 +2057,12 @@ class App extends React.Component<AppProps, AppState> {
           offsetY = step;
         }
 
+        const selectedElements = getSelectedElements(
+          this.scene.getNonDeletedElements(),
+          this.state,
+          true,
+        );
+
         selectedElements.forEach((element) => {
           mutateElement(element, {
             x: element.x + offsetX,

+ 8 - 0
src/components/HelpDialog.tsx

@@ -230,6 +230,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
               label={t("helpDialog.zoomToSelection")}
               shortcuts={["Shift+2"]}
             />
+            <Shortcut
+              label={t("helpDialog.movePageUpDown")}
+              shortcuts={["PgUp/PgDn"]}
+            />
+            <Shortcut
+              label={t("helpDialog.movePageLeftRight")}
+              shortcuts={["Shift+PgUp/PgDn"]}
+            />
             <Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
             <Shortcut
               label={t("buttons.zenMode")}

+ 2 - 0
src/keys.ts

@@ -29,6 +29,8 @@ export const KEYS = {
   ARROW_LEFT: "ArrowLeft",
   ARROW_RIGHT: "ArrowRight",
   ARROW_UP: "ArrowUp",
+  PAGE_UP: "PageUp",
+  PAGE_DOWN: "PageDown",
   BACKSPACE: "Backspace",
   ALT: "Alt",
   CTRL_OR_CMD: isDarwin ? "metaKey" : "ctrlKey",

+ 3 - 1
src/locales/en.json

@@ -312,7 +312,9 @@
     "view": "View",
     "zoomToFit": "Zoom to fit all elements",
     "zoomToSelection": "Zoom to selection",
-    "toggleElementLock": "Lock/unlock selection"
+    "toggleElementLock": "Lock/unlock selection",
+    "movePageUpDown": "Move page up/down",
+    "movePageLeftRight": "Move page left/right"
   },
   "clearCanvasDialog": {
     "title": "Clear canvas"

+ 59 - 0
src/tests/scroll.test.tsx

@@ -6,6 +6,9 @@ import {
 } from "./test-utils";
 import { Excalidraw } from "../packages/excalidraw/index";
 import { API } from "./helpers/api";
+import { Keyboard } from "./helpers/ui";
+import { KEYS } from "../keys";
+import ExcalidrawApp from "../excalidraw-app";
 
 const { h } = window;
 
@@ -50,4 +53,60 @@ describe("appState", () => {
     });
     restoreOriginalGetBoundingClientRect();
   });
+
+  it("moving by page up/down/left/right", async () => {
+    mockBoundingClientRect();
+    await render(<ExcalidrawApp />, {});
+
+    const scrollTest = () => {
+      const initialScrollY = h.state.scrollY;
+      const initialScrollX = h.state.scrollX;
+      const pageStepY = h.state.height / h.state.zoom.value;
+      const pageStepX = h.state.width / h.state.zoom.value;
+      // Assert the following assertions have meaning
+      expect(pageStepY).toBeGreaterThan(0);
+      expect(pageStepX).toBeGreaterThan(0);
+      // Assert we scroll up
+      Keyboard.keyPress(KEYS.PAGE_UP);
+      expect(h.state.scrollY).toBe(initialScrollY + pageStepY);
+      // x-axis unchanged
+      expect(h.state.scrollX).toBe(initialScrollX);
+
+      // Assert we scroll down
+      Keyboard.keyPress(KEYS.PAGE_DOWN);
+      Keyboard.keyPress(KEYS.PAGE_DOWN);
+      expect(h.state.scrollY).toBe(initialScrollY - pageStepY);
+      // x-axis unchanged
+      expect(h.state.scrollX).toBe(initialScrollX);
+
+      // Assert we scroll left
+      Keyboard.withModifierKeys({ shift: true }, () => {
+        Keyboard.keyPress(KEYS.PAGE_UP);
+      });
+      expect(h.state.scrollX).toBe(initialScrollX + pageStepX);
+      // y-axis unchanged
+      expect(h.state.scrollY).toBe(initialScrollY - pageStepY);
+
+      // Assert we scroll right
+      Keyboard.withModifierKeys({ shift: true }, () => {
+        Keyboard.keyPress(KEYS.PAGE_DOWN);
+        Keyboard.keyPress(KEYS.PAGE_DOWN);
+      });
+      expect(h.state.scrollX).toBe(initialScrollX - pageStepX);
+      // y-axis unchanged
+      expect(h.state.scrollY).toBe(initialScrollY - pageStepY);
+    };
+
+    const zoom = h.state.zoom.value;
+    // Assert we scroll properly when zoomed in
+    h.setState({ zoom: { value: (zoom * 1.1) as typeof zoom } });
+    scrollTest();
+    // Assert we scroll properly when zoomed out
+    h.setState({ zoom: { value: (zoom * 0.9) as typeof zoom } });
+    scrollTest();
+    // Assert we scroll properly with normal zoom
+    h.setState({ zoom: { value: zoom } });
+    scrollTest();
+    restoreOriginalGetBoundingClientRect();
+  });
 });