-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #340 from MovieReviewComment/feature/issue-325/use…
…-swiper [#325] Implement useSwiper
- Loading branch information
Showing
1 changed file
with
155 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
'use client'; | ||
|
||
import { useRef, useState } from 'react'; | ||
|
||
import { useStateRef } from '@/hooks/common/use-state-ref'; | ||
|
||
import { getTouchEventData } from '@/lib/utils/common/get-touch-event-data'; | ||
|
||
const MIN_SWIPE_REQUIRED = 40; // px | ||
|
||
/* | ||
<When current slide is slide1 scenario> | ||
\\\\ = current slide. | ||
On touch start, values are initialized: | ||
containerWidth = cw | ||
scrollWidth = 3cw | ||
minOffsetX = containerWidth - scrollWidth = -2cw | ||
initialOffsetX = -cw | ||
-2cw -cw 0 container cw 2cw 3cw | ||
|-------------------|-------------------|-------------------|-------------------|-------------------| | ||
| slide0 |\\\\\ slide1 \\\\\| slide2 | | ||
|-------------------|-------------------|-------------------| | ||
| <---------------------- scrollWidth --------------------> | | ||
offsetX <= initialOffsetX | ||
On touch move, a new offsetX is calculated as follows: | ||
swipeDiff = touchStartX - currentTouchX | ||
newOffsetX = initialOffsetX - swipeDiff | ||
here, newOffsetX is limited by minOffsetX and maxOffsetX ! | ||
maxOffsetX => 0 container cw 2cw 3cw | ||
|-------------------|-------------------|-------------------| | ||
|\\\\\ slide0 \\\\\| slide1 | slide2 | | ||
|-------------------|-------------------|-------------------| | ||
offsetX | ||
-2cw <= minOffsetX -cw 0 container cw | ||
|-------------------|-------------------|-------------------| | ||
| slide0 | slide1 |\\\\\ slide2 \\\\\| | ||
|-------------------|-------------------|-------------------| | ||
offsetX | ||
On touch end, the slides are moved: | ||
if swipeDiff > MIN_SWIPE_REQUIRED, caculate the index of the slide to move | ||
newOffsetX = -(index of the slide to move) * containerWidth | ||
*/ | ||
|
||
export function useSwiper() { | ||
const [offsetX, setOffsetX, offsetXRef] = useStateRef(0); | ||
|
||
const containerRef = useRef<HTMLUListElement>(null); | ||
// It is referenced in `onIndicatorClick`, which is binded on first render. | ||
// So, it needs to be a ref to keep the latest value in the closure. | ||
// Otherwise, `onIndicatorClick` will always have the initial value which is 0. | ||
const containerWidthRef = useRef(0); | ||
|
||
const [isSwiping, setIsSwiping] = useState(false); | ||
const [currentIdx, setCurrentIdx] = useState(0); | ||
|
||
// minOffsetX, initialOffsetX, touchStartX are set in onTouchStart | ||
// and referenced in onTouchMove and onTouchEnd, | ||
// The value of these don't change after they passed to closure (onTouchMove and onTouchEnd). | ||
// So, they don't need to be refs. | ||
let minOffsetX: number; | ||
let initialOffsetX: number; | ||
let touchStartX: number; | ||
|
||
const onTouchStart = (e: React.TouchEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>) => { | ||
setIsSwiping(true); | ||
|
||
const containerEl = containerRef.current!; | ||
|
||
containerWidthRef.current = containerEl.offsetWidth; | ||
|
||
minOffsetX = containerWidthRef.current - containerEl.scrollWidth; | ||
|
||
initialOffsetX = offsetXRef.current!; | ||
touchStartX = getTouchEventData(e).clientX; | ||
|
||
window.addEventListener('touchmove', onTouchMove); | ||
window.addEventListener('touchend', onTouchEnd); | ||
window.addEventListener('mousemove', onTouchMove); | ||
window.addEventListener('mouseup', onTouchEnd); | ||
}; | ||
|
||
const onTouchMove = (e: globalThis.TouchEvent | globalThis.MouseEvent) => { | ||
const currentTouchX = getTouchEventData(e).clientX; | ||
const swipeDiff = touchStartX - currentTouchX; | ||
let newOffsetX = initialOffsetX - swipeDiff; | ||
|
||
const maxOffsetX = 0; | ||
|
||
if (newOffsetX > maxOffsetX) { | ||
newOffsetX = maxOffsetX; | ||
} | ||
|
||
if (newOffsetX < minOffsetX) { | ||
newOffsetX = minOffsetX; | ||
} | ||
|
||
setOffsetX(newOffsetX); | ||
}; | ||
|
||
const onTouchEnd = () => { | ||
let newOffsetX = offsetXRef.current!; | ||
const swipeDiff = initialOffsetX - newOffsetX; | ||
|
||
const containerEl = containerRef.current!; | ||
const containerWidth = containerEl.offsetWidth; | ||
|
||
let negatedIndex: number; // -1 * index of slide to move | ||
|
||
// check difference is bigger than MIN_SWIPE_REQUIRED | ||
if (Math.abs(swipeDiff) > MIN_SWIPE_REQUIRED) { | ||
if (swipeDiff > 0) { | ||
// move to the right slide | ||
negatedIndex = Math.floor(newOffsetX / containerWidth); | ||
} else { | ||
// move to the left slide | ||
negatedIndex = Math.ceil(newOffsetX / containerWidth); | ||
} | ||
} else { | ||
// remain in the current slide | ||
negatedIndex = Math.round(newOffsetX / containerWidth); | ||
} | ||
|
||
newOffsetX = negatedIndex * containerWidth; | ||
|
||
setIsSwiping(false); | ||
setOffsetX(newOffsetX); | ||
setCurrentIdx(Math.abs(negatedIndex)); | ||
|
||
window.removeEventListener('touchend', onTouchEnd); | ||
window.removeEventListener('touchmove', onTouchMove); | ||
window.removeEventListener('mouseup', onTouchEnd); | ||
window.removeEventListener('mousemove', onTouchMove); | ||
}; | ||
|
||
const onIndicatorClick = (idx: number) => { | ||
setCurrentIdx(idx); | ||
setOffsetX(-(containerWidthRef.current * idx)); | ||
}; | ||
|
||
return { | ||
offsetX, | ||
onTouchStart, | ||
isSwiping, | ||
currentIdx, | ||
onIndicatorClick, | ||
containerRef, | ||
}; | ||
} |