@watcha-authentic/react-slider 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,12 +38,49 @@ function App() {
38
38
  items={items}
39
39
  onItemKey={(item) => item.id}
40
40
  onCreateItemView={(item) => <div>{item.title}</div>}
41
- onIndexChange={(newIndex) => console.log("Index changed:", newIndex)}
41
+ onIndexChange={(newIndex, cause) =>
42
+ console.log("Index changed:", newIndex, cause)
43
+ }
42
44
  />
43
45
  );
44
46
  }
45
47
  ```
46
48
 
49
+ ### Ref를 사용한 네비게이션
50
+
51
+ `ref`를 통해 `doNext()`와 `doPrev()` 메서드를 사용하여 슬라이더를 제어할 수 있습니다.
52
+
53
+ ```tsx
54
+ import { Slider, type SliderRef } from "@watcha-authentic/react-slider";
55
+ import { useRef } from "react";
56
+
57
+ function App() {
58
+ const sliderRef = useRef<SliderRef>(null);
59
+
60
+ const handlePrev = () => {
61
+ sliderRef.current?.doPrev();
62
+ };
63
+
64
+ const handleNext = () => {
65
+ sliderRef.current?.doNext();
66
+ };
67
+
68
+ return (
69
+ <>
70
+ <button onClick={handlePrev}>Previous</button>
71
+ <button onClick={handleNext}>Next</button>
72
+ <Slider
73
+ ref={sliderRef}
74
+ items={items}
75
+ onItemKey={(item) => item.id}
76
+ onCreateItemView={(item) => <div>{item.title}</div>}
77
+ defaultIndex={0}
78
+ />
79
+ </>
80
+ );
81
+ }
82
+ ```
83
+
47
84
  ### 스타일 import
48
85
 
49
86
  CSS 스타일을 import하여 사용할 수 있습니다:
@@ -54,31 +91,64 @@ import "@watcha-authentic/react-slider/style.css";
54
91
 
55
92
  ### Context 사용
56
93
 
57
- ```tsx
58
- import {
59
- Slider,
60
- SliderContextProvider,
61
- useSliderContext,
62
- } from "@watcha-authentic/react-slider";
63
-
64
- function CustomNavigation() {
65
- const { currentIndex, goToIndex } = useSliderContext();
94
+ 아이템 내부에서 슬라이더 컨텍스트를 사용하여 포커스 상태나 전환 애니메이션을 처리할 수 있습니다.
66
95
 
67
- return (
68
- <div>
69
- <button onClick={() => goToIndex(currentIndex - 1)}>Previous</button>
70
- <span>Current: {currentIndex}</span>
71
- <button onClick={() => goToIndex(currentIndex + 1)}>Next</button>
72
- </div>
73
- );
96
+ ```tsx
97
+ import { Slider, useSliderContext } from "@watcha-authentic/react-slider";
98
+
99
+ function CustomItem({ item }: { item: { id: number; title: string } }) {
100
+ useSliderContext({
101
+ onFocus: (isAutoSlide) => {
102
+ console.log("Item focused", isAutoSlide);
103
+ },
104
+ onBlur: () => {
105
+ console.log("Item blurred");
106
+ },
107
+ onTransitionChange: (t, immediate) => {
108
+ // t: 0 ~ 1 사이의 값 (0: fade in, 1: fade out)
109
+ // immediate: true면 실시간 값 변경, false면 애니메이션 트리거 가능
110
+ },
111
+ });
112
+
113
+ return <div>{item.title}</div>;
74
114
  }
75
115
 
76
116
  function App() {
77
117
  return (
78
- <SliderContextProvider>
79
- <Slider items={items} onItemKey={(item) => item.id} onCreateItemView={(item) => <div>{item.title}</div>} />
80
- <CustomNavigation />
81
- </SliderContextProvider>
118
+ <Slider
119
+ items={items}
120
+ onItemKey={(item) => item.id}
121
+ onCreateItemView={(item) => <CustomItem item={item} />}
122
+ />
82
123
  );
83
124
  }
84
125
  ```
126
+
127
+ ## 주요 Props
128
+
129
+ ### `defaultIndex?: number`
130
+
131
+ - 초기 인덱스를 설정합니다.
132
+ - 기본값은 `0`입니다.
133
+ - **참고**: `index` prop은 제거되었습니다. 슬라이더 제어는 `ref`의 `doNext()`와 `doPrev()` 메서드를 사용하거나 `onIndexChange` 콜백을 통해 처리하세요.
134
+
135
+ ### `ref: React.Ref<SliderRef>`
136
+
137
+ - `SliderRef` 타입의 ref를 전달하면 다음 메서드에 접근할 수 있습니다:
138
+ - `doNext()`: 다음 슬라이드로 이동
139
+ - `doPrev()`: 이전 슬라이드로 이동
140
+ - 또한 `HTMLUListElement`의 모든 속성과 메서드도 사용할 수 있습니다.
141
+
142
+ ### `onIndexChange?: (newIndex: number, cause: SlideTriggerEvent) => void`
143
+
144
+ - 인덱스가 변경될 때 호출되는 콜백입니다.
145
+ - `cause`: 슬라이드 원인 (`'swipe'`: 키보드 네비게이션, `'drag'`: 드래그/스와이프, `'pending'`: 초기 상태)
146
+
147
+ ### 기타 Props
148
+
149
+ - `items`: 슬라이더에 표시할 아이템 배열
150
+ - `onCreateItemView`: 각 아이템을 렌더링하는 함수
151
+ - `onItemKey`: 각 아이템의 고유 키를 반환하는 함수
152
+ - `animationDuration`: 애니메이션 지속 시간 (기본값: 500ms)
153
+ - `enableDrag`: 드래그 기능 활성화 여부 (기본값: true)
154
+ - `visibleCount`: 중앙 기준 좌우로 보여줄 요소 개수 (기본값: 1)
@@ -19,7 +19,7 @@ const _slidercontextprovider = require("../context/slider-context-provider");
19
19
  * - 드래그로 페이지 전환을 트리거하는 임계값 비율 (아이템 너비 기준)
20
20
  */ const CAN_SCROLL_THRESHOLD_RATIO = 0.15;
21
21
  const DEFAULT_SCROLL_THRESHOLD = 125;
22
- const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "ease", defaultIndex = 0, enableDrag = true, estimateSizeFromEveryElements = false, gap = 0, index = defaultIndex, itemProps, contentProps, items, visibleCount = 1, wrapProps, onCreateItemView, onDraggingNow, onIndexChange, onItemKey }, ref)=>{
22
+ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "ease", defaultIndex = 0, enableDrag = true, estimateSizeFromEveryElements = false, gap = 0, itemProps, contentProps, items, visibleCount = 1, wrapProps, onCreateItemView, onDraggingNow, onIndexChange, onItemKey }, ref)=>{
23
23
  const stableOnIndexChange = (0, _reacteventcallback.useEventCallback)(onIndexChange);
24
24
  /**
25
25
  * - 아이템이 부족할 경우 복제하여 확장된 아이템 배열을 생성합니다.
@@ -58,7 +58,7 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
58
58
  */ const [enableScrollAnimator, setEnableScrollAnimator] = (0, _react.useState)(false);
59
59
  // slider 페이지 인덱스, 아이템 포지션 정보 등
60
60
  const [sliderInfo, setSliderInfo] = (0, _react.useState)({
61
- currentIndex: index,
61
+ currentIndex: defaultIndex,
62
62
  elementStates: [],
63
63
  height: 0
64
64
  });
@@ -66,11 +66,30 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
66
66
  // 아이템 element ref 값
67
67
  const elementInfos = (0, _react.useRef)(new Map());
68
68
  // 콜백 호출용 인덱스 값
69
- const prevCallbackIndexRef = (0, _react.useRef)(sliderInfo.currentIndex);
69
+ const prevCallbackIndex = (0, _react.useRef)(sliderInfo.currentIndex);
70
70
  // 드래그 임계값 (px, 동적으로 계산됨)
71
- const canScrollThresholdRef = (0, _react.useRef)(DEFAULT_SCROLL_THRESHOLD);
72
- const lastSlideTriggerEvent = (0, _react.useRef)("swipe");
73
- (0, _react.useImperativeHandle)(ref, ()=>wrapRef.current, []);
71
+ const canScrollThreshold = (0, _react.useRef)(DEFAULT_SCROLL_THRESHOLD);
72
+ const lastSlideTriggerEvent = (0, _react.useRef)("pending");
73
+ const lastSliderInfoCurrentIndex = (0, _react.useRef)(sliderInfo.currentIndex);
74
+ const animateNow = (0, _react.useRef)(false);
75
+ const animateChecker = (0, _react.useRef)(undefined);
76
+ const capturedDefaultIndex = (0, _react.useRef)(defaultIndex);
77
+ const stopAnimateCheck = (0, _react.useCallback)(()=>{
78
+ clearTimeout(animateChecker.current);
79
+ animateChecker.current = undefined;
80
+ }, []);
81
+ const startAnimateCheck = (0, _react.useCallback)(()=>{
82
+ stopAnimateCheck();
83
+ return new Promise((resolve)=>{
84
+ animateChecker.current = setTimeout(()=>{
85
+ animateNow.current = false;
86
+ resolve();
87
+ }, animationDuration);
88
+ });
89
+ }, [
90
+ animationDuration,
91
+ stopAnimateCheck
92
+ ]);
74
93
  /**
75
94
  * - element reference 를 저장 합니다.
76
95
  */ const updateElement = (0, _react.useCallback)(({ elementInfo: { content, item }, index })=>{
@@ -202,7 +221,7 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
202
221
  /**
203
222
  * - 페이지를 기준으로 element 의 상태를 업데이트 합니다.
204
223
  * - currentIndex 도 함께 업데이트 합니다.
205
- */ const updateStateByPageIndex = (0, _react.useCallback)(({ centerIndex, withAnimate = true })=>{
224
+ */ const updateStateByPageIndex = (0, _react.useCallback)(async ({ centerIndex, withAnimate = true })=>{
206
225
  setEnableScrollAnimator(withAnimate);
207
226
  setSliderInfo((prev)=>({
208
227
  ...prev,
@@ -214,14 +233,20 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
214
233
  prevStates: prev.elementStates
215
234
  })
216
235
  }));
236
+ if (withAnimate) {
237
+ await startAnimateCheck();
238
+ } else {
239
+ stopAnimateCheck();
240
+ }
217
241
  }, [
218
- getNewStatesByItems
242
+ getNewStatesByItems,
243
+ startAnimateCheck,
244
+ stopAnimateCheck
219
245
  ]);
220
246
  /**
221
247
  * - 포인터 드래그에 의해 레이아웃을 조정합니다.
222
248
  * - 뷰포트에 보여질 엘리먼트만 영향을 받습니다. (visibleCount 기준)
223
249
  */ const updateStateByDrag = (0, _react.useCallback)((diff)=>{
224
- setEnableScrollAnimator(false);
225
250
  setSliderInfo((prev)=>{
226
251
  const { currentIndex } = prev;
227
252
  const { length } = extendedItems;
@@ -342,7 +367,7 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
342
367
  estimatedScrollThreshold = (rect?.width ?? DEFAULT_SCROLL_THRESHOLD) * CAN_SCROLL_THRESHOLD_RATIO;
343
368
  }
344
369
  wrapRef.current.style.height = `${estimatedHeight}px`;
345
- canScrollThresholdRef.current = estimatedScrollThreshold;
370
+ canScrollThreshold.current = estimatedScrollThreshold;
346
371
  // 아이템 위치 계산
347
372
  setSliderInfo((prevSliderInfo)=>{
348
373
  return {
@@ -367,82 +392,38 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
367
392
  estimateSizeFromEveryElements,
368
393
  getNewStatesByItems
369
394
  ]);
370
- /**
371
- * - 외부 index 프롭스 변경 시 currentIndex에 반영합니다.
372
- * - 외부 index는 원본 인덱스이므로, 현재 위치에서 가장 가까운 확장 인덱스를 찾습니다.
373
- */ (0, _react.useLayoutEffect)(()=>{
374
- const currentOriginalIndex = items.length > 0 ? sliderInfo.currentIndex % items.length : 0;
375
- if (currentOriginalIndex !== index) {
376
- // 현재 위치에서 가장 가까운 해당 원본 인덱스의 확장 인덱스 찾기
377
- const candidateIndexes = extendedItems.filter((ei)=>ei.originalIndex === index).map((ei)=>ei.extendedIndex);
378
- if (candidateIndexes.length > 0) {
379
- // 현재 인덱스에서 가장 가까운 후보 찾기
380
- let closestIndex = candidateIndexes[0];
381
- if (closestIndex === undefined) {
382
- console.warn("가장 가까운 후보 인덱스를 찾을 수 없습니다.", {
383
- candidateIndexes,
384
- sliderInfoCurrentIndex: sliderInfo.currentIndex,
385
- extendedItems,
386
- index
387
- });
388
- closestIndex = 0;
389
- }
390
- let minDistance = Math.abs(closestIndex - sliderInfo.currentIndex);
391
- for (const candidate of candidateIndexes){
392
- const distance = Math.abs(candidate - sliderInfo.currentIndex);
393
- if (distance < minDistance) {
394
- minDistance = distance;
395
- closestIndex = candidate;
396
- }
397
- }
398
- lastSlideTriggerEvent.current = "swipe";
399
- setSliderInfo((prev)=>({
400
- ...prev,
401
- currentIndex: closestIndex
402
- }));
403
- }
404
- }
395
+ const handleSwipe = (0, _react.useCallback)(async ()=>{
396
+ lastSlideTriggerEvent.current = "swipe";
397
+ await updateStateByPageIndex({
398
+ centerIndex: sliderInfo.currentIndex,
399
+ withAnimate: true
400
+ });
401
+ lastSlideTriggerEvent.current = "pending";
405
402
  }, [
406
- extendedItems,
407
- index,
408
- items.length,
409
- sliderInfo.currentIndex
403
+ sliderInfo.currentIndex,
404
+ updateStateByPageIndex
410
405
  ]);
411
406
  /**
412
407
  * - currentIndex 변경 시 애니메이션을 적용합니다.
413
408
  * - doNext/doPrev 또는 외부 index prop 변경 모두 처리합니다.
414
- */ const prevIndexRef = (0, _react.useRef)(sliderInfo.currentIndex);
415
- const animateNow = (0, _react.useRef)(false);
416
- (0, _react.useLayoutEffect)(()=>{
417
- if (prevIndexRef.current !== sliderInfo.currentIndex) {
418
- updateStateByPageIndex({
419
- centerIndex: sliderInfo.currentIndex,
420
- withAnimate: true
421
- });
422
- prevIndexRef.current = sliderInfo.currentIndex;
423
- animateNow.current = true;
424
- const animateCheck = setTimeout(()=>{
425
- animateNow.current = false;
426
- }, animationDuration);
427
- return ()=>{
428
- animateNow.current = false;
429
- clearTimeout(animateCheck);
430
- };
409
+ */ (0, _react.useLayoutEffect)(()=>{
410
+ if (lastSliderInfoCurrentIndex.current !== sliderInfo.currentIndex) {
411
+ lastSliderInfoCurrentIndex.current = sliderInfo.currentIndex;
412
+ handleSwipe();
431
413
  }
432
414
  }, [
433
- animationDuration,
434
415
  sliderInfo.currentIndex,
435
- updateStateByPageIndex
416
+ handleSwipe
436
417
  ]);
437
418
  /**
438
419
  * - currentIndex 변경 시 콜백을 호출합니다.
439
420
  * - 원본 인덱스로 변환하여 전달합니다.
440
421
  */ (0, _react.useEffect)(()=>{
441
- if (prevCallbackIndexRef.current !== sliderInfo.currentIndex) {
422
+ if (prevCallbackIndex.current !== sliderInfo.currentIndex && lastSlideTriggerEvent.current) {
442
423
  // 원본 인덱스로 변환
443
424
  const originalIndex = items.length > 0 ? sliderInfo.currentIndex % items.length : 0;
444
425
  stableOnIndexChange(originalIndex, lastSlideTriggerEvent.current);
445
- prevCallbackIndexRef.current = sliderInfo.currentIndex;
426
+ prevCallbackIndex.current = sliderInfo.currentIndex;
446
427
  }
447
428
  }, [
448
429
  items.length,
@@ -465,10 +446,11 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
465
446
  * - 트랜잭션을 업데이트 합니다.
466
447
  */ const calculate = primaryPointer?.calculate;
467
448
  if (calculate) {
449
+ setEnableScrollAnimator(false);
468
450
  updateStateByDrag(calculate.diff);
469
451
  if (isEnd) {
470
452
  const diffX = Math.abs(calculate.diff.x);
471
- if (!isCancel && diffX > canScrollThresholdRef.current) {
453
+ if (!isCancel && diffX > canScrollThreshold.current) {
472
454
  lastSlideTriggerEvent.current = "drag";
473
455
  if (calculate.diff.x < 0) {
474
456
  doNext();
@@ -477,7 +459,6 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
477
459
  }
478
460
  } else {
479
461
  // 제자리로 움직이게 합니다.
480
- setEnableScrollAnimator(true);
481
462
  setSliderInfo((prev)=>({
482
463
  ...prev,
483
464
  elementStates: getNewStatesByItems({
@@ -497,15 +478,23 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
497
478
  handler: (event)=>{
498
479
  if (event.key === "ArrowLeft") {
499
480
  event.preventDefault();
500
- lastSlideTriggerEvent.current = "swipe";
501
481
  doPrev();
502
482
  } else if (event.key === "ArrowRight") {
503
483
  event.preventDefault();
504
- lastSlideTriggerEvent.current = "swipe";
505
484
  doNext();
506
485
  }
507
486
  }
508
487
  });
488
+ (0, _react.useImperativeHandle)(ref, (0, _react.useCallback)(()=>{
489
+ return {
490
+ doNext,
491
+ doPrev,
492
+ ...wrapRef.current
493
+ };
494
+ }, [
495
+ doNext,
496
+ doPrev
497
+ ]));
509
498
  return /*#__PURE__*/ (0, _jsxruntime.jsx)("ul", {
510
499
  "aria-roledescription": "carousel",
511
500
  role: "region",
@@ -545,7 +534,7 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
545
534
  itemProps?.className
546
535
  ].join(" "),
547
536
  style: state ? {
548
- transform: `translate(${state.point.x}px, ${state.point.y}px)`,
537
+ transform: `translate3d(${state.point.x}px, ${state.point.y}px, 0px)`,
549
538
  transition: enableScrollAnimator && state.enableAnimation ? `transform ${animationDuration}ms ${animationTimingFunction}` : undefined,
550
539
  zIndex: state.zIndex
551
540
  } : undefined,
@@ -9,7 +9,7 @@ import { SliderItemContextProvider } from "../context/slider-context-provider.js
9
9
  * - 드래그로 페이지 전환을 트리거하는 임계값 비율 (아이템 너비 기준)
10
10
  */ const CAN_SCROLL_THRESHOLD_RATIO = 0.15;
11
11
  const DEFAULT_SCROLL_THRESHOLD = 125;
12
- const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "ease", defaultIndex = 0, enableDrag = true, estimateSizeFromEveryElements = false, gap = 0, index = defaultIndex, itemProps, contentProps, items, visibleCount = 1, wrapProps, onCreateItemView, onDraggingNow, onIndexChange, onItemKey }, ref)=>{
12
+ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "ease", defaultIndex = 0, enableDrag = true, estimateSizeFromEveryElements = false, gap = 0, itemProps, contentProps, items, visibleCount = 1, wrapProps, onCreateItemView, onDraggingNow, onIndexChange, onItemKey }, ref)=>{
13
13
  const stableOnIndexChange = useEventCallback(onIndexChange);
14
14
  /**
15
15
  * - 아이템이 부족할 경우 복제하여 확장된 아이템 배열을 생성합니다.
@@ -48,7 +48,7 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
48
48
  */ const [enableScrollAnimator, setEnableScrollAnimator] = useState(false);
49
49
  // slider 페이지 인덱스, 아이템 포지션 정보 등
50
50
  const [sliderInfo, setSliderInfo] = useState({
51
- currentIndex: index,
51
+ currentIndex: defaultIndex,
52
52
  elementStates: [],
53
53
  height: 0
54
54
  });
@@ -56,11 +56,30 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
56
56
  // 아이템 element ref 값
57
57
  const elementInfos = useRef(new Map());
58
58
  // 콜백 호출용 인덱스 값
59
- const prevCallbackIndexRef = useRef(sliderInfo.currentIndex);
59
+ const prevCallbackIndex = useRef(sliderInfo.currentIndex);
60
60
  // 드래그 임계값 (px, 동적으로 계산됨)
61
- const canScrollThresholdRef = useRef(DEFAULT_SCROLL_THRESHOLD);
62
- const lastSlideTriggerEvent = useRef("swipe");
63
- useImperativeHandle(ref, ()=>wrapRef.current, []);
61
+ const canScrollThreshold = useRef(DEFAULT_SCROLL_THRESHOLD);
62
+ const lastSlideTriggerEvent = useRef("pending");
63
+ const lastSliderInfoCurrentIndex = useRef(sliderInfo.currentIndex);
64
+ const animateNow = useRef(false);
65
+ const animateChecker = useRef(undefined);
66
+ const capturedDefaultIndex = useRef(defaultIndex);
67
+ const stopAnimateCheck = useCallback(()=>{
68
+ clearTimeout(animateChecker.current);
69
+ animateChecker.current = undefined;
70
+ }, []);
71
+ const startAnimateCheck = useCallback(()=>{
72
+ stopAnimateCheck();
73
+ return new Promise((resolve)=>{
74
+ animateChecker.current = setTimeout(()=>{
75
+ animateNow.current = false;
76
+ resolve();
77
+ }, animationDuration);
78
+ });
79
+ }, [
80
+ animationDuration,
81
+ stopAnimateCheck
82
+ ]);
64
83
  /**
65
84
  * - element reference 를 저장 합니다.
66
85
  */ const updateElement = useCallback(({ elementInfo: { content, item }, index })=>{
@@ -192,7 +211,7 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
192
211
  /**
193
212
  * - 페이지를 기준으로 element 의 상태를 업데이트 합니다.
194
213
  * - currentIndex 도 함께 업데이트 합니다.
195
- */ const updateStateByPageIndex = useCallback(({ centerIndex, withAnimate = true })=>{
214
+ */ const updateStateByPageIndex = useCallback(async ({ centerIndex, withAnimate = true })=>{
196
215
  setEnableScrollAnimator(withAnimate);
197
216
  setSliderInfo((prev)=>({
198
217
  ...prev,
@@ -204,14 +223,20 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
204
223
  prevStates: prev.elementStates
205
224
  })
206
225
  }));
226
+ if (withAnimate) {
227
+ await startAnimateCheck();
228
+ } else {
229
+ stopAnimateCheck();
230
+ }
207
231
  }, [
208
- getNewStatesByItems
232
+ getNewStatesByItems,
233
+ startAnimateCheck,
234
+ stopAnimateCheck
209
235
  ]);
210
236
  /**
211
237
  * - 포인터 드래그에 의해 레이아웃을 조정합니다.
212
238
  * - 뷰포트에 보여질 엘리먼트만 영향을 받습니다. (visibleCount 기준)
213
239
  */ const updateStateByDrag = useCallback((diff)=>{
214
- setEnableScrollAnimator(false);
215
240
  setSliderInfo((prev)=>{
216
241
  const { currentIndex } = prev;
217
242
  const { length } = extendedItems;
@@ -332,7 +357,7 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
332
357
  estimatedScrollThreshold = (rect?.width ?? DEFAULT_SCROLL_THRESHOLD) * CAN_SCROLL_THRESHOLD_RATIO;
333
358
  }
334
359
  wrapRef.current.style.height = `${estimatedHeight}px`;
335
- canScrollThresholdRef.current = estimatedScrollThreshold;
360
+ canScrollThreshold.current = estimatedScrollThreshold;
336
361
  // 아이템 위치 계산
337
362
  setSliderInfo((prevSliderInfo)=>{
338
363
  return {
@@ -357,82 +382,38 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
357
382
  estimateSizeFromEveryElements,
358
383
  getNewStatesByItems
359
384
  ]);
360
- /**
361
- * - 외부 index 프롭스 변경 시 currentIndex에 반영합니다.
362
- * - 외부 index는 원본 인덱스이므로, 현재 위치에서 가장 가까운 확장 인덱스를 찾습니다.
363
- */ useLayoutEffect(()=>{
364
- const currentOriginalIndex = items.length > 0 ? sliderInfo.currentIndex % items.length : 0;
365
- if (currentOriginalIndex !== index) {
366
- // 현재 위치에서 가장 가까운 해당 원본 인덱스의 확장 인덱스 찾기
367
- const candidateIndexes = extendedItems.filter((ei)=>ei.originalIndex === index).map((ei)=>ei.extendedIndex);
368
- if (candidateIndexes.length > 0) {
369
- // 현재 인덱스에서 가장 가까운 후보 찾기
370
- let closestIndex = candidateIndexes[0];
371
- if (closestIndex === undefined) {
372
- console.warn("가장 가까운 후보 인덱스를 찾을 수 없습니다.", {
373
- candidateIndexes,
374
- sliderInfoCurrentIndex: sliderInfo.currentIndex,
375
- extendedItems,
376
- index
377
- });
378
- closestIndex = 0;
379
- }
380
- let minDistance = Math.abs(closestIndex - sliderInfo.currentIndex);
381
- for (const candidate of candidateIndexes){
382
- const distance = Math.abs(candidate - sliderInfo.currentIndex);
383
- if (distance < minDistance) {
384
- minDistance = distance;
385
- closestIndex = candidate;
386
- }
387
- }
388
- lastSlideTriggerEvent.current = "swipe";
389
- setSliderInfo((prev)=>({
390
- ...prev,
391
- currentIndex: closestIndex
392
- }));
393
- }
394
- }
385
+ const handleSwipe = useCallback(async ()=>{
386
+ lastSlideTriggerEvent.current = "swipe";
387
+ await updateStateByPageIndex({
388
+ centerIndex: sliderInfo.currentIndex,
389
+ withAnimate: true
390
+ });
391
+ lastSlideTriggerEvent.current = "pending";
395
392
  }, [
396
- extendedItems,
397
- index,
398
- items.length,
399
- sliderInfo.currentIndex
393
+ sliderInfo.currentIndex,
394
+ updateStateByPageIndex
400
395
  ]);
401
396
  /**
402
397
  * - currentIndex 변경 시 애니메이션을 적용합니다.
403
398
  * - doNext/doPrev 또는 외부 index prop 변경 모두 처리합니다.
404
- */ const prevIndexRef = useRef(sliderInfo.currentIndex);
405
- const animateNow = useRef(false);
406
- useLayoutEffect(()=>{
407
- if (prevIndexRef.current !== sliderInfo.currentIndex) {
408
- updateStateByPageIndex({
409
- centerIndex: sliderInfo.currentIndex,
410
- withAnimate: true
411
- });
412
- prevIndexRef.current = sliderInfo.currentIndex;
413
- animateNow.current = true;
414
- const animateCheck = setTimeout(()=>{
415
- animateNow.current = false;
416
- }, animationDuration);
417
- return ()=>{
418
- animateNow.current = false;
419
- clearTimeout(animateCheck);
420
- };
399
+ */ useLayoutEffect(()=>{
400
+ if (lastSliderInfoCurrentIndex.current !== sliderInfo.currentIndex) {
401
+ lastSliderInfoCurrentIndex.current = sliderInfo.currentIndex;
402
+ handleSwipe();
421
403
  }
422
404
  }, [
423
- animationDuration,
424
405
  sliderInfo.currentIndex,
425
- updateStateByPageIndex
406
+ handleSwipe
426
407
  ]);
427
408
  /**
428
409
  * - currentIndex 변경 시 콜백을 호출합니다.
429
410
  * - 원본 인덱스로 변환하여 전달합니다.
430
411
  */ useEffect(()=>{
431
- if (prevCallbackIndexRef.current !== sliderInfo.currentIndex) {
412
+ if (prevCallbackIndex.current !== sliderInfo.currentIndex && lastSlideTriggerEvent.current) {
432
413
  // 원본 인덱스로 변환
433
414
  const originalIndex = items.length > 0 ? sliderInfo.currentIndex % items.length : 0;
434
415
  stableOnIndexChange(originalIndex, lastSlideTriggerEvent.current);
435
- prevCallbackIndexRef.current = sliderInfo.currentIndex;
416
+ prevCallbackIndex.current = sliderInfo.currentIndex;
436
417
  }
437
418
  }, [
438
419
  items.length,
@@ -455,10 +436,11 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
455
436
  * - 트랜잭션을 업데이트 합니다.
456
437
  */ const calculate = primaryPointer?.calculate;
457
438
  if (calculate) {
439
+ setEnableScrollAnimator(false);
458
440
  updateStateByDrag(calculate.diff);
459
441
  if (isEnd) {
460
442
  const diffX = Math.abs(calculate.diff.x);
461
- if (!isCancel && diffX > canScrollThresholdRef.current) {
443
+ if (!isCancel && diffX > canScrollThreshold.current) {
462
444
  lastSlideTriggerEvent.current = "drag";
463
445
  if (calculate.diff.x < 0) {
464
446
  doNext();
@@ -467,7 +449,6 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
467
449
  }
468
450
  } else {
469
451
  // 제자리로 움직이게 합니다.
470
- setEnableScrollAnimator(true);
471
452
  setSliderInfo((prev)=>({
472
453
  ...prev,
473
454
  elementStates: getNewStatesByItems({
@@ -487,15 +468,23 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
487
468
  handler: (event)=>{
488
469
  if (event.key === "ArrowLeft") {
489
470
  event.preventDefault();
490
- lastSlideTriggerEvent.current = "swipe";
491
471
  doPrev();
492
472
  } else if (event.key === "ArrowRight") {
493
473
  event.preventDefault();
494
- lastSlideTriggerEvent.current = "swipe";
495
474
  doNext();
496
475
  }
497
476
  }
498
477
  });
478
+ useImperativeHandle(ref, useCallback(()=>{
479
+ return {
480
+ doNext,
481
+ doPrev,
482
+ ...wrapRef.current
483
+ };
484
+ }, [
485
+ doNext,
486
+ doPrev
487
+ ]));
499
488
  return /*#__PURE__*/ _jsx("ul", {
500
489
  "aria-roledescription": "carousel",
501
490
  role: "region",
@@ -535,7 +524,7 @@ const SliderComponent = ({ animationDuration = 500, animationTimingFunction = "e
535
524
  itemProps?.className
536
525
  ].join(" "),
537
526
  style: state ? {
538
- transform: `translate(${state.point.x}px, ${state.point.y}px)`,
527
+ transform: `translate3d(${state.point.x}px, ${state.point.y}px, 0px)`,
539
528
  transition: enableScrollAnimator && state.enableAnimation ? `transform ${animationDuration}ms ${animationTimingFunction}` : undefined,
540
529
  zIndex: state.zIndex
541
530
  } : undefined,
@@ -1,5 +1,9 @@
1
1
  import type { CSSProperties } from "react";
2
2
  import type { SlideTriggerEvent } from "../../script/type/slider-types";
3
+ export type SliderRef = HTMLUListElement & {
4
+ doNext: () => void;
5
+ doPrev: () => void;
6
+ };
3
7
  type SliderProps<ItemType> = {
4
8
  itemProps?: React.HTMLAttributes<HTMLLIElement>;
5
9
  wrapProps?: React.HTMLAttributes<HTMLUListElement>;
@@ -33,11 +37,6 @@ type SliderProps<ItemType> = {
33
37
  * - 기본값은 0 입니다.
34
38
  */
35
39
  defaultIndex?: number;
36
- /**
37
- * - currentIndex 입니다. 이 값이 변경되면 애니메이션이 동작 될 수 있습니다.
38
- * - 기본값은 defaultIndex 입니다.
39
- */
40
- index?: number;
41
40
  /**
42
41
  * - 중앙 기준 좌우로 보여줄 요소 개수입니다.
43
42
  * - 예: 1이면 좌1 + 중앙1 + 우1 = 3개, 2이면 좌2 + 중앙1 + 우2 = 5개
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * - 슬라이드시 이벤트 트리거 종류
3
3
  */
4
- export type SlideTriggerEvent = "drag" | "swipe";
4
+ export type SlideTriggerEvent = "pending" | "drag" | "swipe";
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "slider",
7
7
  "swiper"
8
8
  ],
9
- "version": "0.1.1",
9
+ "version": "0.1.2",
10
10
  "homepage": "https://github.com/frograms/bistro-house/tree/master/packages/react-slider#readme",
11
11
  "author": {
12
12
  "name": "@watcha-authentic#web-dev-group",