94 lines
2.6 KiB
JavaScript
94 lines
2.6 KiB
JavaScript
import { useState, useRef, useCallback } from 'react';
|
|||
|
|
|
||
|
|
export default function SwipeableItem({
|
||
|
|
children,
|
||
|
|
onSwipeLeft,
|
||
|
|
renderRightActions,
|
||
|
|
swipeThreshold = 80,
|
||
|
|
}) {
|
||
|
|
const [isSwiping, setIsSwiping] = useState(false);
|
||
|
|
const [translateX, setTranslateX] = useState(0);
|
||
|
|
const [isRevealed, setIsRevealed] = useState(false);
|
||
|
|
const startXRef = useRef(0);
|
||
|
|
const currentXRef = useRef(0);
|
||
|
|
const itemRef = useRef(null);
|
||
|
|
|
||
|
|
const handleTouchStart = useCallback((e) => {
|
||
|
|
startXRef.current = e.touches[0].clientX;
|
||
|
|
currentXRef.current = startXRef.current;
|
||
|
|
setIsSwiping(true);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleTouchMove = useCallback((e) => {
|
||
|
|
if (!isSwiping) return;
|
||
|
|
|
||
|
|
currentXRef.current = e.touches[0].clientX;
|
||
|
|
const diff = currentXRef.current - startXRef.current;
|
||
|
|
|
||
|
|
// Only allow swiping left (negative diff)
|
||
|
|
if (diff < 0) {
|
||
|
|
// Limit the swipe distance
|
||
|
|
const newTranslateX = Math.max(diff, -swipeThreshold - 20);
|
||
|
|
setTranslateX(newTranslateX);
|
||
|
|
} else if (isRevealed) {
|
||
|
|
// If already revealed, allow swiping right to close
|
||
|
|
const newTranslateX = Math.min(0, -swipeThreshold + diff);
|
||
|
|
setTranslateX(newTranslateX);
|
||
|
|
}
|
||
|
|
}, [isSwiping, isRevealed, swipeThreshold]);
|
||
|
|
|
||
|
|
const handleTouchEnd = useCallback(() => {
|
||
|
|
setIsSwiping(false);
|
||
|
|
const diff = currentXRef.current - startXRef.current;
|
||
|
|
|
||
|
|
if (diff < -swipeThreshold / 2 || (isRevealed && diff > -swipeThreshold / 2)) {
|
||
|
|
// Reveal actions
|
||
|
|
setTranslateX(-swipeThreshold);
|
||
|
|
setIsRevealed(true);
|
||
|
|
} else {
|
||
|
|
// Snap back
|
||
|
|
setTranslateX(0);
|
||
|
|
setIsRevealed(false);
|
||
|
|
}
|
||
|
|
}, [isRevealed, swipeThreshold]);
|
||
|
|
|
||
|
|
const handleClose = useCallback(() => {
|
||
|
|
setTranslateX(0);
|
||
|
|
setIsRevealed(false);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleActionClick = useCallback((callback) => {
|
||
|
|
callback?.();
|
||
|
|
handleClose();
|
||
|
|
}, [handleClose]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="swipeable-item" ref={itemRef}>
|
||
|
|
<div
|
||
|
|
className={`swipeable-content ${isSwiping ? 'is-swiping' : ''}`}
|
||
|
|
style={{
|
||
|
|
transform: `translateX(${translateX}px)`,
|
||
|
|
transition: isSwiping ? 'none' : 'transform 0.2s ease-out',
|
||
|
|
}}
|
||
|
|
onTouchStart={handleTouchStart}
|
||
|
|
onTouchMove={handleTouchMove}
|
||
|
|
onTouchEnd={handleTouchEnd}
|
||
|
|
onTouchCancel={handleTouchEnd}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
{renderRightActions && (
|
||
|
|
<div
|
||
|
|
className="swipeable-actions"
|
||
|
|
style={{
|
||
|
|
opacity: isRevealed || translateX < 0 ? 1 : 0,
|
||
|
|
transition: 'opacity 0.2s ease-out',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{renderRightActions(handleActionClick)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|