1 import type { CSSProperties, MouseEventHandler, Ref } from 'react';
2 import { useEffect, useRef, useState } from 'react';
4 import { c } from 'ttag';
6 import { Button } from '@proton/atoms';
7 import { PrimaryButton, useBusySlotsAvailable } from '@proton/components';
8 import { useMailSettings } from '@proton/mail/mailSettings/hooks';
9 import type { VIEWS } from '@proton/shared/lib/calendar/constants';
10 import type { WeekStartsOn } from '@proton/shared/lib/date-fns-utc/interface';
11 import type { Address } from '@proton/shared/lib/interfaces';
12 import type { EventModel } from '@proton/shared/lib/interfaces/calendar';
13 import debounce from '@proton/utils/debounce';
14 import throttle from '@proton/utils/throttle';
16 import { getCannotSaveEvent } from '../../helpers/event';
17 import { useRect } from '../../hooks/useRect';
18 import type { InviteActions } from '../../interfaces/Invite';
19 import { INVITE_ACTION_TYPES } from '../../interfaces/Invite';
20 import { busySlotsActions } from '../../store/busySlots/busySlotsSlice';
21 import { useCalendarDispatch } from '../../store/hooks';
22 import PopoverContainer from '../events/PopoverContainer';
23 import PopoverFooter from '../events/PopoverFooter';
24 import PopoverHeader from '../events/PopoverHeader';
25 import EventForm from './EventForm';
26 import validateEventModel from './eventForm/validateEventModel';
27 import { ACTION, useForm } from './hooks/useForm';
30 isSmallViewport: boolean;
31 displayWeekNumbers: boolean;
32 weekStartsOn: WeekStartsOn;
35 onSave: (inviteActions: InviteActions) => Promise<void>;
37 onEdit: (value: EventModel) => void;
39 popoverRef: Ref<HTMLDivElement>;
40 setModel: (value: EventModel) => void;
41 isCreateEvent: boolean;
42 isInvitation: boolean;
43 isDraggingDisabled?: boolean;
44 isDrawerApp?: boolean;
48 const CreateEventPopover = ({
62 isDraggingDisabled = false,
66 const [mailSettings] = useMailSettings();
67 const dispatch = useCalendarDispatch();
68 const isBusySlotsAvailable = useBusySlotsAvailable();
69 const [participantError, setParticipantError] = useState(false);
70 const errors = { ...validateEventModel(model), participantError };
71 const cannotSave = getCannotSaveEvent({
72 isOwnedCalendar: model.calendar.isOwned,
73 isOrganizer: model.isOrganizer,
74 numberOfAttendees: model.attendees.length,
75 maxAttendees: mailSettings?.RecipientLimit,
77 const formRef = useRef<HTMLFormElement>(null);
78 const { isSubmitted, loadingAction, lastAction, handleSubmit } = useForm({
79 containerEl: formRef.current,
83 const formRect = useRect(formRef.current);
85 const handleMore = () => {
88 // new events have no uid yet
89 const inviteActions = {
90 // the type will be more properly assessed in getSaveEventActions by getCorrectedSaveEventActions
91 type: model.isAttendee ? INVITE_ACTION_TYPES.NONE : INVITE_ACTION_TYPES.SEND_INVITATION,
92 selfAddress: model.selfAddress,
95 const [isDragging, setIsDragging] = useState(false);
96 const [offset, setOffset] = useState({ x: 0, y: 0 });
97 const [initialPosition, setInitialPosition] = useState({ clientX: 0, clientY: 0 });
98 const { clientX, clientY } = initialPosition;
100 const dragStyle: CSSProperties = {
101 transform: `translate3d(${offset.x}px, ${offset.y}px, 0)`,
103 const mergedStyle = isSmallViewport ? dragStyle : { ...style, ...dragStyle };
105 const handleMouseDown: MouseEventHandler<HTMLElement> = (event) => {
106 event.preventDefault();
107 setInitialPosition(event);
111 const handleStopDragging = () => {
112 setIsDragging(false);
115 const handleMouseMove = (event: MouseEvent) => {
116 const prevOffset = offset;
117 const cursorMoveXOffset = event.clientX - clientX;
118 const cursorMoveYOffset = event.clientY - clientY;
120 setOffset(({ x, y }) => ({
122 event.clientX >= 0 && event.clientX <= document.documentElement.clientWidth
123 ? prevOffset.x + cursorMoveXOffset
126 event.clientY >= 0 && event.clientY <= document.documentElement.clientHeight
127 ? prevOffset.y + cursorMoveYOffset
133 if (isBusySlotsAvailable) {
134 dispatch(busySlotsActions.setDisplay(true));
143 // When there's an existing offset and added content e.g. a participant group
144 // causes the popover to go off screen, we adjust the offset to stick to the top
145 if (formRect.top < 0 && offset.y && !isDragging) {
146 // Debouncing lets us have a slightly better flicker
147 debounce(() => setOffset((prevState) => ({ ...prevState, y: prevState.y - formRect.top })), 50)();
152 if (isDraggingDisabled) {
156 const throttledMouseMove = throttle(handleMouseMove, 20);
157 const debouncedStopDragging = debounce(handleStopDragging, 50);
160 document.addEventListener('mousemove', throttledMouseMove);
161 document.addEventListener('mouseup', debouncedStopDragging);
162 document.addEventListener('mouseleave', debouncedStopDragging);
166 document.removeEventListener('mousemove', throttledMouseMove);
167 document.removeEventListener('mouseup', debouncedStopDragging);
168 document.removeEventListener('mouseleave', debouncedStopDragging);
170 }, [isDraggingDisabled, isDragging]);
173 <PopoverContainer style={mergedStyle} className="eventpopover" ref={popoverRef} onClose={onClose}>
177 handleSubmit(inviteActions);
179 className="form--icon-labels"
183 className={isDraggingDisabled ? '' : 'eventpopover-header--draggable'}
184 onMouseDown={handleMouseDown}
186 isDraggable={!isDraggingDisabled}
189 displayWeekNumbers={displayWeekNumbers}
190 addresses={addresses}
191 weekStartsOn={weekStartsOn}
192 isSubmitted={isSubmitted}
197 isCreateEvent={isCreateEvent}
198 isInvitation={isInvitation}
199 setParticipantError={setParticipantError}
200 isSmallViewport={isSmallViewport}
201 isDrawerApp={isDrawerApp}
204 <PopoverFooter className="justify-end flex-nowrap flex-column-reverse sm:flex-row gap-2">
206 disabled={loadingAction}
207 data-testid="create-event-popover:more-event-options"
208 className="w-full sm:w-auto"
210 >{c('Action').t`More options`}</Button>
212 data-testid="create-event-popover:save"
214 className={isDrawerApp ? 'w-full' : undefined}
215 loading={loadingAction && lastAction === ACTION.SUBMIT}
216 disabled={loadingAction || cannotSave}
218 {c('Action').t`Save`}
226 export default CreateEventPopover;