#756 improve swipe gesture
This commit is contained in:
committed by
Benoit TELLIER
parent
3d07008a8c
commit
fd2282a65f
@@ -377,8 +377,7 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
|
||||
errorHandler: errorHandler.current
|
||||
})
|
||||
|
||||
const { offsetX, isAnimating, onTouchStart, onTouchMove, onTouchEnd } =
|
||||
useSwipeCalendar({
|
||||
const { offsetX, isAnimating } = useSwipeCalendar({
|
||||
calendarRef,
|
||||
containerRef: calendarWrapperRef,
|
||||
isMobile
|
||||
@@ -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)
|
||||
|
||||
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
|
||||
setTimeout(() => {
|
||||
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)
|
||||
}
|
||||
}, [
|
||||
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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user