Repository: SwitchbackTech/compass
Pull request #381 (2025-04-28 02:12:05)

🐛 Fix: Someday event form not saving when pressing enter key on keyboard
## Description
Closes https://github.com/SwitchbackTech/compass/issues/344

- Fix someday event form not saving when pressing the enter key
- Match the hotkey listener implementation with the Grid Event Form component for consistency
- Implement unit tests


Patch:
diff --git a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx
index 4416037a..f1a6b637 100644
--- a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx
+++ b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx
@@ -1,4 +1,6 @@
 import React, { KeyboardEvent, MouseEvent, useRef } from "react";
+import { useHotkeys } from "react-hotkeys-hook";
+import { OptionsOrDependencyArray } from "react-hotkeys-hook/dist/types";
 import { toast } from "react-toastify";
 import "react-toastify/dist/ReactToastify.css";
 import { Key } from "ts-key-enum";
@@ -18,6 +20,10 @@ import {
 import { FormProps, SetEventFormField } from "@web/views/Forms/EventForm/types";
 import { RepeatSection } from "../EventForm/RepeatSection";
 
+const hotkeysOptions: OptionsOrDependencyArray = {
+  enableOnFormTags: ["input"],
+};
+
 export const SomedayEventForm: React.FC<FormProps> = ({
   event,
   onClose,
@@ -33,7 +39,7 @@ export const SomedayEventForm: React.FC<FormProps> = ({
   const origRecurrence = useRef(event?.recurrence).current;
 
   const ignoreDelete = (e: KeyboardEvent) => {
-    if (e.key === Key.Backspace || e.key == Key.Delete) {
+    if (e.key === Key.Backspace) {
       e.stopPropagation();
     }
   };
@@ -73,7 +79,15 @@ export const SomedayEventForm: React.FC<FormProps> = ({
   };
 
   const onKeyDown = (e: KeyboardEvent<HTMLFormElement>) => {
-    if (e.key === Key.Backspace || e.key == Key.Delete) {
+    if (e.key === Key.Backspace) {
+      e.stopPropagation();
+    }
+  };
+
+  useHotkeys(
+    "delete",
+    () => {
+      console.log("delete");
       const confirmed = window.confirm(
         `Delete ${event.title || "this event"}?`,
       );
@@ -82,15 +96,17 @@ export const SomedayEventForm: React.FC<FormProps> = ({
         onDelete();
         onClose();
       }
-    }
+    },
+    hotkeysOptions,
+  );
 
-    if (e.key === Key.Enter) {
-      e.stopPropagation();
-      if (e.metaKey) {
-        _onSubmit();
-      }
-    }
-  };
+  useHotkeys(
+    "enter",
+    () => {
+      _onSubmit();
+    },
+    hotkeysOptions,
+  );
 
   const onSetEventField: SetEventFormField = (field) => {
     const newEvent = { ...event, ...field };


Test patch:
diff --git a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.test.tsx b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.test.tsx
new file mode 100644
index 00000000..b14e6fd4
--- /dev/null
+++ b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.test.tsx
@@ -0,0 +1,133 @@
+import React, { act } from "react";
+import { toast } from "react-toastify";
+import "@testing-library/jest-dom/extend-expect";
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { Priorities } from "@core/constants/core.constants";
+import { Schema_Event } from "@core/types/event.types";
+import { render } from "@web/__tests__/__mocks__/mock.render";
+import { getSomedayEventsSlice } from "@web/ducks/events/slices/someday.slice";
+import { SomedayEventForm } from "./SomedayEventForm";
+
+jest.mock("react-toastify", () => ({
+  toast: jest.fn(),
+}));
+
+const mockDispatch = jest.fn();
+jest.mock("@web/store/store.hooks", () => ({
+  ...jest.requireActual("@web/store/store.hooks"),
+  useAppDispatch: () => mockDispatch,
+}));
+
+const mockConfirm = jest.spyOn(window, "confirm");
+
+const sampleSomedayEvent: Schema_Event = {
+  _id: "someday123",
+  title: "Test Someday Event for Hotkey",
+  description: "Practice hotkeys",
+  priority: Priorities.UNASSIGNED,
+};
+
+const mockOnClose = jest.fn();
+const mockOnSubmit = jest.fn();
+const mockSetEvent = jest.fn();
+
+describe("SomedayEventForm Hotkeys", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  test("should call onSubmit when enter keyboard shortcut is used", async () => {
+    render(
+      <div>
+        <SomedayEventForm
+          event={sampleSomedayEvent}
+          onClose={mockOnClose}
+          onSubmit={mockOnSubmit}
+          setEvent={mockSetEvent}
+        />
+      </div>,
+    );
+
+    // Ensure the form is rendered (good sanity check)
+    expect(screen.getByRole("form")).toBeInTheDocument();
+
+    await act(async () => {
+      await userEvent.keyboard("{Enter}");
+    });
+
+    expect(mockOnSubmit).toHaveBeenCalledTimes(1);
+    expect(mockOnSubmit).toHaveBeenCalledWith(sampleSomedayEvent);
+
+    expect(mockOnClose).not.toHaveBeenCalled();
+    expect(mockDispatch).not.toHaveBeenCalled();
+    expect(toast).not.toHaveBeenCalled();
+    expect(mockConfirm).not.toHaveBeenCalled();
+  });
+
+  test("should trigger delete flow when delete keyboard shortcut is used and confirmed", async () => {
+    render(
+      <div>
+        <SomedayEventForm
+          event={sampleSomedayEvent}
+          onClose={mockOnClose}
+          onSubmit={mockOnSubmit}
+          setEvent={mockSetEvent}
+        />
+      </div>,
+    );
+
+    expect(screen.getByRole("form")).toBeInTheDocument();
+
+    // Explicitly confirm deletion
+    mockConfirm.mockReturnValue(true);
+
+    await act(async () => {
+      await userEvent.keyboard("{Delete}");
+    });
+
+    expect(mockConfirm).toHaveBeenCalledTimes(1);
+    expect(mockConfirm).toHaveBeenCalledWith(
+      `Delete ${sampleSomedayEvent.title}?`,
+    );
+    expect(mockDispatch).toHaveBeenCalledTimes(1);
+    expect(mockDispatch).toHaveBeenCalledWith(
+      getSomedayEventsSlice.actions.delete({ _id: sampleSomedayEvent._id }),
+    );
+    expect(toast).toHaveBeenCalledTimes(1);
+    expect(toast).toHaveBeenCalledWith(`Deleted ${sampleSomedayEvent.title}`);
+
+    expect(mockOnSubmit).not.toHaveBeenCalled();
+  });
+
+  test("should not trigger delete flow when delete keyboard shortcut is used and cancelled", async () => {
+    render(
+      <div>
+        <SomedayEventForm
+          event={sampleSomedayEvent}
+          onClose={mockOnClose}
+          onSubmit={mockOnSubmit}
+          setEvent={mockSetEvent}
+        />
+      </div>,
+    );
+
+    expect(screen.getByRole("form")).toBeInTheDocument();
+
+    mockConfirm.mockReturnValue(false);
+
+    await act(async () => {
+      await userEvent.keyboard("{Delete}");
+    });
+
+    expect(mockConfirm).toHaveBeenCalledTimes(1);
+    expect(mockConfirm).toHaveBeenCalledWith(
+      `Delete ${sampleSomedayEvent.title}?`,
+    );
+
+    expect(mockDispatch).not.toHaveBeenCalled();
+    expect(toast).not.toHaveBeenCalled();
+    expect(mockOnClose).not.toHaveBeenCalled();
+    expect(mockOnSubmit).not.toHaveBeenCalled();
+  });
+});