From 17e86d78327ca23d3374af1b3aab7d5bd589bb32 Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:12:31 +0200 Subject: [PATCH] [#786] added hook to handle tap separatly from slide (#787) * [#786] fully disable long press * [#786] added hook to handle tap separatly from slide --- src/components/Calendar/Calendar.tsx | 352 ++++++++++--------- src/components/Calendar/useTouchListener.tsx | 75 ++++ 2 files changed, 258 insertions(+), 169 deletions(-) create mode 100644 src/components/Calendar/useTouchListener.tsx diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index bb21ea4..05a61d4 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -1,4 +1,5 @@ import { useAppDispatch, useAppSelector } from '@/app/hooks' +import { setIsMobileSearchOpen } from '@/features/Calendars/CalendarSlice' import EventPopover from '@/features/Events/EventModal' import EventPreviewModal from '@/features/Events/EventPreview' import { CalendarEvent } from '@/features/Events/EventsTypes' @@ -32,20 +33,20 @@ import { EventErrorSnackbar } from '../Error/ErrorSnackbar' import { EventErrorHandler } from '../Error/EventErrorHandler' import { EditModeDialog } from '../Event/EditModeDialog' import { Menubar, MenubarProps } from '../Menubar/Menubar' +import { TimezoneSelector } from '../Timezone/TimezoneSelector' import './Calendar.styl' import './CustomCalendar.styl' import { useCalendarEventHandlers } from './hooks/useCalendarEventHandlers' import { useCalendarViewHandlers } from './hooks/useCalendarViewHandlers' -import { TimezoneSelector } from '../Timezone/TimezoneSelector' +import Sidebar from './Sidebar/SideBar' +import TempSearchDialog from './TempSearchDialog' +import { useTouchListener } from './useTouchListener' import { eventToFullCalendarFormat, extractEvents, updateSlotLabelVisibility } from './utils/calendarUtils' import { CALENDAR_VIEWS } from './utils/constants' -import Sidebar from './Sidebar/SideBar' -import TempSearchDialog from './TempSearchDialog' -import { setIsMobileSearchOpen } from '@/features/Calendars/CalendarSlice' import ViewMoreEvents from './ViewMoreEvents' const localeMap: Record = { @@ -381,6 +382,13 @@ const CalendarApp: React.FC = ({ }, [calendarRef]) const { t, lang } = useI18n() + const calendarWrapperRef = useRef(null) + + useTouchListener( + eventHandlers.handleDateSelect, + isTablet || isMobile, + calendarWrapperRef + ) return (
= ({
{menubarProps?.isIframe && } - {isTablet && ( + {(isTablet || isMobile) && ( = ({ )} {view === 'calendar' && ( - { - if (ref) { - calendarRef.current = ref.getApi() +
+ { + if (ref) { + calendarRef.current = ref.getApi() + } + }} + plugins={[ + dayGridPlugin, + timeGridPlugin, + interactionPlugin, + momentTimezonePlugin + ]} + initialView={ + currentView || + (isTablet + ? CALENDAR_VIEWS.timeGridDay + : CALENDAR_VIEWS.timeGridWeek) } - }} - plugins={[ - dayGridPlugin, - timeGridPlugin, - interactionPlugin, - momentTimezonePlugin - ]} - initialView={ - currentView || - (isTablet - ? CALENDAR_VIEWS.timeGridDay - : CALENDAR_VIEWS.timeGridWeek) - } - firstDay={1} - editable={true} - locale={localeMap[lang]} - hiddenDays={hiddenDays} - selectable={true} - timeZone={timezone} - height="100%" - select={eventHandlers.handleDateSelect} - longPressDelay={100} - nowIndicator - slotLabelClassNames={arg => [ - updateSlotLabelVisibility(new Date(), arg, timezone) - ]} - nowIndicatorContent={viewHandlers.handleNowIndicatorContent} - headerToolbar={false} - views={{ - timeGridWeek: { titleFormat: { month: 'long', year: 'numeric' } } - }} - dayMaxEvents={true} - moreLinkClick={handleMoreLinkClick} - events={eventToFullCalendarFormat( - filteredEvents, - filteredTempEvents, - userId, - userData?.email, - isPending, - calendars - )} - eventOrder={(a: EventApi, b: EventApi) => - a.extendedProps.priority - b.extendedProps.priority - } - weekNumbers={ - currentView === CALENDAR_VIEWS.timeGridWeek || - currentView === CALENDAR_VIEWS.timeGridDay - } - weekNumberFormat={{ week: 'long' }} - weekNumberContent={arg => { - return ( -
- {displayWeekNumbers && ( -
- {t('menubar.views.week')} {arg.num} -
- )} - - dispatch(setTimeZone(newTimezone)) - } - /> -
- ) - }} - dayCellContent={arg => { - const month = arg.date.toLocaleDateString(t('locale'), { - month: 'short', - timeZone: timezone - }) - if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) { + firstDay={1} + editable={true} + locale={localeMap[lang]} + hiddenDays={hiddenDays} + selectable={true} + timeZone={timezone} + height="100%" + select={eventHandlers.handleDateSelect} + nowIndicator + slotLabelClassNames={arg => [ + updateSlotLabelVisibility(new Date(), arg, timezone) + ]} + nowIndicatorContent={viewHandlers.handleNowIndicatorContent} + headerToolbar={false} + views={{ + timeGridWeek: { + titleFormat: { month: 'long', year: 'numeric' } + } + }} + dayMaxEvents={true} + moreLinkClick={handleMoreLinkClick} + events={eventToFullCalendarFormat( + filteredEvents, + filteredTempEvents, + userId, + userData?.email, + isPending, + calendars + )} + eventOrder={(a: EventApi, b: EventApi) => + a.extendedProps.priority - b.extendedProps.priority + } + weekNumbers={ + currentView === CALENDAR_VIEWS.timeGridWeek || + currentView === CALENDAR_VIEWS.timeGridDay + } + weekNumberFormat={{ week: 'long' }} + weekNumberContent={arg => { return ( - - {arg.dayNumberText === '1' ? month : ''} {arg.dayNumberText} - +
+ {displayWeekNumbers && ( +
+ {t('menubar.views.week')} {arg.num} +
+ )} + + dispatch(setTimeZone(newTimezone)) + } + /> +
) - } - }} - slotDuration="00:30:00" - slotLabelInterval="01:00:00" - scrollTime="12:00:00" - unselectAuto={false} - allDayText="" - slotLabelFormat={{ - hour: '2-digit', - minute: '2-digit', - hour12: false - }} - datesSet={arg => { - onViewChange?.(arg.view.type) - const calendarCurrentDate = - calendarRef.current?.getDate() || new Date(arg.start) - setDisplayedDateAndRange(calendarCurrentDate) - - if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) { - const start = new Date(arg.start).getTime() - const end = new Date(arg.end).getTime() - const middle = start + (end - start) / 2 - setSelectedDate(new Date(middle)) - setSelectedMiniDate(calendarCurrentDate) - } else { - setSelectedDate(calendarCurrentDate) - setSelectedMiniDate(calendarCurrentDate) - } - - // Always use the calendar's current date for consistency - if (onDateChange) { - onDateChange(calendarCurrentDate) - } - - // Notify parent about view change - setCurrentView(arg.view.type) - - // Update slot label visibility when view changes - setTimeout(() => { - updateSlotLabelVisibility(new Date()) - }, 100) - }} - dayHeaderContent={arg => { - const m = moment.tz(arg.date, timezone) - - const date = m.date() - const weekDay = m - .toDate() - .toLocaleDateString(t('locale'), { - weekday: 'short', + }} + dayCellContent={arg => { + const month = arg.date.toLocaleDateString(t('locale'), { + month: 'short', timeZone: timezone }) - .toUpperCase() - - return ( -
- {weekDay} - {arg.view.type !== CALENDAR_VIEWS.dayGridMonth && ( + if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) { + return ( - {date} + {arg.dayNumberText === '1' ? month : ''}{' '} + {arg.dayNumberText} - )} -
- ) - }} - dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount} - dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount} - viewDidMount={viewHandlers.handleViewDidMount} - viewWillUnmount={viewHandlers.handleViewWillUnmount} - eventClick={eventHandlers.handleEventClick} - eventAllow={eventHandlers.handleEventAllow} - eventDrop={arg => { - void eventHandlers.handleEventDrop(arg) - }} - eventResize={arg => { - void eventHandlers.handleEventResize(arg) - }} - eventContent={viewHandlers.handleEventContent} - eventDidMount={viewHandlers.handleEventDidMount} - /> + ) + } + }} + slotDuration="00:30:00" + slotLabelInterval="01:00:00" + scrollTime="12:00:00" + unselectAuto={false} + allDayText="" + slotLabelFormat={{ + hour: '2-digit', + minute: '2-digit', + hour12: false + }} + datesSet={arg => { + onViewChange?.(arg.view.type) + const calendarCurrentDate = + calendarRef.current?.getDate() || new Date(arg.start) + setDisplayedDateAndRange(calendarCurrentDate) + + if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) { + const start = new Date(arg.start).getTime() + const end = new Date(arg.end).getTime() + const middle = start + (end - start) / 2 + setSelectedDate(new Date(middle)) + setSelectedMiniDate(calendarCurrentDate) + } else { + setSelectedDate(calendarCurrentDate) + setSelectedMiniDate(calendarCurrentDate) + } + + // Always use the calendar's current date for consistency + if (onDateChange) { + onDateChange(calendarCurrentDate) + } + + // Notify parent about view change + setCurrentView(arg.view.type) + + // Update slot label visibility when view changes + setTimeout(() => { + updateSlotLabelVisibility(new Date()) + }, 100) + }} + dayHeaderContent={arg => { + const m = moment.tz(arg.date, timezone) + + const date = m.date() + const weekDay = m + .toDate() + .toLocaleDateString(t('locale'), { + weekday: 'short', + timeZone: timezone + }) + .toUpperCase() + + return ( +
+ {weekDay} + {arg.view.type !== CALENDAR_VIEWS.dayGridMonth && ( + + {date} + + )} +
+ ) + }} + dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount} + dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount} + viewDidMount={viewHandlers.handleViewDidMount} + viewWillUnmount={viewHandlers.handleViewWillUnmount} + eventClick={eventHandlers.handleEventClick} + eventAllow={eventHandlers.handleEventAllow} + eventDrop={arg => { + void eventHandlers.handleEventDrop(arg) + }} + eventResize={arg => { + void eventHandlers.handleEventResize(arg) + }} + eventContent={viewHandlers.handleEventContent} + eventDidMount={viewHandlers.handleEventDidMount} + /> +
)} {view === 'search' && } { + const rect = el.getBoundingClientRect() + return x >= rect.left && x <= rect.right + }) + return col?.getAttribute('data-date') ?? null +} + +function buildSelectArg(date: string, time: string): DateSelectArg { + const start = new Date(`${date}T${time}`) + const end = new Date(start.getTime() + 30 * 60 * 1000) + return { start, end, allDay: false } as DateSelectArg +} + +export function useTouchListener( + handleDateSelect: (selectInfo: DateSelectArg | null) => void, + isTouch: boolean, + wrapperRef: RefObject +): void { + useEffect(() => { + const el = wrapperRef.current + if (!el || !isTouch) return + + let startX = 0, + startY = 0, + startTime = 0 + + const onTouchStart = (e: TouchEvent): void => { + startX = e.touches[0].clientX + startY = e.touches[0].clientY + startTime = Date.now() + } + + const onTouchEnd = (e: TouchEvent): void => { + const touch = e.changedTouches[0] + const dx = Math.abs(touch.clientX - startX) + const dy = Math.abs(touch.clientY - startY) + + if (!isTapGesture(dx, dy, Date.now() - startTime)) return + + const time = getTimeAtPoint(touch.clientX, touch.clientY) + const date = getDateAtPoint(touch.clientX) + + if (time && date) { + handleDateSelect(buildSelectArg(date, time)) + } + } + + el.addEventListener('touchstart', onTouchStart, { passive: true }) + el.addEventListener('touchend', onTouchEnd, { passive: true }) + + return (): void => { + el.removeEventListener('touchstart', onTouchStart) + el.removeEventListener('touchend', onTouchEnd) + } + }, [handleDateSelect, isTouch, wrapperRef]) +}