Skip to content

Commit f730d0f

Browse files
✨ feat(day-view-agenda): create and edit calendar events from agenda (#1342)
* 🎨 style(all-agenda): provide clickable area to add new all day event * ✨ feat(day-view-agenda): create and edit calendar events from agenda 🎨 style(EventForm): optimize re-rendering with deepEqual and add fast-deep-equal dependency 🎨 style(AllDayAgendaEvent): import DATA_EVENT_ELEMENT_ID constant for better event handling 🎨 style(useMovementEvent): remove unused preventDefault function and streamline mouse event handling 🎨 style(event-emitter): introduce mouseDown$ BehaviorSubject for improved mouse state tracking 🎨 style(package): add fast-deep-equal dependency for performance improvements
1 parent e68cf17 commit f730d0f

File tree

40 files changed

+1766
-571
lines changed

40 files changed

+1766
-571
lines changed

packages/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"classnames": "^2.3.1",
1818
"css-loader": "^6.3.0",
1919
"dayjs": "^1.10.7",
20+
"fast-deep-equal": "^3.1.3",
2021
"html-webpack-plugin": "^5.6.4",
2122
"mini-css-extract-plugin": "^2.3.0",
2223
"normalizr": "^3.6.1",

packages/web/src/__tests__/__mocks__/mock.render.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
render,
1313
renderHook,
1414
} from "@testing-library/react";
15-
import { useSetupKeyEvents } from "@web/common/hooks/useKeyboardEvent";
15+
import { ID_ROOT } from "@web/common/constants/web.constants";
16+
import { useSetupKeyboardEvents } from "@web/common/hooks/useKeyboardEvent";
17+
import { useSetupMovementEvents } from "@web/common/hooks/useMovementEvent";
1618
import { sagaMiddleware } from "@web/common/store/middlewares";
1719
import { AbsoluteOverflowLoader } from "@web/components/AbsoluteOverflowLoader";
1820
import { CompassRequiredProviders } from "@web/components/CompassProvider/CompassProvider";
@@ -36,13 +38,16 @@ const TestProviders = (props?: {
3638
store?: typeof compassStore;
3739
}) => {
3840
return function TestProvidersWrapper({ children }: PropsWithChildren) {
39-
useSetupKeyEvents();
41+
useSetupKeyboardEvents();
42+
useSetupMovementEvents();
4043

4144
if (!props?.router) {
4245
return (
43-
<CompassRequiredProviders {...props}>
44-
{children}
45-
</CompassRequiredProviders>
46+
<div id={ID_ROOT} data-testid={ID_ROOT}>
47+
<CompassRequiredProviders {...props}>
48+
{children}
49+
</CompassRequiredProviders>
50+
</div>
4651
);
4752
}
4853

@@ -53,15 +58,17 @@ const TestProviders = (props?: {
5358
}
5459

5560
return (
56-
<CompassRequiredProviders store={props?.store}>
57-
<RouterProvider
58-
router={props.router}
59-
fallbackElement={<AbsoluteOverflowLoader />}
60-
future={{
61-
v7_startTransition: true,
62-
}}
63-
/>
64-
</CompassRequiredProviders>
61+
<div id={ID_ROOT} data-testid={ID_ROOT}>
62+
<CompassRequiredProviders store={props?.store}>
63+
<RouterProvider
64+
router={props.router}
65+
fallbackElement={<AbsoluteOverflowLoader />}
66+
future={{
67+
v7_startTransition: true,
68+
}}
69+
/>
70+
</CompassRequiredProviders>
71+
</div>
6572
);
6673
};
6774
};
@@ -112,7 +119,8 @@ const customRenderHook = <ReturnType, Props>(
112119
const BaseProviders = TestProviders({ store, router });
113120

114121
const Wrapper = (props: PropsWithChildren) => {
115-
useSetupKeyEvents();
122+
useSetupKeyboardEvents();
123+
useSetupMovementEvents();
116124

117125
if (!WrapperComponent) return <BaseProviders {...props} />;
118126

packages/web/src/__tests__/jsdom.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export default class WASMEnvironment extends TestEnvironment {
7272

7373
this.global.window.HTMLElement.prototype.scroll = () => {};
7474
this.global.window.HTMLElement.prototype.scrollIntoView = () => {};
75+
this.global.window.document.elementFromPoint = () => null;
76+
this.global.window.document.caretPositionFromPoint = () => null;
7577

7678
this.global.fetch = fetch as unknown as typeof globalThis.fetch;
7779
this.global.Blob = globalThis.Blob;

packages/web/src/common/constants/web.constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ export const COLUMN_MONTH = "monthEvents";
33

44
export const GOOGLE = "google";
55

6+
export const ID_ROOT = "root";
67
export const ID_ALLDAY_COLUMNS = "allDayColumns";
78
export const ID_EVENT_FORM = "Event Form";
89
export const ID_GRID_ALLDAY_ROW = "allDayRow";
910
export const ID_GRID_EVENTS_ALLDAY = "allDayEvents";
1011
export const ID_GRID_EVENTS_TIMED = "timedEvents";
12+
export const ID_SOMEDAY_WEEK_COLUMN = "somedayWeekColumn";
1113
export const ID_GRID_MAIN = "mainGrid";
1214
export const ID_REMINDER_INPUT = "reminderInput";
1315
export const ID_MAIN = "mainSection";
@@ -22,6 +24,10 @@ export const DATA_EVENT_ELEMENT_ID = "data-event-id";
2224
export const DATA_TASK_ELEMENT_ID = "data-task-id";
2325
export const ID_CONTEXT_MENU_ITEMS = "context-menu-items";
2426
export const ID_ADD_TASK_BUTTON = "add-task-button";
27+
export const CLASS_ALL_DAY_CALENDAR_EVENT = "all-day-calendar-event";
28+
export const CLASS_TIMED_CALENDAR_EVENT = "timed-calendar-event";
29+
export const CLASS_WEEK_SOMEDAY_EVENT = "week-someday-event";
30+
export const CLASS_MONTH_SOMEDAY_EVENT = "month-someday-event";
2531

2632
export enum ZIndex {
2733
LAYER_1 = 1,
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import {
2+
Dispatch,
3+
PropsWithChildren,
4+
createContext,
5+
useCallback,
6+
useMemo,
7+
useState,
8+
} from "react";
9+
import { Subject } from "rxjs";
10+
import {
11+
Placement,
12+
ReferenceType,
13+
Strategy,
14+
UseFloatingOptions,
15+
UseInteractionsReturn,
16+
autoUpdate,
17+
flip,
18+
offset,
19+
shift,
20+
useDismiss,
21+
useFloating,
22+
useInteractions,
23+
} from "@floating-ui/react";
24+
import {
25+
COLUMN_MONTH,
26+
COLUMN_WEEK,
27+
ID_GRID_ALLDAY_ROW,
28+
ID_GRID_MAIN,
29+
ID_ROOT,
30+
ID_SIDEBAR,
31+
} from "@web/common/constants/web.constants";
32+
import { useMovementEvent } from "@web/common/hooks/useMovementEvent";
33+
import { Coordinates } from "@web/common/types/util.types";
34+
import { DomMovement } from "@web/common/utils/dom-events/event-emitter.util";
35+
36+
type Floating = ReturnType<typeof useFloating>;
37+
38+
type OpenChangeParams = Parameters<
39+
Exclude<UseFloatingOptions["onOpenChange"], undefined>
40+
>;
41+
42+
interface MousePosition {
43+
caret: CaretPosition | null;
44+
element: Element | null;
45+
mousedown: boolean;
46+
isOverGrid: boolean;
47+
mouseCoords: Coordinates;
48+
isOverSidebar: boolean;
49+
isOverMainGrid: boolean;
50+
isOverSomedayWeek: boolean;
51+
isOverSomedayMonth: boolean;
52+
selectionStart: Coordinates | null;
53+
isOverAllDayRow: boolean;
54+
isOpenAtMouse: boolean;
55+
openChange$: Subject<OpenChangeParams>;
56+
floating: (Floating & UseInteractionsReturn) | null;
57+
mousePointRef: ReferenceType | null;
58+
setOpenAtMousePosition: Dispatch<React.SetStateAction<boolean>>;
59+
toggleMouseMovementTracking: (pauseTracking?: boolean) => void;
60+
}
61+
62+
const openChange$ = new Subject<OpenChangeParams>();
63+
64+
export const MousePositionContext = createContext<MousePosition>({
65+
caret: null,
66+
element: null,
67+
floating: null,
68+
mousedown: false,
69+
isOverGrid: false,
70+
isOverSidebar: false,
71+
openChange$,
72+
mouseCoords: { x: 0, y: 0 },
73+
mousePointRef: null,
74+
isOverMainGrid: false,
75+
isOpenAtMouse: false,
76+
selectionStart: null,
77+
isOverAllDayRow: false,
78+
isOverSomedayWeek: false,
79+
isOverSomedayMonth: false,
80+
setOpenAtMousePosition: () => {}, // no-op fn
81+
toggleMouseMovementTracking: () => {}, // no-op fn
82+
});
83+
84+
export function MousePositionProvider({ children }: PropsWithChildren<{}>) {
85+
const [isOverSidebar, setIsOverSidebar] = useState(false);
86+
const [isOverSomedayWeek, setIsOverSomedayWeek] = useState(false);
87+
const [isOverSomedayMonth, setIsOverSomedayMonth] = useState(false);
88+
const [isOverGrid, setIsOverGrid] = useState(false);
89+
const [isOverMainGrid, setIsOverMainGrid] = useState(false);
90+
const [isOverAllDayRow, setIsOverAllDayRow] = useState(false);
91+
const [mouseCoords, setMouseCoords] = useState<Coordinates>({ x: 0, y: 0 });
92+
const [caret, setCaretPosition] = useState<CaretPosition | null>(null);
93+
const [mousedown, setMousedown] = useState<boolean>(false);
94+
const [selectionStart, setStart] = useState<Coordinates | null>(null);
95+
const [element, setElement] = useState<Element | null>(null);
96+
const [isOpenAtMouse, setOpenAtMousePosition] = useState<boolean>(false);
97+
const [strategy, setStrategy] = useState<Strategy>("fixed");
98+
const [placement, setPlacement] = useState<Placement>("right-start");
99+
100+
const { x, y } = mouseCoords;
101+
102+
const handler = useCallback(
103+
({ x, y, element, mousedown, caret, selectionStart }: DomMovement) => {
104+
const overSidebar = !!element?.closest(`#${ID_SIDEBAR}`);
105+
const overSomedayWeek = !!element?.closest(`#${COLUMN_WEEK}`);
106+
const overSomedayMonth = !!element?.closest(`#${COLUMN_MONTH}`);
107+
const overAllDayRow = !!element?.closest(`#${ID_GRID_ALLDAY_ROW}`);
108+
const overMainGrid = !!element?.closest(`#${ID_GRID_MAIN}`);
109+
const overGrid = overAllDayRow || overMainGrid;
110+
111+
setIsOverSidebar(overSidebar);
112+
setIsOverSomedayWeek(overSomedayWeek);
113+
setIsOverSomedayMonth(overSomedayMonth);
114+
setIsOverGrid(overGrid);
115+
setIsOverAllDayRow(overAllDayRow);
116+
setIsOverMainGrid(overMainGrid);
117+
setMouseCoords({ x, y });
118+
setCaretPosition(caret);
119+
setMousedown(mousedown);
120+
setElement(element);
121+
setStrategy("fixed");
122+
setPlacement("right-start");
123+
setStart({
124+
x: selectionStart?.clientX ?? 0,
125+
y: selectionStart?.clientY ?? 0,
126+
});
127+
128+
if (overSidebar) setStrategy("absolute");
129+
if (overSomedayMonth) setPlacement("right");
130+
},
131+
[
132+
setIsOverSidebar,
133+
setIsOverSomedayWeek,
134+
setIsOverSomedayMonth,
135+
setIsOverGrid,
136+
setIsOverAllDayRow,
137+
setIsOverMainGrid,
138+
setMouseCoords,
139+
setCaretPosition,
140+
setMousedown,
141+
setElement,
142+
setStrategy,
143+
setPlacement,
144+
setStart,
145+
],
146+
);
147+
148+
const { toggleMouseMovementTracking } = useMovementEvent({
149+
selectors: [`#${ID_ROOT}`],
150+
handler,
151+
deps: [
152+
setIsOverSidebar,
153+
setIsOverGrid,
154+
setIsOverAllDayRow,
155+
setIsOverMainGrid,
156+
setMouseCoords,
157+
setCaretPosition,
158+
setMousedown,
159+
],
160+
});
161+
162+
const floating = useFloating({
163+
placement,
164+
strategy,
165+
middleware: [
166+
flip({
167+
fallbackPlacements: [
168+
"right-start",
169+
"right",
170+
"left-start",
171+
"left",
172+
"top-start",
173+
"bottom-start",
174+
"top",
175+
"bottom",
176+
],
177+
fallbackStrategy: "bestFit",
178+
}),
179+
offset(7),
180+
shift(),
181+
],
182+
open: isOpenAtMouse,
183+
onOpenChange: (open, event, reason) => {
184+
setOpenAtMousePosition(open);
185+
openChange$.next([open, event, reason]);
186+
},
187+
whileElementsMounted: autoUpdate,
188+
});
189+
190+
const dismiss = useDismiss(floating.context);
191+
const interactions = useInteractions([dismiss]);
192+
193+
const mousePointRef = useMemo<ReferenceType>(
194+
() => ({
195+
getBoundingClientRect: () => ({
196+
x,
197+
y,
198+
top: y,
199+
left: x,
200+
bottom: y,
201+
right: x,
202+
width: 0,
203+
height: 0,
204+
toJSON: () => ({}),
205+
}),
206+
}),
207+
[x, y],
208+
);
209+
210+
return (
211+
<MousePositionContext.Provider
212+
value={{
213+
caret,
214+
element,
215+
floating: { ...floating, ...interactions },
216+
mousedown,
217+
isOverGrid,
218+
mouseCoords,
219+
openChange$,
220+
mousePointRef,
221+
isOpenAtMouse,
222+
isOverSidebar,
223+
isOverMainGrid,
224+
selectionStart,
225+
isOverAllDayRow,
226+
isOverSomedayWeek,
227+
isOverSomedayMonth,
228+
setOpenAtMousePosition,
229+
toggleMouseMovementTracking,
230+
}}
231+
>
232+
{children}
233+
</MousePositionContext.Provider>
234+
);
235+
}

0 commit comments

Comments
 (0)