Repository: SwitchbackTech/compass (ts)

Issue:
Runtime error when dragging an all-day draft
### Where did this happen?

Local and prod


### Expected Behavior

User should be able to drag an all day event and all day draft without any errors

### Current Behavior

After dragging an all day draft and moving the mouse, an undefined error appears

https://github.com/user-attachments/assets/4237e6c5-f23b-4dbb-8f51-73b1a76b4cc1

```
Uncaught runtime errors:
×
ERROR
Cannot read properties of null (reading 'endDate')
TypeError: Cannot read properties of null (reading 'endDate')
    at http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:64886:81
    at basicStateReducer (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:104348:41)
    at updateReducer (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:104472:22)
    at updateState (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:104812:10)
    at Object.useState (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:105723:16)
    at useState (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:135255:21)
    at useDraftState (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:64959:76)
    at DraftProvider (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:64135:80)
    at renderWithHooks (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:104113:18)
    at updateFunctionComponent (http://localhost:9080/main.125931896040c620ea19.js?42544fdfb82082812bb9:108239:20)
```

### Steps to Reproduce

1. Create all day event
2. Click event to start a draft
3. Form will open
4. Click event rectangle. Do not hold down the cursor. 
5. Wait, without moving the mouse
6. Cursor will turn from `default` to `move`.
7. Move cursor to move event to another day
8. Release
9. Move mouse over the main grid
10. See error

### Root Cause
When mousing up after triggering a drag for an all-day event, the AllDay event is immediately triggering a new drag (`AllDayEvents.tsx:36`).

On the second time clicking the event, it's not waiting. It's immediately putting it into the drag state (even if the cursor isn't changing)


### Possible Solution
This is likely caused by the cursor timer. It seems like it's handling a click (mouse down + mouse up) the same as a click and hold (keeping the mouse down )

Probably need to update the state cleanup after dragging. This isn't happening for grid events, so reference how it works over there

When form opens, need to stop dragging

### Context


Pull request:
🐛 Fix: Runtime error when dragging all-day draft
Closes #286 

### After
https://github.com/user-attachments/assets/7fe92a1d-b565-47ff-bcdd-5afd68e6225b

This pull request includes several changes to the calendar event handling and testing in the `packages/web` directory. The most important changes involve refactoring the event mouse handling logic to differentiate between click and drag actions, updating related components, and adding new tests to ensure the functionality works as expected.

### Event Mouse Handling Refactor:
* Introduced `useGridEventMouseDown` hook to differentiate between click and drag actions based on mouse behavior. 
- This is a hook that differentiates between click and drag actions based on mouse behavior:
  - Quick press and release triggers a click
  - Hold for delay or move beyond threshold triggers drag
  - When form is open, only allows drag if mouse is still held down
- This hook replaces the previous `useGridEventMouseHold` hook.

* [`packages/web/src/views/Calendar/hooks/grid/useGridEventMouseDown.test.ts`](diffhunk://#diff-396cde65032a8f863888ebb0ec156b24b62685459f6e584e4121e41f5d675ee2R1-R158): Added new tests for the `useGridEventMouseDown` hook to verify its behavior for different mouse actions and scenarios.

### Test File Changes:
* [`packages/web/src/views/Calendar/Calendar.form.test.tsx`](diffhunk://#diff-b6aef46bbadad79c93104c7aeb8ee2b17daa72b19b229cc2ccd7f92d0eba4dccL4-R4): Removed commented-out tests and unnecessary helper functions, and made minor adjustments to existing tests. [[1]](diffhunk://#diff-b6aef46bbadad79c93104c7aeb8ee2b17daa72b19b229cc2ccd7f92d0eba4dccL4-R4) [[2]](diffhunk://#diff-b6aef46bbadad79c93104c7aeb8ee2b17daa72b19b229cc2ccd7f92d0eba4dccL15-L62) [[3]](diffhunk://#diff-b6aef46bbadad79c93104c7aeb8ee2b17daa72b19b229cc2ccd7f92d0eba4dccL104-L118)

### Additional Changes:
* [`packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.ts`](diffhunk://#diff-2980848dde51e606e5af442ee101c80728fd46fe12088fa8ccdbc545bd0182d3L43): Adjusted logic to ensure that the draft state is correctly managed during dragging and resizing events. [[1]](diffhunk://#diff-2980848dde51e606e5af442ee101c80728fd46fe12088fa8ccdbc545bd0182d3L43) [[2]](diffhunk://#diff-2980848dde51e606e5af442ee101c80728fd46fe12088fa8ccdbc545bd0182d3L61-L74)
* [`packages/web/src/views/Calendar/hooks/grid/useDragEventSmartScroll.ts`](diffhunk://#diff-eb1672ac004e57d40f33399ed3e526f69e998f2b20858c2ff69d2f3c7e64deacL27-R27): Added condition to handle all-day events during drag operations.



Patch:
diff --git a/packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx b/packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx
index b801f005..47235464 100644
--- a/packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx
+++ b/packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx
@@ -3,7 +3,7 @@ import { FloatingFocusManager } from "@floating-ui/react";
 import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants";
 import { Categories_Event } from "@core/types/event.types";
 import { Schema_GridEvent } from "@web/common/types/web.event.types";
-import { useGridEventMouseHold } from "@web/views/Calendar/hooks/grid/useGridEventMouseHold";
+import { useGridEventMouseDown } from "@web/views/Calendar/hooks/grid/useGridEventMouseDown";
 import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { EventForm } from "@web/views/Forms/EventForm";
@@ -35,9 +35,16 @@ export const GridDraft: FC<Props> = ({ measurements, weekProps }) => {
     actions.convert(start, end);
   };
 
-  const { onMouseDown } = useGridEventMouseHold((event) => {
+  const handleClick = () => {};
+  const handleDrag = () => {
     setIsDragging(true);
-  }, Categories_Event.TIMED);
+  };
+
+  const { onMouseDown } = useGridEventMouseDown(
+    Categories_Event.TIMED,
+    handleClick,
+    handleDrag,
+  );
 
   if (!draft) return null;
 
diff --git a/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts b/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts
index e4e5a890..d8c19f19 100644
--- a/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts
+++ b/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts
@@ -147,14 +147,12 @@ export const useDraftActions = (
   };
 
   const discard = useCallback(() => {
-    if (draft) {
-      setDraft(null);
-    }
+    reset();
 
     if (reduxDraft || reduxDraftType) {
       dispatch(draftSlice.actions.discard());
     }
-  }, [dispatch, draft, reduxDraft, reduxDraftType]);
+  }, [dispatch, draft, reduxDraft, reduxDraftType, setDraft]);
 
   const drag = useCallback(
     (e: MouseEvent) => {
diff --git a/packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.ts b/packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.ts
index a13288f6..1d69ad4d 100644
--- a/packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.ts
+++ b/packages/web/src/views/Calendar/components/Draft/hooks/effects/useDraftEffects.ts
@@ -40,7 +40,6 @@ export const useDraftEffects = (
   }, [dateBeingChanged, isResizing]);
 
   useEffect(() => {
-    // const isStaleDraft = !isDrafting && (isResizing || isDragging);
     const isStaleDraft = !isDrafting;
     if (isStaleDraft) {
       setDraft(null);
@@ -51,26 +50,28 @@ export const useDraftEffects = (
       setResizeStatus(null);
       setDateBeingChanged(null);
     }
-  }, [isDrafting]);
+  }, [
+    isDrafting,
+    setDateBeingChanged,
+    setDraft,
+    setDragStatus,
+    setIsDragging,
+    setIsFormOpen,
+    setIsResizing,
+    setResizeStatus,
+  ]);
 
   useEffect(() => {
     handleChange();
   }, [handleChange]);
 
   useEffect(() => {
-    if (isDragging) {
+    if (isDragging && draft) {
       setIsFormOpen(false);
-      setDraft((_draft) => {
-        const durationMin = dayjs(_draft.endDate).diff(
-          _draft.startDate,
-          "minutes",
-        );
-
-        setDragStatus({
-          durationMin,
-        });
+      const durationMin = dayjs(draft.endDate).diff(draft.startDate, "minutes");
 
-        return draft;
+      setDragStatus({
+        durationMin,
       });
     }
   }, [isDragging]);
diff --git a/packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvents.tsx b/packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvents.tsx
index dfa8d558..926a1d7e 100644
--- a/packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvents.tsx
+++ b/packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvents.tsx
@@ -2,13 +2,12 @@ import React from "react";
 import { Categories_Event } from "@core/types/event.types";
 import { ID_GRID_EVENTS_ALLDAY } from "@web/common/constants/web.constants";
 import { Schema_GridEvent } from "@web/common/types/web.event.types";
-import { isSomedayEventFormOpen } from "@web/common/utils";
 import { isLeftClick } from "@web/common/utils/mouse/mouse.util";
 import { selectDraftId } from "@web/ducks/events/selectors/draft.selectors";
 import { selectAllDayEvents } from "@web/ducks/events/selectors/event.selectors";
 import { draftSlice } from "@web/ducks/events/slices/draft.slice";
 import { useAppDispatch, useAppSelector } from "@web/store/store.hooks";
-import { useGridEventMouseHold } from "@web/views/Calendar/hooks/grid/useGridEventMouseHold";
+import { useGridEventMouseDown } from "@web/views/Calendar/hooks/grid/useGridEventMouseDown";
 import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { AllDayEventMemo } from "./AllDayEvent";
@@ -28,18 +27,30 @@ export const AllDayEvents = ({
   const draftId = useAppSelector(selectDraftId);
   const dispatch = useAppDispatch();
 
-  const { onMouseDown } = useGridEventMouseHold((event) => {
-    if (isSomedayEventFormOpen()) {
-      dispatch(draftSlice.actions.discard());
-    }
+  const handleClick = (event: Schema_GridEvent) => {
+    dispatch(
+      draftSlice.actions.start({
+        activity: "gridClick",
+        event,
+        eventType: Categories_Event.ALLDAY,
+      }),
+    );
+  };
 
+  const handleDrag = (event: Schema_GridEvent) => {
     dispatch(
       draftSlice.actions.startDragging({
         category: Categories_Event.ALLDAY,
         event,
       }),
     );
-  }, Categories_Event.ALLDAY);
+  };
+
+  const { onMouseDown } = useGridEventMouseDown(
+    Categories_Event.ALLDAY,
+    handleClick,
+    handleDrag,
+  );
 
   return (
     <StyledEvents id={ID_GRID_EVENTS_ALLDAY}>
diff --git a/packages/web/src/views/Calendar/components/Grid/MainGrid/MainGridEvents.tsx b/packages/web/src/views/Calendar/components/Grid/MainGrid/MainGridEvents.tsx
index 6e2bf7d6..ea92ed26 100644
--- a/packages/web/src/views/Calendar/components/Grid/MainGrid/MainGridEvents.tsx
+++ b/packages/web/src/views/Calendar/components/Grid/MainGrid/MainGridEvents.tsx
@@ -2,13 +2,12 @@ import React from "react";
 import { Categories_Event } from "@core/types/event.types";
 import { ID_GRID_EVENTS_TIMED } from "@web/common/constants/web.constants";
 import { Schema_GridEvent } from "@web/common/types/web.event.types";
-import { isEventFormOpen } from "@web/common/utils";
 import { adjustOverlappingEvents } from "@web/common/utils/overlap/overlap";
 import { selectDraftId } from "@web/ducks/events/selectors/draft.selectors";
 import { selectGridEvents } from "@web/ducks/events/selectors/event.selectors";
 import { draftSlice } from "@web/ducks/events/slices/draft.slice";
 import { useAppDispatch, useAppSelector } from "@web/store/store.hooks";
-import { useGridEventMouseHold } from "@web/views/Calendar/hooks/grid/useGridEventMouseHold";
+import { useGridEventMouseDown } from "@web/views/Calendar/hooks/grid/useGridEventMouseDown";
 import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { GridEventMemo } from "../../Event/Grid/GridEvent/GridEvent";
@@ -27,34 +26,40 @@ export const MainGridEvents = ({ measurements, weekProps }: Props) => {
   const adjustedEvents = adjustOverlappingEvents(timedEvents);
   const category = Categories_Event.TIMED;
 
-  const { onMouseDown } = useGridEventMouseHold((event) => {
-    if (isEventFormOpen()) {
-      dispatch(
-        draftSlice.actions.swap({ event, category: Categories_Event.TIMED }),
-      );
-      return;
-    }
-    editTimedEvent(event);
-  }, Categories_Event.TIMED);
+  const handleClick = (event: Schema_GridEvent) => {
+    dispatch(
+      draftSlice.actions.start({
+        activity: "gridClick",
+        event,
+        eventType: category,
+      }),
+    );
+  };
 
-  const resizeTimedEvent = (
-    event: Schema_GridEvent,
-    dateToChange: "startDate" | "endDate",
-  ) => {
+  const handleDrag = (event: Schema_GridEvent) => {
     dispatch(
-      draftSlice.actions.startResizing({
+      draftSlice.actions.startDragging({
         category,
         event,
-        dateToChange,
       }),
     );
   };
 
-  const editTimedEvent = (event: Schema_GridEvent) => {
+  const { onMouseDown } = useGridEventMouseDown(
+    Categories_Event.TIMED,
+    handleClick,
+    handleDrag,
+  );
+
+  const resizeTimedEvent = (
+    event: Schema_GridEvent,
+    dateToChange: "startDate" | "endDate",
+  ) => {
     dispatch(
-      draftSlice.actions.startDragging({
+      draftSlice.actions.startResizing({
         category,
         event,
+        dateToChange,
       }),
     );
   };
diff --git a/packages/web/src/views/Calendar/hooks/grid/useDragEventSmartScroll.ts b/packages/web/src/views/Calendar/hooks/grid/useDragEventSmartScroll.ts
index 1706146f..54648294 100644
--- a/packages/web/src/views/Calendar/hooks/grid/useDragEventSmartScroll.ts
+++ b/packages/web/src/views/Calendar/hooks/grid/useDragEventSmartScroll.ts
@@ -13,7 +13,7 @@ export const useDragEventSmartScroll = (
 
   useEffect(() => {
     if (!state.isDragging) return;
-    if (state.draft?.isAllDay) return;
+    if (state.draft?.isAllDay !== false) return;
 
     const updateMousePosition = (event: MouseEvent) => {
       setMousePosition({ x: event.clientX, y: event.clientY });
@@ -24,15 +24,16 @@ export const useDragEventSmartScroll = (
     return () => {
       window.removeEventListener("mousemove", updateMousePosition);
     };
-  }, [state.isDragging]);
+  }, [state.draft?.isAllDay, state.isDragging]);
 
   useEffect(() => {
     if (!mainGridRef.current) return;
-
     const container = mainGridRef.current;
 
     const scrollIfNeeded = () => {
+      if (!state.isDragging) return;
       if (!container) return;
+      if (state.draft?.isAllDay !== false) return;
 
       const { top, bottom } = container.getBoundingClientRect();
       const { y } = mousePosition;
@@ -67,6 +68,5 @@ export const useDragEventSmartScroll = (
         scrollRef.current = null;
       }
     };
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [mousePosition]);
 };
diff --git a/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseDown.ts b/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseDown.ts
new file mode 100644
index 00000000..b62cb084
--- /dev/null
+++ b/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseDown.ts
@@ -0,0 +1,128 @@
+import { MouseEvent as ReactMouseEvent, useRef } from "react";
+import { Categories_Event } from "@core/types/event.types";
+import {
+  ID_GRID_ALLDAY_ROW,
+  ID_GRID_MAIN,
+} from "@web/common/constants/web.constants";
+import { Schema_GridEvent } from "@web/common/types/web.event.types";
+import { isEventFormOpen } from "@web/common/utils";
+import { getElemById } from "@web/common/utils/grid.util";
+
+export const GRID_EVENT_MOUSE_HOLD_DELAY = 750; // ms
+export const GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD = 25; // pixels
+
+const elementEventTypeMap = {
+  [Categories_Event.TIMED]: ID_GRID_MAIN,
+  [Categories_Event.ALLDAY]: ID_GRID_ALLDAY_ROW,
+};
+
+/**
+ * Hook that differentiates between click and drag actions based on mouse behavior:
+ * - Quick press and release triggers a click
+ * - Hold for delay or move beyond threshold triggers drag
+ * - When form is open, only allows drag if mouse is still held down
+ */
+export const useGridEventMouseDown = (
+  eventType: Categories_Event.TIMED | Categories_Event.ALLDAY,
+  onClick: (event: Schema_GridEvent) => void,
+  onDrag: (event: Schema_GridEvent) => void,
+  delay: number = GRID_EVENT_MOUSE_HOLD_DELAY,
+) => {
+  const timeoutId = useRef<NodeJS.Timeout | null>(null);
+  const mouseMoved = useRef<boolean>(false);
+  const elementId = elementEventTypeMap[eventType];
+
+  const hasExceededMoveThreshold = (
+    currentX: number,
+    currentY: number,
+    initialX: number,
+    initialY: number,
+  ) => {
+    const deltaX = Math.abs(currentX - initialX);
+    const deltaY = Math.abs(currentY - initialY);
+    return (
+      deltaX > GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD ||
+      deltaY > GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD
+    );
+  };
+
+  const cleanup = (
+    element: HTMLElement,
+    onMouseMove: (e: MouseEvent) => void,
+    onMouseUp: () => void,
+  ) => {
+    if (timeoutId.current) {
+      clearTimeout(timeoutId.current);
+    }
+    element.removeEventListener("mousemove", onMouseMove);
+    element.removeEventListener("mouseup", onMouseUp);
+  };
+
+  const handleDrag = (
+    event: Schema_GridEvent,
+    element: HTMLElement,
+    handlers: { onMouseMove: (e: MouseEvent) => void; onMouseUp: () => void },
+  ) => {
+    // If form is open, only allow drag if mouse is still held
+    if (isEventFormOpen()) {
+      const isMouseDown = document.querySelector(":active") !== null;
+      if (!isMouseDown) {
+        cleanup(element, handlers.onMouseMove, handlers.onMouseUp);
+        return;
+      }
+    }
+    mouseMoved.current = true;
+    onDrag(event);
+  };
+
+  const onMouseDown = (e: ReactMouseEvent, event: Schema_GridEvent) => {
+    e.stopPropagation();
+    const element = getElemById(elementId);
+    const initialX = e.clientX;
+    const initialY = e.clientY;
+    mouseMoved.current = false;
+
+    const onMouseMove = (moveEvent: MouseEvent) => {
+      if (
+        hasExceededMoveThreshold(
+          moveEvent.clientX,
+          moveEvent.clientY,
+          initialX,
+          initialY,
+        )
+      ) {
+        mouseMoved.current = true;
+        if (timeoutId.current) {
+          clearTimeout(timeoutId.current);
+        }
+        if (isEventFormOpen()) {
+          cleanup(element, onMouseMove, onMouseUp);
+          return;
+        }
+        onDrag(event);
+        cleanup(element, onMouseMove, onMouseUp);
+      }
+    };
+
+    const onMouseUp = () => {
+      if (!mouseMoved.current && timeoutId.current) {
+        clearTimeout(timeoutId.current);
+        onClick(event);
+      }
+      cleanup(element, onMouseMove, onMouseUp);
+    };
+
+    // Start hold timer
+    timeoutId.current = setTimeout(() => {
+      if (!mouseMoved.current) {
+        handleDrag(event, element, { onMouseMove, onMouseUp });
+      }
+    }, delay);
+
+    // Set up event listeners
+    element.addEventListener("mousemove", onMouseMove);
+    element.addEventListener("mouseup", onMouseUp);
+  };
+
+  return { onMouseDown };
+};
diff --git a/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseHold.ts b/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseHold.ts
deleted file mode 100644
index 16cb43c3..00000000
--- a/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseHold.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { MouseEvent as ReactMouseEvent, useRef } from "react";
-import { Categories_Event } from "@core/types/event.types";
-import {
-  ID_GRID_ALLDAY_ROW,
-  ID_GRID_MAIN,
-} from "@web/common/constants/web.constants";
-import { Schema_GridEvent } from "@web/common/types/web.event.types";
-import { getElemById } from "@web/common/utils/grid.util";
-
-const GRID_EVENT_MOUSE_HOLD_DELAY = 750; // ms
-const GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD = 25; // pixels
-
-export const useGridEventMouseHold = (
-  cb: (event: Schema_GridEvent) => void,
-  eventType: Categories_Event.TIMED | Categories_Event.ALLDAY,
-  delay: number = GRID_EVENT_MOUSE_HOLD_DELAY,
-) => {
-  const elementEventTypeMap = {
-    [Categories_Event.TIMED]: ID_GRID_MAIN,
-    [Categories_Event.ALLDAY]: ID_GRID_ALLDAY_ROW,
-  };
-
-  const elementId = elementEventTypeMap[eventType];
-
-  const timeoutId = useRef<NodeJS.Timeout | null>(null);
-  const mouseMoved = useRef<boolean>(false);
-
-  const handleCallback = (event: Schema_GridEvent) => {
-    cb(event);
-  };
-
-  const onMouseDown = (e: ReactMouseEvent, event: Schema_GridEvent) => {
-    e.stopPropagation();
-    mouseMoved.current = false;
-
-    const initialX = e.clientX;
-    const initialY = e.clientY;
-
-    timeoutId.current = setTimeout(() => {
-      if (!mouseMoved.current) {
-        handleCallback(event);
-      }
-    }, delay);
-
-    const onMouseMove = (moveEvent: MouseEvent) => {
-      // Calculate distance moved
-      const deltaX = Math.abs(moveEvent.clientX - initialX);
-      const deltaY = Math.abs(moveEvent.clientY - initialY);
-      // Only count as moved if threshold is exceeded
-      if (
-        deltaX > GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD ||
-        deltaY > GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD
-      ) {
-        mouseMoved.current = true;
-        if (timeoutId.current) {
-          clearTimeout(timeoutId.current);
-        }
-        handleCallback(event);
-        cleanup();
-      }
-    };
-
-    const onMouseUp = () => {
-      cleanup();
-
-      // Manually dispatch a new 'mouseup' event to ensure other listeners execute
-      const _event = new MouseEvent("mouseup", {
-        bubbles: true,
-        cancelable: true,
-        button: 0,
-        clientX: e.clientX,
-        clientY: e.clientY,
-      });
-
-      // Delay dispatching the new event to let React flush updates
-      setTimeout(() => {
-        getElemById(elementId).dispatchEvent(_event);
-      }, 1);
-    };
-
-    const cleanup = () => {
-      handleCallback(event);
-
-      if (timeoutId.current) {
-        clearTimeout(timeoutId.current);
-      }
-
-      getElemById(elementId).removeEventListener("mousemove", onMouseMove);
-      getElemById(elementId).removeEventListener("mouseup", onMouseUp);
-    };
-
-    getElemById(elementId).addEventListener("mousemove", onMouseMove);
-    getElemById(elementId).addEventListener("mouseup", onMouseUp);
-  };
-
-  return { onMouseDown };
-};
diff --git a/packages/web/src/views/Forms/EventForm/EventForm.tsx b/packages/web/src/views/Forms/EventForm/EventForm.tsx
index 3c6371cf..f0e07c7b 100644
--- a/packages/web/src/views/Forms/EventForm/EventForm.tsx
+++ b/packages/web/src/views/Forms/EventForm/EventForm.tsx
@@ -202,10 +202,7 @@ export const EventForm: React.FC<FormProps> = ({
       isOpen={isFormOpen}
       name={ID_EVENT_FORM}
       onKeyDown={onFormKeyDown}
-      onMouseUp={(e) => {
-        e.stopPropagation();
-        e.preventDefault();
-
+      onMouseUp={() => {
         if (isStartDatePickerOpen) {
           setIsStartDatePickerOpen(false);
         }
@@ -233,7 +230,11 @@ export const EventForm: React.FC<FormProps> = ({
             {"<"}
           </StyledMigrateArrowInForm>
         )}
-        <IconButton onClick={onDeleteForm} aria-label="Delete Event">
+        <IconButton
+          onClick={onDeleteForm}
+          aria-label="Delete Event"
+          type="button"
+        >
           <Trash />
         </IconButton>
       </StyledIconRow>
diff --git a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx
index e5461a8f..0d3d6deb 100644
--- a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx
+++ b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx
@@ -122,7 +122,11 @@ export const SomedayEventForm: React.FC<FormProps> = ({
       role="form"
     >
       <StyledIconRow>
-        <IconButton onClick={onDelete} aria-label="Delete Someday Event">
+        <IconButton
+          onClick={onDelete}
+          aria-label="Delete Someday Event"
+          type="button"
+        >
           <Trash />
         </IconButton>
       </StyledIconRow>


Test patch:
diff --git a/packages/web/src/views/Calendar/Calendar.form.test.tsx b/packages/web/src/views/Calendar/Calendar.form.test.tsx
index d8826c4b..8a9914cf 100644
--- a/packages/web/src/views/Calendar/Calendar.form.test.tsx
+++ b/packages/web/src/views/Calendar/Calendar.form.test.tsx
@@ -1,7 +1,7 @@
 import React, { act } from "react";
 import "@testing-library/jest-dom";
 import { screen, waitFor, within } from "@testing-library/react";
-import userEvent, { UserEvent } from "@testing-library/user-event";
+import userEvent from "@testing-library/user-event";
 import { CLIMB } from "@core/__mocks__/events/events.misc";
 import { render } from "@web/__tests__/__mocks__/mock.render";
 import { preloadedState } from "@web/__tests__/__mocks__/state/state.weekEvents";
@@ -12,54 +12,20 @@ jest.mock("@web/views/Calendar/hooks/mouse/useEventListener", () => ({
 }));
 
 describe("Event Form", () => {
-  //   it("opens when clicking events", async () => {
-  //     const user = userEvent.setup();
-  //     render(<CalendarView />, { state: preloadedState });
-  //     expect(screen.queryByRole("form")).not.toBeInTheDocument();
-  /* timed event */
-  // await act(async () => {
-  //   await user.click(screen.getByRole("button", { name: /Ty & Tim/i }));
-  //   await _confirmCorrectEventFormIsOpen(TY_TIM.title);
-  // });
-  /* multi-week event */
-  // await act(async () => {
-  //   await _clickHeading(user);
-  //   await user.click(
-  //     screen.getByRole("button", { name: /multiweek event/i })
-  //   );
-  // });
-  // await waitFor(
-  //   () => {
-  //     expect(screen.getByRole("form")).toBeInTheDocument();
-  //   },
-  //   { timeout: 10000 }
-  // );
-  // await _confirmCorrectEventFormIsOpen(MULTI_WEEK.title);
-  // await waitFor(() => {
-  //   expect(
-  //     within(screen.getByRole("form")).getByText(MULTI_WEEK.title)
-  //   ).toBeInTheDocument();
-  // });
-  // /* someday event */
-  // await _clickHeading(user);
-  // await user.click(screen.getByRole("button", { name: /takeover world/i }));
-  // expect(
-  //   within(screen.getRole("form", name: {"Someday Event Form"})).getByText("Takeover world")
-  // ).toBeInTheDocument();
-  // }, 20000);
   it("closes after clicking outside", async () => {
     render(<CalendarView />, { state: preloadedState });
     const user = userEvent.setup();
 
     await act(async () => {
       await user.click(screen.getByRole("button", { name: CLIMB.title }));
-      await _clickHeading(user);
     });
 
-    await waitFor(() => {
-      expect(screen.queryByRole("form")).not.toBeInTheDocument();
+    await act(async () => {
+      await user.click(document.body);
     });
-  }, 10000);
+
+    expect(screen.queryByRole("form")).not.toBeInTheDocument();
+  });
   it("closes after clicking trash icon", async () => {
     const user = userEvent.setup();
     render(<CalendarView />, { state: preloadedState });
@@ -101,18 +67,3 @@ describe("Event Form", () => {
     });
   });
 });
-
-/***********
- * Helpers *
- ***********/
-const _clickHeading = async (user: UserEvent) => {
-  await user.click(screen.getByRole("heading", { level: 1 }));
-};
-
-const _confirmCorrectEventFormIsOpen = async (eventName: string) => {
-  await waitFor(() => {
-    expect(
-      within(screen.getByRole("form")).getByText(eventName),
-    ).toBeInTheDocument();
-  });
-};
diff --git a/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseDown.test.ts b/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseDown.test.ts
new file mode 100644
index 00000000..3255fd03
--- /dev/null
+++ b/packages/web/src/views/Calendar/hooks/grid/useGridEventMouseDown.test.ts
@@ -0,0 +1,158 @@
+import { renderHook } from "@testing-library/react";
+import { Categories_Event } from "@core/types/event.types";
+import { Schema_GridEvent } from "@web/common/types/web.event.types";
+import * as utils from "@web/common/utils";
+import * as gridUtils from "@web/common/utils/grid.util";
+import {
+  GRID_EVENT_MOUSE_HOLD_DELAY,
+  GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD,
+  useGridEventMouseDown,
+} from "./useGridEventMouseDown";
+
+describe("useGridEventMouseDown", () => {
+  const onClick = jest.fn();
+  const onDrag = jest.fn();
+  const stopPropagation = jest.fn();
+  const mockGridElement = document.createElement("div");
+
+  const mockEvent = {
+    _id: "123",
+    position: {
+      isOverlapping: false,
+      widthMultiplier: 1,
+      horizontalOrder: 1,
+    },
+  } as Schema_GridEvent;
+
+  const mockMouseEvent = {
+    target: mockGridElement,
+    clientX: 0,
+    clientY: 0,
+    stopPropagation,
+  } as unknown as React.MouseEvent;
+
+  beforeEach(() => {
+    jest.useFakeTimers();
+    onClick.mockClear();
+    onDrag.mockClear();
+    stopPropagation.mockClear();
+    jest.spyOn(utils, "isEventFormOpen").mockReturnValue(false);
+    jest.spyOn(gridUtils, "getElemById").mockReturnValue(mockGridElement);
+  });
+
+  afterEach(() => {
+    jest.useRealTimers();
+    jest.restoreAllMocks();
+  });
+
+  it("should trigger click when mouse releases before hold delay", () => {
+    const { result } = renderHook(() =>
+      useGridEventMouseDown(Categories_Event.TIMED, onClick, onDrag),
+    );
+
+    result.current.onMouseDown(mockMouseEvent, mockEvent);
+    jest.advanceTimersByTime(GRID_EVENT_MOUSE_HOLD_DELAY - 1);
+
+    // mouse up on the event
+    const mouseupEvent = new MouseEvent("mouseup", {
+      bubbles: true,
+      cancelable: true,
+    });
+    mockGridElement.dispatchEvent(mouseupEvent);
+
+    expect(onClick).toHaveBeenCalledWith(mockEvent);
+    expect(onDrag).not.toHaveBeenCalled();
+  });
+
+  it("should trigger drag when mouse stays down after hold delay", () => {
+    const { result } = renderHook(() =>
+      useGridEventMouseDown(Categories_Event.TIMED, onClick, onDrag),
+    );
+
+    result.current.onMouseDown(mockMouseEvent, mockEvent);
+    jest.advanceTimersByTime(GRID_EVENT_MOUSE_HOLD_DELAY + 1);
+
+    expect(onDrag).toHaveBeenCalledWith(mockEvent);
+    expect(onClick).not.toHaveBeenCalled();
+  });
+
+  it("should trigger drag when mouse moves before hold delay", () => {
+    const { result } = renderHook(() =>
+      useGridEventMouseDown(Categories_Event.TIMED, onClick, onDrag),
+    );
+
+    result.current.onMouseDown(mockMouseEvent, mockEvent);
+    jest.advanceTimersByTime(GRID_EVENT_MOUSE_HOLD_DELAY - 1);
+
+    // move the event beyond the threshold
+    const mousemoveEvent = new MouseEvent("mousemove", {
+      bubbles: true,
+      cancelable: true,
+      clientX: GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD + 1,
+      clientY: GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD + 1,
+    });
+    mockGridElement.dispatchEvent(mousemoveEvent);
+
+    expect(onDrag).toHaveBeenCalledWith(mockEvent);
+    expect(onClick).not.toHaveBeenCalled();
+  });
+
+  describe("When form is open", () => {
+    beforeEach(() => {
+      jest.spyOn(utils, "isEventFormOpen").mockReturnValue(true);
+    });
+
+    it("should trigger drag when mouse is held down after delay", () => {
+      const { result } = renderHook(() =>
+        useGridEventMouseDown(Categories_Event.TIMED, onClick, onDrag),
+      );
+
+      // Mock :active state to simulate mouse still being down
+      jest
+        .spyOn(document, "querySelector")
+        .mockReturnValue(document.createElement("div"));
+
+      result.current.onMouseDown(mockMouseEvent, mockEvent);
+      jest.advanceTimersByTime(GRID_EVENT_MOUSE_HOLD_DELAY + 1);
+
+      expect(onDrag).toHaveBeenCalledWith(mockEvent);
+      expect(onClick).not.toHaveBeenCalled();
+    });
+
+    it("should not trigger drag when mouse is released before delay", () => {
+      const { result } = renderHook(() =>
+        useGridEventMouseDown(Categories_Event.TIMED, onClick, onDrag),
+      );
+
+      result.current.onMouseDown(mockMouseEvent, mockEvent);
+
+      // Mock :active state to simulate mouse not being down - do this AFTER onMouseDown
+      jest.spyOn(document, "querySelector").mockReturnValue(null);
+
+      jest.advanceTimersByTime(GRID_EVENT_MOUSE_HOLD_DELAY + 1);
+
+      expect(onDrag).not.toHaveBeenCalled();
+      expect(onClick).not.toHaveBeenCalled();
+    });
+
+    it("should not trigger drag when mouse moves", () => {
+      const { result } = renderHook(() =>
+        useGridEventMouseDown(Categories_Event.TIMED, onClick, onDrag),
+      );
+
+      result.current.onMouseDown(mockMouseEvent, mockEvent);
+
+      // move the event beyond the threshold
+      const mousemoveEvent = new MouseEvent("mousemove", {
+        bubbles: true,
+        cancelable: true,
+        clientX: GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD + 1,
+        clientY: GRID_EVENT_MOUSE_HOLD_MOVE_THRESHOLD + 1,
+      });
+      mockGridElement.dispatchEvent(mousemoveEvent);
+
+      expect(onDrag).not.toHaveBeenCalled();
+      expect(onClick).not.toHaveBeenCalled();
+    });
+  });
+});
diff --git a/packages/web/src/views/Forms/EventForm/EventForm.test.tsx b/packages/web/src/views/Forms/EventForm/EventForm.test.tsx
index 971f268b..87826552 100644
--- a/packages/web/src/views/Forms/EventForm/EventForm.test.tsx
+++ b/packages/web/src/views/Forms/EventForm/EventForm.test.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable testing-library/no-unnecessary-act */
 import React, { act } from "react";
 import "@testing-library/jest-dom/extend-expect";
 import { screen } from "@testing-library/react";
@@ -41,7 +40,7 @@ test("start date picker opens and then closes when clicking the end input", asyn
   await _clickEndInput();
 
   // Check if the date picker is closed
-  //   expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
 });
 
 const _clickStartInput = async () => {