#756 improve swipe gesture

This commit is contained in:
lethemanh
2026-04-16 17:33:13 +07:00
committed by Benoit TELLIER
parent 3d07008a8c
commit fd2282a65f
3 changed files with 110 additions and 88 deletions
+6 -10
View File
@@ -377,12 +377,11 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
errorHandler: errorHandler.current
})
const { offsetX, isAnimating, onTouchStart, onTouchMove, onTouchEnd } =
useSwipeCalendar({
calendarRef,
containerRef: calendarWrapperRef,
isMobile
})
const { offsetX, isAnimating } = useSwipeCalendar({
calendarRef,
containerRef: calendarWrapperRef,
isMobile
})
useEffect(() => {
if (process.env.NODE_ENV === 'test') {
@@ -441,10 +440,7 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
<div
ref={calendarWrapperRef}
className="calendar-viewport"
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
style={{ height: '100%' }}
style={{ height: '100%', touchAction: 'pan-y' }}
>
<div
className={`calendar-track ${isAnimating ? 'calendar-track--animate' : ''}`}
@@ -17,28 +17,17 @@ export const useSwipeAnimation = (
const finalizeSwipe = useCallback(
(targetOffset: number, deltaX: number) => {
setIsAnimating(true)
setOffsetX(targetOffset)
if (deltaX > 0) {
calendarRef.current?.prev()
} else {
calendarRef.current?.next()
}
setOffsetX(0)
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)
}, 300)
},
[calendarRef]
)
@@ -1,10 +1,4 @@
import {
useRef,
useState,
useCallback,
MutableRefObject,
RefObject
} from 'react'
import { useRef, useState, useEffect, MutableRefObject, RefObject } from 'react'
import { CalendarApi } from '@fullcalendar/core'
import { useSwipeAnimation } from './useSwipeAnimation'
@@ -17,25 +11,70 @@ interface UseSwipeCalendarProps {
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 isHorizontalSwipe = (dx: number, dy: number): boolean =>
Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 10
const getTargetOffset = (
deltaX: number,
containerWidth: number,
threshold: number
const getSwipeTarget = (
offsetX: number,
containerWidth: number
): number | null => {
if (Math.abs(deltaX) <= threshold) return null
return deltaX > 0 ? containerWidth : -containerWidth
if (Math.abs(offsetX) <= containerWidth * 0.1) return null
return offsetX > 0 ? containerWidth : -containerWidth
}
const preventSwipping = (): void => {}
const handleTouchMove = (
e: TouchEvent,
touchStartRef: React.MutableRefObject<{ x: number; y: number } | null>,
isHorizontalRef: React.MutableRefObject<boolean>,
setOffsetX: (dx: number) => void
): void => {
if (!touchStartRef.current) return
const dx = e.touches[0].clientX - touchStartRef.current.x
const dy = e.touches[0].clientY - touchStartRef.current.y
if (!isHorizontalRef.current)
isHorizontalRef.current = isHorizontalSwipe(dx, dy)
if (isHorizontalRef.current && e.cancelable) {
e.preventDefault()
setOffsetX(dx)
}
}
const handleTouchEnd = ({
touchStartRef,
isHorizontalRef,
calendarRef,
offsetX,
containerWidth,
setOffsetX,
finalizeSwipe,
cancelSwipe
}: {
touchStartRef: React.MutableRefObject<{ x: number; y: number } | null>
isHorizontalRef: React.MutableRefObject<boolean>
calendarRef: MutableRefObject<CalendarApi | null>
offsetX: number
containerWidth: number
setOffsetX: (dx: number) => void
finalizeSwipe: (target: number, currentOffset: number) => void
cancelSwipe: () => void
}): void => {
const hasValidTouch = touchStartRef.current && calendarRef.current
if (!hasValidTouch) {
setOffsetX(0)
touchStartRef.current = null
isHorizontalRef.current = false
return
}
if (isHorizontalRef.current) {
const target = getSwipeTarget(offsetX, containerWidth)
if (target !== null) finalizeSwipe(target, offsetX)
else cancelSwipe()
}
touchStartRef.current = null
isHorizontalRef.current = false
}
export const useSwipeCalendar = ({
calendarRef,
@@ -46,57 +85,55 @@ export const useSwipeCalendar = ({
useSwipeAnimation(calendarRef)
const [containerWidth, setContainerWidth] = useState(0)
const touchStartRef = useRef<{ x: number; y: number } | null>(null)
const isHorizontalRef = useRef<boolean>(false)
const onTouchStart = useCallback(
(e: React.TouchEvent) => {
if (containerRef.current)
setContainerWidth(containerRef.current.getBoundingClientRect().width)
useEffect(() => {
const el = containerRef.current
if (!el || !isMobile) return
const onTouchStart = (e: TouchEvent): void => {
setContainerWidth(el.getBoundingClientRect().width)
touchStartRef.current = {
x: e.touches[0].clientX,
y: e.touches[0].clientY
}
},
[containerRef]
)
isHorizontalRef.current = false
}
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 onTouchMove = (e: TouchEvent): void =>
handleTouchMove(e, touchStartRef, isHorizontalRef, 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
const onTouchEnd = (): void =>
handleTouchEnd({
touchStartRef,
isHorizontalRef,
calendarRef,
offsetX,
containerWidth,
setOffsetX,
finalizeSwipe,
cancelSwipe
})
el.addEventListener('touchstart', onTouchStart, { passive: true })
el.addEventListener('touchmove', onTouchMove, { passive: false })
el.addEventListener('touchend', onTouchEnd, { passive: true })
return (): void => {
el.removeEventListener('touchstart', onTouchStart)
el.removeEventListener('touchmove', onTouchMove)
el.removeEventListener('touchend', onTouchEnd)
}
}, [
calendarRef,
containerRef,
isMobile,
offsetX,
containerWidth,
setOffsetX,
finalizeSwipe,
cancelSwipe,
setOffsetX
calendarRef
])
if (!isMobile) {
return {
offsetX: 0,
isAnimating: false,
onTouchStart: preventSwipping,
onTouchMove: preventSwipping,
onTouchEnd: preventSwipping
}
}
return { offsetX, isAnimating, onTouchStart, onTouchMove, onTouchEnd }
return { offsetX, isAnimating }
}