Skip to content

Commit 51e1a0f

Browse files
✨ Feat: Update event cursor css when dragging (#268)
* bug(web): remove `cursor: grabbing` from event styles `cursor` style is overridden in on hover pseudo class, making this line redundant. * feature(web): Update event cursor css when dragging * chore(web): rename `useGridClick` to `useGridMouseUp` Better emphasizes its purpose, since the hook only handles mouseup events. * feature(web): Implement mouse hold delay for drafting an event This is a small enhancement that mimics how Google Calendar delays drafting an event when we mouse hold over it without moving the mouse * chore(web): small refactor * feature(web): mouse hold delay feature, support all day events * feature(web): mouse hold delay feature, support draft events * ♻️ refactor(web): Remove constant from layout and inline in hook layout.constants is meant for detailing the page's layout, which'll come in handy as we make the page more dynamic and responsive. The values are shared across many parts of the app, so have a dedicated file for it is helpful. This value isn't related to the layout, and is only used in one place, so it can live there instead. --------- Co-authored-by: Tyler Dane <tyler@switchback.tech>
1 parent e3210c7 commit 51e1a0f

File tree

9 files changed

+130
-26
lines changed

9 files changed

+130
-26
lines changed

packages/web/src/common/utils/mouse/mouse.util.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ import { MouseEvent } from "react";
33
export const isRightClick = (e: MouseEvent) => {
44
return e.button === 2;
55
};
6+
7+
export const isLeftClick = (e: MouseEvent) => {
8+
return e.button === 0;
9+
};

packages/web/src/views/Calendar/components/Draft/Draft.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout"
88
import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
99
import { useDraftContext } from "./context/useDraftContext";
1010
import { GridDraft } from "./grid/GridDraft";
11-
import { useGridClick } from "./grid/hooks/useGridClick";
1211
import { useGridMouseMove } from "./grid/hooks/useGridMouseMove";
12+
import { useGridMouseUp } from "./grid/hooks/useGridMouseUp";
1313

1414
interface Props {
1515
measurements: Measurements_Grid;
1616
weekProps: WeekProps;
1717
}
1818

1919
export const Draft: FC<Props> = ({ measurements, weekProps }) => {
20-
useGridClick();
20+
useGridMouseUp();
2121
useGridMouseMove();
2222

2323
const category = useAppSelector(selectDraftCategory);

packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import React, { FC, MouseEvent } from "react";
22
import { FloatingFocusManager } from "@floating-ui/react";
33
import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants";
4+
import { Categories_Event } from "@core/types/event.types";
45
import { Schema_GridEvent } from "@web/common/types/web.event.types";
6+
import { useGridEventMouseHold } from "@web/views/Calendar/hooks/grid/useGridEventMouseHold";
57
import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
68
import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
79
import { EventForm } from "@web/views/Forms/EventForm";
@@ -33,6 +35,10 @@ export const GridDraft: FC<Props> = ({ measurements, weekProps }) => {
3335
actions.convert(start, end);
3436
};
3537

38+
const { onMouseDown } = useGridEventMouseHold((event) => {
39+
setIsDragging(true);
40+
}, Categories_Event.TIMED);
41+
3642
if (!draft) return null;
3743

3844
return (
@@ -46,9 +52,8 @@ export const GridDraft: FC<Props> = ({ measurements, weekProps }) => {
4652
key={`draft-${draft?._id}`}
4753
measurements={measurements}
4854
onEventMouseDown={(event: Schema_GridEvent, e: MouseEvent) => {
49-
e.stopPropagation();
5055
e.preventDefault();
51-
setIsDragging(true);
56+
onMouseDown(e, event);
5257
}}
5358
onScalerMouseDown={(
5459
event: Schema_GridEvent,

packages/web/src/views/Calendar/components/Draft/grid/hooks/useGridMouseMove.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MouseEvent, useCallback } from "react";
1+
import { MouseEvent, useCallback, useEffect } from "react";
22
import { selectIsDrafting } from "@web/ducks/events/selectors/draft.selectors";
33
import { useAppSelector } from "@web/store/store.hooks";
44
import { useEventListener } from "@web/views/Calendar/hooks/mouse/useEventListener";
@@ -19,11 +19,24 @@ export const useGridMouseMove = () => {
1919
resize(e);
2020
} else if (isDragging) {
2121
e.preventDefault();
22+
document.body.style.cursor = "move";
2223
drag(e);
2324
}
2425
},
2526
[draft?.isAllDay, drag, isDrafting, isDragging, isResizing, resize],
2627
);
2728

29+
const _onMouseUp = useCallback(() => {
30+
document.body.style.cursor = "";
31+
}, []);
32+
2833
useEventListener("mousemove", _onMouseMove);
34+
useEventListener("mouseup", _onMouseUp);
35+
36+
// Ensure cursor resets when the component unmounts
37+
useEffect(() => {
38+
return () => {
39+
document.body.style.cursor = "";
40+
};
41+
}, []);
2942
};

packages/web/src/views/Calendar/components/Draft/grid/hooks/useGridClick.ts renamed to packages/web/src/views/Calendar/components/Draft/grid/hooks/useGridMouseUp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useAppSelector } from "@web/store/store.hooks";
1010
import { useEventListener } from "@web/views/Calendar/hooks/mouse/useEventListener";
1111
import { useDraftContext } from "../../context/useDraftContext";
1212

13-
export const useGridClick = () => {
13+
export const useGridMouseUp = () => {
1414
const { actions, state } = useDraftContext();
1515
const { draft, dragStatus, isDragging, isResizing, resizeStatus } = state;
1616
const { discard, openForm, stopDragging, stopResizing, submit } = actions;

packages/web/src/views/Calendar/components/Event/styled.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,7 @@ export const StyledEvent = styled.div.attrs<StyledEventProps>((props) => {
5252
})<StyledEventProps>`
5353
background-color: ${(props) => props.backgroundColor};
5454
border-radius: 2px;
55-
${(props) => props.isDragging && `cursor: grabbing`}
56-
filter: brightness(
57-
${({ isInPast }) => (isInPast ? 0.7 : null)}
58-
);
55+
filter: brightness(${({ isInPast }) => (isInPast ? 0.7 : null)});
5956
height: ${({ height }) => height}px;
6057
left: ${(props) => props.left}px;
6158
opacity: ${(props) => props.opacity};
@@ -76,6 +73,7 @@ export const StyledEvent = styled.div.attrs<StyledEventProps>((props) => {
7673
backgroundColor,
7774
isOptimistic,
7875
isPlaceholder,
76+
isDragging,
7977
isResizing,
8078
hoverColor,
8179
theme,
@@ -84,7 +82,7 @@ export const StyledEvent = styled.div.attrs<StyledEventProps>((props) => {
8482
!isResizing &&
8583
`
8684
background-color: ${isOptimistic ? darken(backgroundColor) : hoverColor};
87-
cursor: ${isOptimistic ? "wait" : "pointer"};
85+
cursor: ${isDragging ? "move" : isOptimistic ? "wait" : "pointer"};
8886
drop-shadow(2px 4px 4px ${theme.color.shadow.default});
8987
`};
9088
}

packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvents.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import React, { MouseEvent } from "react";
1+
import React from "react";
22
import { Categories_Event } from "@core/types/event.types";
33
import { ID_GRID_EVENTS_ALLDAY } from "@web/common/constants/web.constants";
44
import { Schema_GridEvent } from "@web/common/types/web.event.types";
55
import { isSomedayEventFormOpen } from "@web/common/utils";
6+
import { isLeftClick } from "@web/common/utils/mouse/mouse.util";
67
import { selectDraftId } from "@web/ducks/events/selectors/draft.selectors";
78
import { selectAllDayEvents } from "@web/ducks/events/selectors/event.selectors";
89
import { draftSlice } from "@web/ducks/events/slices/draft.slice";
910
import { useAppDispatch, useAppSelector } from "@web/store/store.hooks";
11+
import { useGridEventMouseHold } from "@web/views/Calendar/hooks/grid/useGridEventMouseHold";
1012
import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
1113
import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
1214
import { AllDayEventMemo } from "./AllDayEvent";
@@ -26,13 +28,7 @@ export const AllDayEvents = ({
2628
const draftId = useAppSelector(selectDraftId);
2729
const dispatch = useAppDispatch();
2830

29-
const onMouseDown = (e: MouseEvent, event: Schema_GridEvent) => {
30-
e.stopPropagation();
31-
32-
if (e.button !== 0) {
33-
return;
34-
}
35-
31+
const { onMouseDown } = useGridEventMouseHold((event) => {
3632
if (isSomedayEventFormOpen()) {
3733
dispatch(draftSlice.actions.discard());
3834
}
@@ -43,7 +39,7 @@ export const AllDayEvents = ({
4339
event,
4440
}),
4541
);
46-
};
42+
}, Categories_Event.ALLDAY);
4743

4844
return (
4945
<StyledEvents id={ID_GRID_EVENTS_ALLDAY}>
@@ -56,7 +52,12 @@ export const AllDayEvents = ({
5652
startOfView={startOfView}
5753
endOfView={endOfView}
5854
measurements={measurements}
59-
onMouseDown={onMouseDown}
55+
onMouseDown={(e, event) => {
56+
if (!isLeftClick(e)) {
57+
return;
58+
}
59+
onMouseDown(e, event);
60+
}}
6061
/>
6162
);
6263
})}

packages/web/src/views/Calendar/components/Grid/MainGrid/MainGridEvents.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { MouseEvent } from "react";
1+
import React from "react";
22
import { Categories_Event } from "@core/types/event.types";
33
import { ID_GRID_EVENTS_TIMED } from "@web/common/constants/web.constants";
44
import { Schema_GridEvent } from "@web/common/types/web.event.types";
@@ -8,6 +8,7 @@ import { selectDraftId } from "@web/ducks/events/selectors/draft.selectors";
88
import { selectGridEvents } from "@web/ducks/events/selectors/event.selectors";
99
import { draftSlice } from "@web/ducks/events/slices/draft.slice";
1010
import { useAppDispatch, useAppSelector } from "@web/store/store.hooks";
11+
import { useGridEventMouseHold } from "@web/views/Calendar/hooks/grid/useGridEventMouseHold";
1112
import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
1213
import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
1314
import { GridEventMemo } from "../../Event/Grid/GridEvent/GridEvent";
@@ -26,17 +27,15 @@ export const MainGridEvents = ({ measurements, weekProps }: Props) => {
2627
const adjustedEvents = adjustOverlappingEvents(timedEvents);
2728
const category = Categories_Event.TIMED;
2829

29-
const onMouseDown = (e: MouseEvent, event: Schema_GridEvent) => {
30-
e.stopPropagation();
30+
const { onMouseDown } = useGridEventMouseHold((event) => {
3131
if (isEventFormOpen()) {
3232
dispatch(
3333
draftSlice.actions.swap({ event, category: Categories_Event.TIMED }),
3434
);
3535
return;
3636
}
37-
3837
editTimedEvent(event);
39-
};
38+
}, Categories_Event.TIMED);
4039

4140
const resizeTimedEvent = (
4241
event: Schema_GridEvent,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { MouseEvent as ReactMouseEvent, useRef } from "react";
2+
import { Categories_Event } from "@core/types/event.types";
3+
import {
4+
ID_GRID_ALLDAY_ROW,
5+
ID_GRID_MAIN,
6+
} from "@web/common/constants/web.constants";
7+
import { Schema_GridEvent } from "@web/common/types/web.event.types";
8+
import { getElemById } from "@web/common/utils/grid.util";
9+
10+
const GRID_EVENT_MOUSE_HOLD_DELAY = 750;
11+
12+
export const useGridEventMouseHold = (
13+
cb: (event: Schema_GridEvent) => void,
14+
eventType: Categories_Event.TIMED | Categories_Event.ALLDAY,
15+
delay: number = GRID_EVENT_MOUSE_HOLD_DELAY,
16+
) => {
17+
const elementEventTypeMap = {
18+
[Categories_Event.TIMED]: ID_GRID_MAIN,
19+
[Categories_Event.ALLDAY]: ID_GRID_ALLDAY_ROW,
20+
};
21+
22+
const elementId = elementEventTypeMap[eventType];
23+
24+
const timeoutId = useRef<NodeJS.Timeout | null>(null);
25+
const mouseMoved = useRef<boolean>(false);
26+
27+
const handleCallback = (event: Schema_GridEvent) => {
28+
cb(event);
29+
};
30+
31+
const onMouseDown = (e: ReactMouseEvent, event: Schema_GridEvent) => {
32+
e.stopPropagation();
33+
mouseMoved.current = false;
34+
35+
timeoutId.current = setTimeout(() => {
36+
if (!mouseMoved.current) {
37+
handleCallback(event);
38+
}
39+
}, delay);
40+
41+
const onMouseMove = () => {
42+
mouseMoved.current = true;
43+
if (timeoutId.current) {
44+
clearTimeout(timeoutId.current);
45+
}
46+
handleCallback(event);
47+
cleanup();
48+
};
49+
50+
const onMouseUp = () => {
51+
cleanup();
52+
53+
// Manually dispatch a new 'mouseup' event to ensure other listeners execute
54+
const _event = new MouseEvent("mouseup", {
55+
bubbles: true,
56+
cancelable: true,
57+
button: 0,
58+
clientX: e.clientX,
59+
clientY: e.clientY,
60+
});
61+
62+
// Delay dispatching the new event to let React flush updates
63+
setTimeout(() => {
64+
getElemById(elementId).dispatchEvent(_event);
65+
}, 1);
66+
};
67+
68+
const cleanup = () => {
69+
handleCallback(event);
70+
71+
if (timeoutId.current) {
72+
clearTimeout(timeoutId.current);
73+
}
74+
75+
getElemById(elementId).removeEventListener("mousemove", onMouseMove);
76+
getElemById(elementId).removeEventListener("mouseup", onMouseUp);
77+
};
78+
79+
getElemById(elementId).addEventListener("mousemove", onMouseMove);
80+
getElemById(elementId).addEventListener("mouseup", onMouseUp);
81+
};
82+
83+
return { onMouseDown };
84+
};

0 commit comments

Comments
 (0)