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:
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user