1- import { useRef , useState } from 'react' ;
1+ import { useEffect , useRef , useState } from 'react' ;
22import type { MouseEvent , TouchEvent , UIEvent } from 'react' ;
33
44interface DragState {
55 isPointerDown : boolean ;
66 startX : number ;
77 startScrollLeft : number ;
8+ deltaX : number ;
89}
910
1011interface UseDraggableCarouselParams {
@@ -15,16 +16,58 @@ const INITIAL_DRAG_STATE: DragState = {
1516 isPointerDown : false ,
1617 startX : 0 ,
1718 startScrollLeft : 0 ,
19+ deltaX : 0 ,
1820} ;
21+ const SWIPE_THRESHOLD_RATIO = 0.2 ;
1922
20- const getSnapScrollLeft = ( element : HTMLElement ) => {
21- if ( element . clientWidth === 0 ) {
23+ const getStartIndex = ( startScrollLeft : number , clientWidth : number ) => {
24+ if ( clientWidth === 0 ) {
2225 return 0 ;
2326 }
2427
25- return (
26- Math . round ( element . scrollLeft / element . clientWidth ) * element . clientWidth
27- ) ;
28+ return Math . round ( startScrollLeft / clientWidth ) ;
29+ } ;
30+
31+ const getMovedRatio = ( deltaX : number , clientWidth : number ) => {
32+ if ( clientWidth === 0 ) {
33+ return 0 ;
34+ }
35+
36+ return Math . abs ( deltaX ) / clientWidth ;
37+ } ;
38+
39+ const getSwipeDirection = ( deltaX : number ) => {
40+ if ( deltaX > 0 ) {
41+ return - 1 ;
42+ }
43+
44+ if ( deltaX < 0 ) {
45+ return 1 ;
46+ }
47+
48+ return 0 ;
49+ } ;
50+
51+ const getNextIndex = ( {
52+ clientWidth,
53+ deltaX,
54+ itemCount,
55+ startScrollLeft,
56+ } : {
57+ clientWidth : number ;
58+ deltaX : number ;
59+ itemCount : number ;
60+ startScrollLeft : number ;
61+ } ) => {
62+ const startIndex = getStartIndex ( startScrollLeft , clientWidth ) ;
63+ const movedRatio = getMovedRatio ( deltaX , clientWidth ) ;
64+
65+ if ( movedRatio < SWIPE_THRESHOLD_RATIO ) {
66+ return startIndex ;
67+ }
68+
69+ const direction = getSwipeDirection ( deltaX ) ;
70+ return Math . min ( Math . max ( startIndex + direction , 0 ) , itemCount - 1 ) ;
2871} ;
2972
3073export const useDraggableCarousel = ( {
@@ -34,6 +77,9 @@ export const useDraggableCarousel = ({
3477 const [ isDragging , setIsDragging ] = useState ( false ) ;
3578 const trackRef = useRef < HTMLUListElement | null > ( null ) ;
3679 const dragStateRef = useRef < DragState > ( INITIAL_DRAG_STATE ) ;
80+ const currentPageRef = useRef ( 1 ) ;
81+ const rafRef = useRef < number | null > ( null ) ;
82+ const lockedPageRef = useRef < number | null > ( null ) ;
3783 const isIndicatorVisible = itemCount > 1 ;
3884
3985 const handleScroll = ( event : UIEvent < HTMLUListElement > ) => {
@@ -42,25 +88,72 @@ export const useDraggableCarousel = ({
4288 return ;
4389 }
4490
45- setCurrentPage ( Math . round ( scrollLeft / clientWidth ) + 1 ) ;
91+ const nextPage = Math . round ( scrollLeft / clientWidth ) + 1 ;
92+
93+ if ( dragStateRef . current . isPointerDown ) {
94+ return ;
95+ }
96+
97+ if ( lockedPageRef . current !== null && lockedPageRef . current !== nextPage ) {
98+ return ;
99+ }
100+
101+ if ( lockedPageRef . current === nextPage ) {
102+ lockedPageRef . current = null ;
103+ }
104+
105+ if ( currentPageRef . current === nextPage ) {
106+ return ;
107+ }
108+
109+ currentPageRef . current = nextPage ;
110+ setCurrentPage ( nextPage ) ;
46111 } ;
47112
48113 const startDrag = ( startX : number , startScrollLeft : number ) => {
49114 dragStateRef . current = {
50115 isPointerDown : true ,
51116 startX,
52117 startScrollLeft,
118+ deltaX : 0 ,
53119 } ;
54120 setIsDragging ( true ) ;
55121 } ;
56122
123+ const scrollToIndex = ( index : number ) => {
124+ if ( ! trackRef . current ) {
125+ return ;
126+ }
127+
128+ const nextIndex = Math . min ( Math . max ( index , 0 ) , itemCount - 1 ) ;
129+ const { clientWidth } = trackRef . current ;
130+ const nextPage = nextIndex + 1 ;
131+
132+ lockedPageRef . current = nextPage ;
133+ currentPageRef . current = nextPage ;
134+ setCurrentPage ( nextPage ) ;
135+ trackRef . current . scrollTo ( {
136+ left : nextIndex * clientWidth ,
137+ behavior : 'smooth' ,
138+ } ) ;
139+ } ;
140+
57141 const updateDrag = ( currentX : number , element : HTMLUListElement ) => {
58142 if ( ! dragStateRef . current . isPointerDown ) {
59143 return ;
60144 }
61145
62146 const deltaX = currentX - dragStateRef . current . startX ;
63- element . scrollLeft = dragStateRef . current . startScrollLeft - deltaX ;
147+ dragStateRef . current . deltaX = deltaX ;
148+
149+ if ( rafRef . current !== null ) {
150+ cancelAnimationFrame ( rafRef . current ) ;
151+ }
152+
153+ rafRef . current = requestAnimationFrame ( ( ) => {
154+ element . scrollLeft = dragStateRef . current . startScrollLeft - deltaX ;
155+ rafRef . current = null ;
156+ } ) ;
64157 } ;
65158
66159 const handleMouseDown = ( event : MouseEvent < HTMLUListElement > ) => {
@@ -94,11 +187,20 @@ export const useDraggableCarousel = ({
94187 return ;
95188 }
96189
190+ if ( rafRef . current !== null ) {
191+ cancelAnimationFrame ( rafRef . current ) ;
192+ rafRef . current = null ;
193+ }
194+
97195 if ( trackRef . current ) {
98- trackRef . current . scrollTo ( {
99- left : getSnapScrollLeft ( trackRef . current ) ,
100- behavior : 'smooth' ,
196+ const { clientWidth } = trackRef . current ;
197+ const nextIndex = getNextIndex ( {
198+ clientWidth,
199+ deltaX : dragStateRef . current . deltaX ,
200+ itemCount,
201+ startScrollLeft : dragStateRef . current . startScrollLeft ,
101202 } ) ;
203+ scrollToIndex ( nextIndex ) ;
102204 }
103205
104206 dragStateRef . current = INITIAL_DRAG_STATE ;
@@ -113,6 +215,14 @@ export const useDraggableCarousel = ({
113215 endDrag ( ) ;
114216 } ;
115217
218+ useEffect ( ( ) => {
219+ return ( ) => {
220+ if ( rafRef . current !== null ) {
221+ cancelAnimationFrame ( rafRef . current ) ;
222+ }
223+ } ;
224+ } , [ ] ) ;
225+
116226 return {
117227 currentPage,
118228 handleMouseDown,
0 commit comments