#756 improve swipe gesture
This commit is contained in:
committed by
Benoit TELLIER
parent
3d07008a8c
commit
fd2282a65f
@@ -377,12 +377,11 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
|
|||||||
errorHandler: errorHandler.current
|
errorHandler: errorHandler.current
|
||||||
})
|
})
|
||||||
|
|
||||||
const { offsetX, isAnimating, onTouchStart, onTouchMove, onTouchEnd } =
|
const { offsetX, isAnimating } = useSwipeCalendar({
|
||||||
useSwipeCalendar({
|
calendarRef,
|
||||||
calendarRef,
|
containerRef: calendarWrapperRef,
|
||||||
containerRef: calendarWrapperRef,
|
isMobile
|
||||||
isMobile
|
})
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
@@ -441,10 +440,7 @@ const CalendarApp: React.FC<CalendarAppProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={calendarWrapperRef}
|
ref={calendarWrapperRef}
|
||||||
className="calendar-viewport"
|
className="calendar-viewport"
|
||||||
onTouchStart={onTouchStart}
|
style={{ height: '100%', touchAction: 'pan-y' }}
|
||||||
onTouchMove={onTouchMove}
|
|
||||||
onTouchEnd={onTouchEnd}
|
|
||||||
style={{ height: '100%' }}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`calendar-track ${isAnimating ? 'calendar-track--animate' : ''}`}
|
className={`calendar-track ${isAnimating ? 'calendar-track--animate' : ''}`}
|
||||||
|
|||||||
@@ -17,28 +17,17 @@ export const useSwipeAnimation = (
|
|||||||
const finalizeSwipe = useCallback(
|
const finalizeSwipe = useCallback(
|
||||||
(targetOffset: number, deltaX: number) => {
|
(targetOffset: number, deltaX: number) => {
|
||||||
setIsAnimating(true)
|
setIsAnimating(true)
|
||||||
setOffsetX(targetOffset)
|
|
||||||
|
|
||||||
|
if (deltaX > 0) {
|
||||||
|
calendarRef.current?.prev()
|
||||||
|
} else {
|
||||||
|
calendarRef.current?.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
setOffsetX(0)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsAnimating(false)
|
setIsAnimating(false)
|
||||||
setOffsetX(-targetOffset)
|
}, 300)
|
||||||
|
|
||||||
// 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]
|
[calendarRef]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { useRef, useState, useEffect, MutableRefObject, RefObject } from 'react'
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
MutableRefObject,
|
|
||||||
RefObject
|
|
||||||
} from 'react'
|
|
||||||
import { CalendarApi } from '@fullcalendar/core'
|
import { CalendarApi } from '@fullcalendar/core'
|
||||||
import { useSwipeAnimation } from './useSwipeAnimation'
|
import { useSwipeAnimation } from './useSwipeAnimation'
|
||||||
|
|
||||||
@@ -17,25 +11,70 @@ interface UseSwipeCalendarProps {
|
|||||||
interface SwipeCalendarReturn {
|
interface SwipeCalendarReturn {
|
||||||
offsetX: number
|
offsetX: number
|
||||||
isAnimating: boolean
|
isAnimating: boolean
|
||||||
onTouchStart: (e: React.TouchEvent) => void
|
|
||||||
onTouchMove: (e: React.TouchEvent) => void
|
|
||||||
onTouchEnd: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHorizontalSwipe = (deltaX: number, deltaY: number): boolean => {
|
const isHorizontalSwipe = (dx: number, dy: number): boolean =>
|
||||||
return Math.abs(deltaX) > Math.abs(deltaY) || Math.abs(deltaX) > 10
|
Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 10
|
||||||
}
|
|
||||||
|
|
||||||
const getTargetOffset = (
|
const getSwipeTarget = (
|
||||||
deltaX: number,
|
offsetX: number,
|
||||||
containerWidth: number,
|
containerWidth: number
|
||||||
threshold: number
|
|
||||||
): number | null => {
|
): number | null => {
|
||||||
if (Math.abs(deltaX) <= threshold) return null
|
if (Math.abs(offsetX) <= containerWidth * 0.1) return null
|
||||||
return deltaX > 0 ? containerWidth : -containerWidth
|
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 = ({
|
export const useSwipeCalendar = ({
|
||||||
calendarRef,
|
calendarRef,
|
||||||
@@ -46,57 +85,55 @@ export const useSwipeCalendar = ({
|
|||||||
useSwipeAnimation(calendarRef)
|
useSwipeAnimation(calendarRef)
|
||||||
const [containerWidth, setContainerWidth] = useState(0)
|
const [containerWidth, setContainerWidth] = useState(0)
|
||||||
const touchStartRef = useRef<{ x: number; y: number } | null>(null)
|
const touchStartRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
|
const isHorizontalRef = useRef<boolean>(false)
|
||||||
|
|
||||||
const onTouchStart = useCallback(
|
useEffect(() => {
|
||||||
(e: React.TouchEvent) => {
|
const el = containerRef.current
|
||||||
if (containerRef.current)
|
if (!el || !isMobile) return
|
||||||
setContainerWidth(containerRef.current.getBoundingClientRect().width)
|
|
||||||
|
const onTouchStart = (e: TouchEvent): void => {
|
||||||
|
setContainerWidth(el.getBoundingClientRect().width)
|
||||||
touchStartRef.current = {
|
touchStartRef.current = {
|
||||||
x: e.touches[0].clientX,
|
x: e.touches[0].clientX,
|
||||||
y: e.touches[0].clientY
|
y: e.touches[0].clientY
|
||||||
}
|
}
|
||||||
},
|
isHorizontalRef.current = false
|
||||||
[containerRef]
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const onTouchMove = useCallback(
|
const onTouchMove = (e: TouchEvent): void =>
|
||||||
(e: React.TouchEvent) => {
|
handleTouchMove(e, touchStartRef, isHorizontalRef, setOffsetX)
|
||||||
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(() => {
|
const onTouchEnd = (): void =>
|
||||||
if (!touchStartRef.current || !calendarRef.current) return setOffsetX(0)
|
handleTouchEnd({
|
||||||
const target = getTargetOffset(
|
touchStartRef,
|
||||||
offsetX,
|
isHorizontalRef,
|
||||||
containerWidth,
|
calendarRef,
|
||||||
containerWidth * 0.1
|
offsetX,
|
||||||
)
|
containerWidth,
|
||||||
if (target !== null) finalizeSwipe(target, offsetX)
|
setOffsetX,
|
||||||
else cancelSwipe()
|
finalizeSwipe,
|
||||||
touchStartRef.current = null
|
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,
|
offsetX,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
|
setOffsetX,
|
||||||
finalizeSwipe,
|
finalizeSwipe,
|
||||||
cancelSwipe,
|
cancelSwipe,
|
||||||
setOffsetX
|
calendarRef
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!isMobile) {
|
return { offsetX, isAnimating }
|
||||||
return {
|
|
||||||
offsetX: 0,
|
|
||||||
isAnimating: false,
|
|
||||||
onTouchStart: preventSwipping,
|
|
||||||
onTouchMove: preventSwipping,
|
|
||||||
onTouchEnd: preventSwipping
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { offsetX, isAnimating, onTouchStart, onTouchMove, onTouchEnd }
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user