Skip to content

Commit 82d5fd2

Browse files
Refactor(apps): 주최 공지 이미지 슬라이드 스와이프 개선 (#336)
* refactor: 스와이프 감도 개선 * feat: clear 함수 추가 * feat: 공연 이미지 수정/삭제 기능 추가 * Revert "feat: 공연 이미지 수정/삭제 기능 추가" This reverts commit 4a740d2. * Revert "feat: clear 함수 추가" This reverts commit a8142b4. * refactor: 캐러셀 스와이프 개선 * refactor: 스와이프 성능 개선 * fix: 인디케이터 흔들림 개선 --------- Co-authored-by: eunkr82 <eun.kr82@gmail.com>
1 parent f5475fc commit 82d5fd2

File tree

2 files changed

+127
-12
lines changed

2 files changed

+127
-12
lines changed

packages/compositions/src/notice-image-carousel/notice-image-carousel.css.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@ export const imageItem = style({
1717
flexShrink: 0,
1818
width: '100%',
1919
listStyle: 'none',
20+
scrollSnapAlign: 'start',
21+
scrollSnapStop: 'always',
2022
});
2123

2224
export const imageTrack = style({
2325
display: 'flex',
2426
overflowX: 'auto',
27+
overflowY: 'hidden',
2528
padding: 0,
2629
margin: 0,
30+
touchAction: 'pan-y pinch-zoom',
2731
scrollSnapType: 'x mandatory',
28-
scrollBehavior: 'smooth',
2932
scrollbarWidth: 'none',
33+
overscrollBehaviorX: 'contain',
34+
WebkitOverflowScrolling: 'touch',
3035
selectors: {
3136
'&::-webkit-scrollbar': {
3237
display: 'none',

packages/shared/src/hooks/use-draggable-carousel.ts

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useRef, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22
import type { MouseEvent, TouchEvent, UIEvent } from 'react';
33

44
interface DragState {
55
isPointerDown: boolean;
66
startX: number;
77
startScrollLeft: number;
8+
deltaX: number;
89
}
910

1011
interface 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

3073
export 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

Comments
 (0)