Skip to content

Commit 76b1c86

Browse files
committed
feat(web): WIP Synthetically drag someday event to other list when reaching end/beginning of list
This commit is a WIP. Its in a flaky state and has a number of issues that needs fixing
1 parent b4d777d commit 76b1c86

File tree

5 files changed

+224
-5
lines changed

5 files changed

+224
-5
lines changed

packages/web/src/views/Calendar/components/Draft/sidebar/hooks/useSidebarActions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ export const useSidebarActions = (
555555
onPlaceholderClick,
556556
onSubmit,
557557
reset,
558+
onReorder: reorder,
558559
resetLocalDraftStateIfNeeded,
559560
setDraft,
560561
};

packages/web/src/views/Calendar/components/Draft/sidebar/hooks/useSidebarState.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useState } from "react";
2-
import { Schema_Event } from "@core/types/event.types";
2+
import { Categories_Event, Schema_Event } from "@core/types/event.types";
33
import { COLUMN_MONTH, COLUMN_WEEK } from "@web/common/constants/web.constants";
4+
import { Schema_SomedayEvent } from "@web/common/types/web.event.types";
45
import { selectIsDNDing } from "@web/ducks/events/selectors/draft.selectors";
56
import { selectCategorizedEvents } from "@web/ducks/events/selectors/someday.selectors";
67
import { useAppSelector } from "@web/store/store.hooks";
@@ -37,6 +38,16 @@ export const useSidebarState = (measurements: Measurements_Grid) => {
3738

3839
const shouldPreviewOnGrid = isDNDing && isOverGrid;
3940

41+
const getEventsByCategory = (
42+
category: Categories_Event,
43+
): Schema_SomedayEvent[] => {
44+
return somedayEvents.columns[
45+
category === Categories_Event.SOMEDAY_WEEK ? COLUMN_WEEK : COLUMN_MONTH
46+
].eventIds.map((id) =>
47+
Object.values(categorizedEvents.events).find((event) => event._id === id),
48+
) as Schema_SomedayEvent[];
49+
};
50+
4051
const state = {
4152
draft,
4253
somedayIds,
@@ -53,6 +64,7 @@ export const useSidebarState = (measurements: Measurements_Grid) => {
5364
mouseCoords,
5465
shouldPreviewOnGrid,
5566
somedayEvents,
67+
getEventsByCategory,
5668
};
5769
const setters = {
5870
setDraft,

packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/DraggableSomedayEvent/DraggableSomedayEvent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const DraggableSomedayEvent: FC<Props> = ({
5050
snapshot={snapshot}
5151
setEvent={setters.setDraft}
5252
weekViewRange={{ startDate: start, endDate: end }}
53+
index={index}
5354
/>
5455
</>
5556
);

packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/SomedayEventContainer.tsx

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import React, { useState } from "react";
1+
import React, { useEffect, useState } from "react";
22
import { FloatingFocusManager, FloatingPortal } from "@floating-ui/react";
3-
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
3+
import {
4+
DraggableProvided,
5+
DraggableStateSnapshot,
6+
DropResult,
7+
} from "@hello-pangea/dnd";
48
import { Priorities } from "@core/constants/core.constants";
59
import { Categories_Event, Schema_Event } from "@core/types/event.types";
10+
import { COLUMN_MONTH, COLUMN_WEEK } from "@web/common/constants/web.constants";
611
import { useDraftForm } from "@web/views/Calendar/components/Draft/hooks/state/useDraftForm";
712
import { useSidebarContext } from "@web/views/Calendar/components/Draft/sidebar/context/useSidebarContext";
813
import { Setters_Sidebar } from "@web/views/Calendar/components/Draft/sidebar/hooks/useSidebarState";
14+
import {
15+
canSendToOtherSection,
16+
useKeyboardDrag,
17+
} from "@web/views/Calendar/hooks/keyboard/useKeyboardDrag";
918
import { SIDEBAR_OPEN_WIDTH } from "@web/views/Calendar/layout.constants";
1019
import { SomedayEventForm } from "@web/views/Forms/SomedayEventForm/SomedayEventForm";
1120
import { StyledFloatContainer } from "@web/views/Forms/SomedayEventForm/styled";
@@ -25,6 +34,8 @@ export interface Props {
2534
startDate: string;
2635
endDate: string;
2736
};
37+
index: number;
38+
totalEvents: number;
2839
}
2940

3041
export const SomedayEventContainer = ({
@@ -38,8 +49,12 @@ export const SomedayEventContainer = ({
3849
snapshot,
3950
setEvent,
4051
weekViewRange,
52+
index,
4153
}: Props) => {
42-
const { actions, setters, state } = useSidebarContext();
54+
// Using non-null assertion since `useSidebarContext` throws if context is missing.
55+
56+
const { actions, setters, state } = useSidebarContext()!;
57+
const events = state.getEventsByCategory(category);
4358

4459
const formProps = useDraftForm(
4560
category,
@@ -50,10 +65,101 @@ export const SomedayEventContainer = ({
5065
);
5166

5267
const [isFocused, setIsFocused] = useState(false);
68+
const [hasCrossColumnReorder, setHasCrossColumnReorder] = useState(null);
5369

5470
const isDraftingThisEvent =
5571
state.isDrafting && state.draft?._id === event._id;
5672

73+
useEffect(() => {
74+
if (hasCrossColumnReorder) {
75+
setTimeout(() => {
76+
// Perform synthetic space bar press
77+
// select data-event-id
78+
const el = document.querySelector(
79+
`[data-event-id="${hasCrossColumnReorder}"]`,
80+
);
81+
if (el) {
82+
const spaceEvent = new KeyboardEvent("keydown", {
83+
key: " ",
84+
code: "Space",
85+
keyCode: 32,
86+
charCode: 32,
87+
bubbles: true,
88+
});
89+
el.dispatchEvent(spaceEvent);
90+
}
91+
}, 100);
92+
93+
setHasCrossColumnReorder(null);
94+
}
95+
}, [hasCrossColumnReorder]);
96+
97+
const handleKeyDown = useKeyboardDrag({
98+
event,
99+
category,
100+
index,
101+
isDragging: isDragging || isDrafting,
102+
onExitDrag: (index) => {
103+
const _canSendToOtherSection = canSendToOtherSection(
104+
events.length,
105+
category,
106+
index,
107+
);
108+
if (!_canSendToOtherSection) {
109+
return;
110+
}
111+
112+
// Programmatically dispatch a "space" keydown event on the currently
113+
// focused element. In `@hello-pangea/dnd`, pressing the space key while
114+
// in keyboard-dragging mode finalises (drops) the draggable, therefore
115+
// this emulates the user pressing space to exit drag mode when they
116+
// reach either end of the list.
117+
118+
const activeEl = document.activeElement as HTMLElement;
119+
const keyboardEvent = new KeyboardEvent("keydown", {
120+
key: " ",
121+
code: "Space",
122+
keyCode: 32,
123+
charCode: 32,
124+
bubbles: true,
125+
});
126+
activeEl.dispatchEvent(keyboardEvent);
127+
128+
// After "dropping" the item via space-bar event, perform a synthetic
129+
// cross-column drag so the item is moved from week → month or vice-versa.
130+
const sourceDroppableId =
131+
category === Categories_Event.SOMEDAY_WEEK ? COLUMN_WEEK : COLUMN_MONTH;
132+
133+
const destinationDroppableId =
134+
category === Categories_Event.SOMEDAY_WEEK ? COLUMN_MONTH : COLUMN_WEEK;
135+
136+
const syntheticResult: DropResult = {
137+
draggableId: event._id!,
138+
source: {
139+
droppableId: sourceDroppableId,
140+
index,
141+
},
142+
destination: {
143+
droppableId: destinationDroppableId,
144+
index: 0, // place at top of destination list
145+
},
146+
// The fields below are not used by the `reorder` helper but are
147+
// required by the `DropResult` type. We can safely set sensible
148+
// defaults.
149+
combine: null,
150+
mode: "FLUID",
151+
reason: "DROP",
152+
type: "DEFAULT",
153+
} as DropResult;
154+
155+
// Trigger sidebar state update + backend sync
156+
actions.onReorder(syntheticResult);
157+
158+
// Set flag to trigger useEffect for synthetic space bar press
159+
setHasCrossColumnReorder(event._id);
160+
},
161+
});
162+
57163
return (
58164
<>
59165
<SomedayEvent
@@ -68,7 +174,9 @@ export const SomedayEventContainer = ({
68174
actions.onDraft(event, category);
69175
}}
70176
onFocus={() => setIsFocused(true)}
71-
onKeyDown={() => console.log("onKeyDown")}
177+
onKeyDown={(e) => {
178+
if (isDragging) handleKeyDown(e);
179+
}}
72180
priority={event.priority || Priorities.UNASSIGNED}
73181
provided={provided}
74182
snapshot={snapshot}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useRef } from "react";
2+
import { Categories_Event, Schema_Event } from "@core/types/event.types";
3+
import { COLUMN_MONTH, COLUMN_WEEK } from "@web/common/constants/web.constants";
4+
import { useSidebarContext } from "@web/views/Calendar/components/Draft/sidebar/context/useSidebarContext";
5+
6+
interface UseKeyboardDragProps {
7+
event: Schema_Event;
8+
category: Categories_Event;
9+
index: number;
10+
isDragging: boolean;
11+
onExitDrag: (index: number) => void;
12+
}
13+
14+
export function canSendToOtherSection(
15+
numberOfEvents: number,
16+
category: Categories_Event,
17+
index: number,
18+
) {
19+
if (category === Categories_Event.SOMEDAY_MONTH) {
20+
return numberOfEvents > index + 1;
21+
} else {
22+
// Someday week
23+
return index !== 0;
24+
}
25+
}
26+
27+
export const useKeyboardDrag = ({
28+
event,
29+
category,
30+
index,
31+
isDragging,
32+
onExitDrag,
33+
}: UseKeyboardDragProps) => {
34+
const sidebarContext = useSidebarContext();
35+
36+
// TS Guard
37+
if (!sidebarContext)
38+
throw new Error(
39+
"useSidebarContext must be used within SidebarDraftProvider",
40+
);
41+
42+
const { state } = sidebarContext;
43+
const column =
44+
state.somedayEvents.columns[
45+
category === Categories_Event.SOMEDAY_WEEK ? COLUMN_WEEK : COLUMN_MONTH
46+
];
47+
const totalEvents = column.eventIds.length;
48+
49+
// Keep track of current index in a ref to persist between renders
50+
const currentIndexRef = useRef<number>(index);
51+
const isDraggingRef = useRef<boolean>(isDragging);
52+
53+
const updateIndex = (newIndex: number) => {
54+
currentIndexRef.current = newIndex;
55+
};
56+
57+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
58+
// Update the ref with the latest index prop on each keyboard event
59+
if (isDraggingRef.current !== isDragging) {
60+
isDraggingRef.current = isDragging;
61+
currentIndexRef.current = index;
62+
}
63+
64+
// Track index changes based on arrow keys
65+
if (e.key === "ArrowUp") {
66+
// If we are at the beginning of the list
67+
if (currentIndexRef.current === 0) {
68+
console.log(
69+
"Reached beginning of list:",
70+
event.title,
71+
"current index:",
72+
currentIndexRef.current,
73+
);
74+
return onExitDrag(currentIndexRef.current);
75+
}
76+
77+
const newIndex = Math.max(0, currentIndexRef.current - 1);
78+
updateIndex(newIndex);
79+
} else if (e.key === "ArrowDown") {
80+
// If we are at the end of the list
81+
if (currentIndexRef.current === totalEvents - 1) {
82+
console.log(
83+
"Reached end of list:",
84+
event.title,
85+
"current index:",
86+
currentIndexRef.current,
87+
);
88+
89+
return onExitDrag(currentIndexRef.current);
90+
}
91+
const newIndex = Math.min(totalEvents - 1, currentIndexRef.current + 1);
92+
updateIndex(newIndex);
93+
}
94+
};
95+
96+
return handleKeyDown;
97+
};

0 commit comments

Comments
 (0)