Enhance Carousel with real-time drag feedback

- Track touch position and show live drag offset while swiping
- Add rubber band effect at edges (25% resistance)
- Disable CSS transition during drag for instant response
- Improve accessibility with role/aria attributes
This commit is contained in:
fahed
2026-02-03 15:19:33 +03:00
parent 7ed7af314c
commit 222d583847

View File

@@ -1,4 +1,4 @@
import React, { useRef, useCallback } from 'react'; import React, { useRef, useCallback, useState } from 'react';
function Carousel({ function Carousel({
children, children,
@@ -8,24 +8,57 @@ function Carousel({
showLabels = true, showLabels = true,
className = '' className = ''
}) { }) {
const touchStart = useRef(null); const touchStartX = useRef(null);
const touchStartY = useRef(null);
const trackRef = useRef(null);
const [dragOffset, setDragOffset] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const itemCount = React.Children.count(children); const itemCount = React.Children.count(children);
const SWIPE_THRESHOLD = 50;
const handleTouchStart = useCallback((e) => { const handleTouchStart = useCallback((e) => {
touchStart.current = e.touches[0].clientX; touchStartX.current = e.touches[0].clientX;
touchStartY.current = e.touches[0].clientY;
setIsDragging(true);
}, []); }, []);
const handleTouchMove = useCallback((e) => {
if (!touchStartX.current || !isDragging) return;
const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY;
const diffX = currentX - touchStartX.current;
const diffY = Math.abs(currentY - touchStartY.current);
// Only drag horizontally if not scrolling vertically
if (Math.abs(diffX) > diffY) {
// Add resistance at edges
let offset = diffX;
if ((activeIndex === 0 && diffX > 0) || (activeIndex === itemCount - 1 && diffX < 0)) {
offset = diffX * 0.25;
}
setDragOffset(offset);
}
}, [isDragging, activeIndex, itemCount]);
const handleTouchEnd = useCallback((e) => { const handleTouchEnd = useCallback((e) => {
if (!touchStart.current) return; if (!touchStartX.current) return;
const diff = touchStart.current - e.changedTouches[0].clientX;
if (Math.abs(diff) > 50) { const diff = touchStartX.current - e.changedTouches[0].clientX;
if (Math.abs(diff) > SWIPE_THRESHOLD) {
if (diff > 0 && activeIndex < itemCount - 1) { if (diff > 0 && activeIndex < itemCount - 1) {
setActiveIndex(activeIndex + 1); setActiveIndex(activeIndex + 1);
} else if (diff < 0 && activeIndex > 0) { } else if (diff < 0 && activeIndex > 0) {
setActiveIndex(activeIndex - 1); setActiveIndex(activeIndex - 1);
} }
} }
touchStart.current = null;
touchStartX.current = null;
touchStartY.current = null;
setDragOffset(0);
setIsDragging(false);
}, [activeIndex, setActiveIndex, itemCount]); }, [activeIndex, setActiveIndex, itemCount]);
const handleKeyDown = useCallback((e) => { const handleKeyDown = useCallback((e) => {
@@ -36,18 +69,41 @@ function Carousel({
} }
}, [activeIndex, setActiveIndex, itemCount]); }, [activeIndex, setActiveIndex, itemCount]);
// Calculate transform with drag offset
const baseTransform = -(activeIndex * 100);
const dragPercent = trackRef.current
? (dragOffset / trackRef.current.offsetWidth) * 100
: 0;
const transform = baseTransform + dragPercent;
return ( return (
<div className={`carousel ${className}`} onKeyDown={handleKeyDown} tabIndex={0}> <div
className={`carousel ${className}`}
onKeyDown={handleKeyDown}
tabIndex={0}
role="region"
aria-label="Carousel"
>
<div className="carousel-container"> <div className="carousel-container">
<div className="carousel-viewport"> <div className="carousel-viewport">
<div <div
ref={trackRef}
className="carousel-track" className="carousel-track"
style={{ transform: `translateX(-${activeIndex * 100}%)` }} style={{
transform: `translateX(${transform}%)`,
transition: isDragging ? 'none' : undefined
}}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
> >
{React.Children.map(children, (child, i) => ( {React.Children.map(children, (child, i) => (
<div className="carousel-slide" key={i}> <div
className="carousel-slide"
key={i}
role="tabpanel"
aria-hidden={activeIndex !== i}
>
{child} {child}
</div> </div>
))} ))}