#756 swipe gesture on calendar
This commit is contained in:
committed by
Benoit TELLIER
parent
b481eefed0
commit
3d07008a8c
@@ -133,4 +133,41 @@
|
|||||||
.weekSelector
|
.weekSelector
|
||||||
display flex
|
display flex
|
||||||
flex-direction column
|
flex-direction column
|
||||||
align-items center
|
align-items center
|
||||||
|
.calendar-viewport
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.calendar-track
|
||||||
|
display flex
|
||||||
|
width 300%
|
||||||
|
height 100%
|
||||||
|
margin-left -100%
|
||||||
|
will-change transform
|
||||||
|
|
||||||
|
.calendar-track--animate
|
||||||
|
transition transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)
|
||||||
|
|
||||||
|
.calendar-page
|
||||||
|
flex 0 0 33.333%
|
||||||
|
width 33.333%
|
||||||
|
height 100%
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
background-color inherit
|
||||||
|
|
||||||
|
.calendar-page-placeholder
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
background-color #f8f9fa
|
||||||
|
|
||||||
|
.placeholder-content
|
||||||
|
text-align center
|
||||||
|
|
||||||
|
[data-theme="dark"] .calendar-page-placeholder
|
||||||
|
background-color #1e1e1e
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
} from './utils/calendarUtils'
|
} from './utils/calendarUtils'
|
||||||
import { CALENDAR_VIEWS } from './utils/constants'
|
import { CALENDAR_VIEWS } from './utils/constants'
|
||||||
import ViewMoreEvents from './ViewMoreEvents'
|
import ViewMoreEvents from './ViewMoreEvents'
|
||||||
|
import { useSwipeCalendar } from './hooks/useSwipeCalendar'
|
||||||
|
|
||||||
const localeMap: Record<string, LocaleInput | undefined> = {
|
const localeMap: Record<string, LocaleInput | undefined> = {
|
||||||
fr: frLocale,
|
fr: frLocale,
|
||||||
@@ -84,6 +85,7 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
|
|||||||
return (): void => clearTimeout(t)
|
return (): void => clearTimeout(t)
|
||||||
}, [selectedDate])
|
}, [selectedDate])
|
||||||
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date())
|
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date())
|
||||||
|
const calendarWrapperRef = useRef<HTMLDivElement>(null)
|
||||||
const userId = useAppSelector(state => state.user.userData?.openpaasId) ?? ''
|
const userId = useAppSelector(state => state.user.userData?.openpaasId) ?? ''
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const view = useAppSelector(state => state.settings.view)
|
const view = useAppSelector(state => state.settings.view)
|
||||||
@@ -375,6 +377,13 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
|
|||||||
errorHandler: errorHandler.current
|
errorHandler: errorHandler.current
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { offsetX, isAnimating, onTouchStart, onTouchMove, onTouchEnd } =
|
||||||
|
useSwipeCalendar({
|
||||||
|
calendarRef,
|
||||||
|
containerRef: calendarWrapperRef,
|
||||||
|
isMobile
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
window.__calendarRef = calendarRef
|
window.__calendarRef = calendarRef
|
||||||
@@ -382,7 +391,6 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
|
|||||||
}, [calendarRef])
|
}, [calendarRef])
|
||||||
|
|
||||||
const { t, lang } = useI18n()
|
const { t, lang } = useI18n()
|
||||||
const calendarWrapperRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
useTouchListener(
|
useTouchListener(
|
||||||
eventHandlers.handleDateSelect,
|
eventHandlers.handleDateSelect,
|
||||||
@@ -430,183 +438,204 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
|
|||||||
</Fab>
|
</Fab>
|
||||||
)}
|
)}
|
||||||
{view === 'calendar' && (
|
{view === 'calendar' && (
|
||||||
<div ref={calendarWrapperRef} style={{ height: '100%' }}>
|
<div
|
||||||
<FullCalendar
|
ref={calendarWrapperRef}
|
||||||
key={hiddenDays.join(',')}
|
className="calendar-viewport"
|
||||||
ref={ref => {
|
onTouchStart={onTouchStart}
|
||||||
if (ref) {
|
onTouchMove={onTouchMove}
|
||||||
calendarRef.current = ref.getApi()
|
onTouchEnd={onTouchEnd}
|
||||||
}
|
style={{ height: '100%' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`calendar-track ${isAnimating ? 'calendar-track--animate' : ''}`}
|
||||||
|
style={{
|
||||||
|
transform: `translateX(${offsetX}px)`,
|
||||||
|
transition: isAnimating
|
||||||
|
? 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||||
|
: 'none'
|
||||||
}}
|
}}
|
||||||
plugins={[
|
>
|
||||||
dayGridPlugin,
|
<div className="calendar-page" />
|
||||||
timeGridPlugin,
|
<div className="calendar-page">
|
||||||
interactionPlugin,
|
<FullCalendar
|
||||||
momentTimezonePlugin
|
key={hiddenDays.join(',')}
|
||||||
]}
|
ref={ref => {
|
||||||
initialView={
|
if (ref) {
|
||||||
currentView ||
|
calendarRef.current = ref.getApi()
|
||||||
(isTablet
|
}
|
||||||
? CALENDAR_VIEWS.timeGridDay
|
}}
|
||||||
: CALENDAR_VIEWS.timeGridWeek)
|
plugins={[
|
||||||
}
|
dayGridPlugin,
|
||||||
firstDay={1}
|
timeGridPlugin,
|
||||||
editable={true}
|
interactionPlugin,
|
||||||
locale={localeMap[lang]}
|
momentTimezonePlugin
|
||||||
hiddenDays={hiddenDays}
|
]}
|
||||||
selectable={true}
|
initialView={
|
||||||
timeZone={timezone}
|
currentView ||
|
||||||
height="100%"
|
(isTablet
|
||||||
select={eventHandlers.handleDateSelect}
|
? CALENDAR_VIEWS.timeGridDay
|
||||||
nowIndicator
|
: CALENDAR_VIEWS.timeGridWeek)
|
||||||
slotLabelClassNames={arg => [
|
}
|
||||||
updateSlotLabelVisibility(new Date(), arg, timezone)
|
firstDay={1}
|
||||||
]}
|
editable={true}
|
||||||
nowIndicatorContent={viewHandlers.handleNowIndicatorContent}
|
locale={localeMap[lang]}
|
||||||
headerToolbar={false}
|
hiddenDays={hiddenDays}
|
||||||
views={{
|
selectable={true}
|
||||||
timeGridWeek: {
|
timeZone={timezone}
|
||||||
titleFormat: { month: 'long', year: 'numeric' }
|
height="100%"
|
||||||
}
|
select={eventHandlers.handleDateSelect}
|
||||||
}}
|
nowIndicator
|
||||||
dayMaxEvents={true}
|
slotLabelClassNames={arg => [
|
||||||
moreLinkClick={handleMoreLinkClick}
|
updateSlotLabelVisibility(new Date(), arg, timezone)
|
||||||
events={eventToFullCalendarFormat(
|
]}
|
||||||
filteredEvents,
|
nowIndicatorContent={viewHandlers.handleNowIndicatorContent}
|
||||||
filteredTempEvents,
|
headerToolbar={false}
|
||||||
userId,
|
views={{
|
||||||
userData?.email,
|
timeGridWeek: {
|
||||||
isPending,
|
titleFormat: { month: 'long', year: 'numeric' }
|
||||||
calendars
|
}
|
||||||
)}
|
}}
|
||||||
eventOrder={(a: EventApi, b: EventApi) =>
|
dayMaxEvents={true}
|
||||||
a.extendedProps.priority - b.extendedProps.priority
|
moreLinkClick={handleMoreLinkClick}
|
||||||
}
|
events={eventToFullCalendarFormat(
|
||||||
weekNumbers={
|
filteredEvents,
|
||||||
currentView === CALENDAR_VIEWS.timeGridWeek ||
|
filteredTempEvents,
|
||||||
currentView === CALENDAR_VIEWS.timeGridDay
|
userId,
|
||||||
}
|
userData?.email,
|
||||||
weekNumberFormat={{ week: 'long' }}
|
isPending,
|
||||||
weekNumberContent={arg => {
|
calendars
|
||||||
return (
|
)}
|
||||||
<div className="weekSelector">
|
eventOrder={(a: EventApi, b: EventApi) =>
|
||||||
{displayWeekNumbers && (
|
a.extendedProps.priority - b.extendedProps.priority
|
||||||
<div>
|
}
|
||||||
{t('menubar.views.week')} {arg.num}
|
weekNumbers={
|
||||||
|
currentView === CALENDAR_VIEWS.timeGridWeek ||
|
||||||
|
currentView === CALENDAR_VIEWS.timeGridDay
|
||||||
|
}
|
||||||
|
weekNumberFormat={{ week: 'long' }}
|
||||||
|
weekNumberContent={arg => {
|
||||||
|
return (
|
||||||
|
<div className="weekSelector">
|
||||||
|
{displayWeekNumbers && (
|
||||||
|
<div>
|
||||||
|
{t('menubar.views.week')} {arg.num}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<TimezoneSelector
|
||||||
|
value={timezone}
|
||||||
|
referenceDate={
|
||||||
|
calendarRef.current?.getDate() ?? new Date()
|
||||||
|
}
|
||||||
|
onChange={(newTimezone: string) =>
|
||||||
|
dispatch(setTimeZone(newTimezone))
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
<TimezoneSelector
|
}}
|
||||||
value={timezone}
|
dayCellContent={arg => {
|
||||||
referenceDate={
|
const month = arg.date.toLocaleDateString(t('locale'), {
|
||||||
calendarRef.current?.getDate() ?? new Date()
|
month: 'short',
|
||||||
}
|
timeZone: timezone
|
||||||
onChange={(newTimezone: string) =>
|
})
|
||||||
dispatch(setTimeZone(newTimezone))
|
if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) {
|
||||||
}
|
return (
|
||||||
/>
|
<span
|
||||||
</div>
|
className={`fc-daygrid-day-number ${
|
||||||
)
|
arg.isToday ? 'current-date' : ''
|
||||||
}}
|
}`}
|
||||||
dayCellContent={arg => {
|
>
|
||||||
const month = arg.date.toLocaleDateString(t('locale'), {
|
{arg.dayNumberText === '1' ? month : ''}{' '}
|
||||||
month: 'short',
|
{arg.dayNumberText}
|
||||||
timeZone: timezone
|
</span>
|
||||||
})
|
)
|
||||||
if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) {
|
}
|
||||||
return (
|
}}
|
||||||
<span
|
slotDuration="00:30:00"
|
||||||
className={`fc-daygrid-day-number ${
|
slotLabelInterval="01:00:00"
|
||||||
arg.isToday ? 'current-date' : ''
|
scrollTime="12:00:00"
|
||||||
}`}
|
unselectAuto={false}
|
||||||
>
|
allDayText=""
|
||||||
{arg.dayNumberText === '1' ? month : ''}{' '}
|
slotLabelFormat={{
|
||||||
{arg.dayNumberText}
|
hour: '2-digit',
|
||||||
</span>
|
minute: '2-digit',
|
||||||
)
|
hour12: false
|
||||||
}
|
}}
|
||||||
}}
|
datesSet={arg => {
|
||||||
slotDuration="00:30:00"
|
onViewChange?.(arg.view.type)
|
||||||
slotLabelInterval="01:00:00"
|
const calendarCurrentDate =
|
||||||
scrollTime="12:00:00"
|
calendarRef.current?.getDate() || new Date(arg.start)
|
||||||
unselectAuto={false}
|
setDisplayedDateAndRange(calendarCurrentDate)
|
||||||
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) {
|
if (arg.view.type === CALENDAR_VIEWS.dayGridMonth) {
|
||||||
const start = new Date(arg.start).getTime()
|
const start = new Date(arg.start).getTime()
|
||||||
const end = new Date(arg.end).getTime()
|
const end = new Date(arg.end).getTime()
|
||||||
const middle = start + (end - start) / 2
|
const middle = start + (end - start) / 2
|
||||||
setSelectedDate(new Date(middle))
|
setSelectedDate(new Date(middle))
|
||||||
setSelectedMiniDate(calendarCurrentDate)
|
setSelectedMiniDate(calendarCurrentDate)
|
||||||
} else {
|
} else {
|
||||||
setSelectedDate(calendarCurrentDate)
|
setSelectedDate(calendarCurrentDate)
|
||||||
setSelectedMiniDate(calendarCurrentDate)
|
setSelectedMiniDate(calendarCurrentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always use the calendar's current date for consistency
|
// Always use the calendar's current date for consistency
|
||||||
if (onDateChange) {
|
if (onDateChange) {
|
||||||
onDateChange(calendarCurrentDate)
|
onDateChange(calendarCurrentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify parent about view change
|
// Notify parent about view change
|
||||||
setCurrentView(arg.view.type)
|
setCurrentView(arg.view.type)
|
||||||
|
|
||||||
// Update slot label visibility when view changes
|
// Update slot label visibility when view changes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateSlotLabelVisibility(new Date())
|
updateSlotLabelVisibility(new Date())
|
||||||
}, 100)
|
}, 100)
|
||||||
}}
|
}}
|
||||||
dayHeaderContent={arg => {
|
dayHeaderContent={arg => {
|
||||||
const m = moment.tz(arg.date, timezone)
|
const m = moment.tz(arg.date, timezone)
|
||||||
|
|
||||||
const date = m.date()
|
const date = m.date()
|
||||||
const weekDay = m
|
const weekDay = m
|
||||||
.toDate()
|
.toDate()
|
||||||
.toLocaleDateString(t('locale'), {
|
.toLocaleDateString(t('locale'), {
|
||||||
weekday: 'short',
|
weekday: 'short',
|
||||||
timeZone: timezone
|
timeZone: timezone
|
||||||
})
|
})
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fc-daygrid-day-top ${isTablet || isMobile ? 'fc-daygrid-day-top--mobile' : ''}`}
|
className={`fc-daygrid-day-top ${isTablet || isMobile ? 'fc-daygrid-day-top--mobile' : ''}`}
|
||||||
>
|
|
||||||
<small>{weekDay}</small>
|
|
||||||
{arg.view.type !== CALENDAR_VIEWS.dayGridMonth && (
|
|
||||||
<span
|
|
||||||
className={`fc-daygrid-day-number ${arg.isToday ? 'current-date' : ''}`}
|
|
||||||
>
|
>
|
||||||
{date}
|
<small>{weekDay}</small>
|
||||||
</span>
|
{arg.view.type !== CALENDAR_VIEWS.dayGridMonth && (
|
||||||
)}
|
<span
|
||||||
</div>
|
className={`fc-daygrid-day-number ${arg.isToday ? 'current-date' : ''}`}
|
||||||
)
|
>
|
||||||
}}
|
{date}
|
||||||
dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount}
|
</span>
|
||||||
dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount}
|
)}
|
||||||
viewDidMount={viewHandlers.handleViewDidMount}
|
</div>
|
||||||
viewWillUnmount={viewHandlers.handleViewWillUnmount}
|
)
|
||||||
eventClick={eventHandlers.handleEventClick}
|
}}
|
||||||
eventAllow={eventHandlers.handleEventAllow}
|
dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount}
|
||||||
eventDrop={arg => {
|
dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount}
|
||||||
void eventHandlers.handleEventDrop(arg)
|
viewDidMount={viewHandlers.handleViewDidMount}
|
||||||
}}
|
viewWillUnmount={viewHandlers.handleViewWillUnmount}
|
||||||
eventResize={arg => {
|
eventClick={eventHandlers.handleEventClick}
|
||||||
void eventHandlers.handleEventResize(arg)
|
eventAllow={eventHandlers.handleEventAllow}
|
||||||
}}
|
eventDrop={arg => {
|
||||||
eventContent={viewHandlers.handleEventContent}
|
void eventHandlers.handleEventDrop(arg)
|
||||||
eventDidMount={viewHandlers.handleEventDidMount}
|
}}
|
||||||
/>
|
eventResize={arg => {
|
||||||
|
void eventHandlers.handleEventResize(arg)
|
||||||
|
}}
|
||||||
|
eventContent={viewHandlers.handleEventContent}
|
||||||
|
eventDidMount={viewHandlers.handleEventDidMount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="calendar-page" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{view === 'search' && <SearchResultsPage />}
|
{view === 'search' && <SearchResultsPage />}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useState, useCallback, MutableRefObject } from 'react'
|
||||||
|
import { CalendarApi } from '@fullcalendar/core'
|
||||||
|
|
||||||
|
export const useSwipeAnimation = (
|
||||||
|
calendarRef: MutableRefObject<CalendarApi | null>
|
||||||
|
): {
|
||||||
|
offsetX: number
|
||||||
|
isAnimating: boolean
|
||||||
|
setOffsetX: (offset: number) => void
|
||||||
|
finalizeSwipe: (targetOffset: number, deltaX: number) => void
|
||||||
|
cancelSwipe: () => void
|
||||||
|
setIsAnimating: (isAnimating: boolean) => void
|
||||||
|
} => {
|
||||||
|
const [offsetX, setOffsetX] = useState(0)
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false)
|
||||||
|
|
||||||
|
const finalizeSwipe = useCallback(
|
||||||
|
(targetOffset: number, deltaX: number) => {
|
||||||
|
setIsAnimating(true)
|
||||||
|
setOffsetX(targetOffset)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsAnimating(false)
|
||||||
|
setOffsetX(-targetOffset)
|
||||||
|
|
||||||
|
// Apply date change
|
||||||
|
if (deltaX > 0) {
|
||||||
|
calendarRef.current?.prev()
|
||||||
|
} else {
|
||||||
|
calendarRef.current?.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slide back to center (requesting animation state)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsAnimating(true)
|
||||||
|
setOffsetX(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Immediate reset to preserve "snappy" behavior as per latest user tweak
|
||||||
|
setIsAnimating(false)
|
||||||
|
}, 100)
|
||||||
|
},
|
||||||
|
[calendarRef]
|
||||||
|
)
|
||||||
|
|
||||||
|
const cancelSwipe = useCallback(() => {
|
||||||
|
setIsAnimating(true)
|
||||||
|
setOffsetX(0)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsAnimating(false)
|
||||||
|
}, 150)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
offsetX,
|
||||||
|
isAnimating,
|
||||||
|
setOffsetX,
|
||||||
|
setIsAnimating,
|
||||||
|
finalizeSwipe,
|
||||||
|
cancelSwipe
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
MutableRefObject,
|
||||||
|
RefObject
|
||||||
|
} from 'react'
|
||||||
|
import { CalendarApi } from '@fullcalendar/core'
|
||||||
|
import { useSwipeAnimation } from './useSwipeAnimation'
|
||||||
|
|
||||||
|
interface UseSwipeCalendarProps {
|
||||||
|
calendarRef: MutableRefObject<CalendarApi | null>
|
||||||
|
containerRef: RefObject<HTMLElement>
|
||||||
|
isMobile: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SwipeCalendarReturn {
|
||||||
|
offsetX: number
|
||||||
|
isAnimating: boolean
|
||||||
|
onTouchStart: (e: React.TouchEvent) => void
|
||||||
|
onTouchMove: (e: React.TouchEvent) => void
|
||||||
|
onTouchEnd: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHorizontalSwipe = (deltaX: number, deltaY: number): boolean => {
|
||||||
|
return Math.abs(deltaX) > Math.abs(deltaY) || Math.abs(deltaX) > 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTargetOffset = (
|
||||||
|
deltaX: number,
|
||||||
|
containerWidth: number,
|
||||||
|
threshold: number
|
||||||
|
): number | null => {
|
||||||
|
if (Math.abs(deltaX) <= threshold) return null
|
||||||
|
return deltaX > 0 ? containerWidth : -containerWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
const preventSwipping = (): void => {}
|
||||||
|
|
||||||
|
export const useSwipeCalendar = ({
|
||||||
|
calendarRef,
|
||||||
|
containerRef,
|
||||||
|
isMobile
|
||||||
|
}: UseSwipeCalendarProps): SwipeCalendarReturn => {
|
||||||
|
const { offsetX, isAnimating, setOffsetX, finalizeSwipe, cancelSwipe } =
|
||||||
|
useSwipeAnimation(calendarRef)
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0)
|
||||||
|
const touchStartRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
|
const onTouchStart = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
if (containerRef.current)
|
||||||
|
setContainerWidth(containerRef.current.getBoundingClientRect().width)
|
||||||
|
touchStartRef.current = {
|
||||||
|
x: e.touches[0].clientX,
|
||||||
|
y: e.touches[0].clientY
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[containerRef]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTouchMove = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
if (!touchStartRef.current) return
|
||||||
|
const dx = e.touches[0].clientX - touchStartRef.current.x
|
||||||
|
const dy = e.touches[0].clientY - touchStartRef.current.y
|
||||||
|
if (isHorizontalSwipe(dx, dy)) setOffsetX(dx)
|
||||||
|
},
|
||||||
|
[setOffsetX]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTouchEnd = useCallback(() => {
|
||||||
|
if (!touchStartRef.current || !calendarRef.current) return setOffsetX(0)
|
||||||
|
const target = getTargetOffset(
|
||||||
|
offsetX,
|
||||||
|
containerWidth,
|
||||||
|
containerWidth * 0.1
|
||||||
|
)
|
||||||
|
if (target !== null) finalizeSwipe(target, offsetX)
|
||||||
|
else cancelSwipe()
|
||||||
|
touchStartRef.current = null
|
||||||
|
}, [
|
||||||
|
calendarRef,
|
||||||
|
offsetX,
|
||||||
|
containerWidth,
|
||||||
|
finalizeSwipe,
|
||||||
|
cancelSwipe,
|
||||||
|
setOffsetX
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!isMobile) {
|
||||||
|
return {
|
||||||
|
offsetX: 0,
|
||||||
|
isAnimating: false,
|
||||||
|
onTouchStart: preventSwipping,
|
||||||
|
onTouchMove: preventSwipping,
|
||||||
|
onTouchEnd: preventSwipping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { offsetX, isAnimating, onTouchStart, onTouchMove, onTouchEnd }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user