@watcha-authentic/react-slider 0.0.1 → 0.1.1

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.
@@ -0,0 +1,585 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAccessibilityHandler } from "@watcha-authentic/react-a11y";
3
+ import { useEventCallback } from "@watcha-authentic/react-event-callback";
4
+ import { addPoint, usePointerMove } from "@watcha-authentic/react-motion";
5
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ import { SliderItemContextProvider } from "../context/slider-context-provider.js";
7
+ // TODO: loop false 에 대한 기능 추가
8
+ /**
9
+ * - 드래그로 페이지 전환을 트리거하는 임계값 비율 (아이템 너비 기준)
10
+ */ const CAN_SCROLL_THRESHOLD_RATIO = 0.15;
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)=>{
13
+ const stableOnIndexChange = useEventCallback(onIndexChange);
14
+ /**
15
+ * - 아이템이 부족할 경우 복제하여 확장된 아이템 배열을 생성합니다.
16
+ * - 최소 필요 개수: 2 * (visibleCount + 1) + 1
17
+ */ const extendedItems = useMemo(()=>{
18
+ const minItems = 2 * (visibleCount + 1) + 1;
19
+ const mult = items.length > 0 ? Math.ceil(minItems / items.length) : 1;
20
+ const extended = [];
21
+ for(let m = 0; m < mult; m++){
22
+ for(let i = 0; i < items.length; i++){
23
+ const item = items[i];
24
+ if (item === undefined) {
25
+ console.warn("Item element 를 찾을 수 없습니다.", {
26
+ minItems,
27
+ mult,
28
+ items,
29
+ m,
30
+ i
31
+ });
32
+ continue;
33
+ }
34
+ extended.push({
35
+ extendedIndex: m * items.length + i,
36
+ item,
37
+ originalIndex: i
38
+ });
39
+ }
40
+ }
41
+ return extended;
42
+ }, [
43
+ items,
44
+ visibleCount
45
+ ]);
46
+ /**
47
+ * - 드래그 애니메이션 활성화 여부
48
+ */ const [enableScrollAnimator, setEnableScrollAnimator] = useState(false);
49
+ // slider 페이지 인덱스, 아이템 포지션 정보 등
50
+ const [sliderInfo, setSliderInfo] = useState({
51
+ currentIndex: index,
52
+ elementStates: [],
53
+ height: 0
54
+ });
55
+ const wrapRef = useRef(null);
56
+ // 아이템 element ref 값
57
+ const elementInfos = useRef(new Map());
58
+ // 콜백 호출용 인덱스 값
59
+ const prevCallbackIndexRef = useRef(sliderInfo.currentIndex);
60
+ // 드래그 임계값 (px, 동적으로 계산됨)
61
+ const canScrollThresholdRef = useRef(DEFAULT_SCROLL_THRESHOLD);
62
+ const lastSlideTriggerEvent = useRef("swipe");
63
+ useImperativeHandle(ref, ()=>wrapRef.current, []);
64
+ /**
65
+ * - element reference 를 저장 합니다.
66
+ */ const updateElement = useCallback(({ elementInfo: { content, item }, index })=>{
67
+ let elementInfo = elementInfos.current.get(index);
68
+ if (!elementInfo) {
69
+ elementInfo = {
70
+ content: null,
71
+ item: null
72
+ };
73
+ elementInfos.current.set(index, elementInfo);
74
+ }
75
+ // null 은 값을 null 로 설정 할 수 있어야 합니다.
76
+ if (item !== undefined) {
77
+ elementInfo.item = item;
78
+ }
79
+ if (content !== undefined) {
80
+ elementInfo.content = content;
81
+ }
82
+ }, []);
83
+ /**
84
+ * - element 의 상태(position 등)를 계산 합니다.
85
+ */ const calcElementState = useCallback(({ centerIndex, elementIndex, itemSize, prevState })=>{
86
+ const { width } = itemSize;
87
+ const { length } = extendedItems;
88
+ // 원형 배열에서 centerIndex 기준으로 시계방향/반시계방향 거리 계산
89
+ const clockwiseDistance = (elementIndex - centerIndex + length) % length;
90
+ const counterClockwiseDistance = (centerIndex - elementIndex + length) % length;
91
+ let positionType;
92
+ let point;
93
+ let zIndex;
94
+ if (elementIndex === centerIndex) {
95
+ // center
96
+ positionType = "center";
97
+ point = {
98
+ x: 0,
99
+ y: 0
100
+ };
101
+ zIndex = length; // 가장 높음
102
+ } else if (counterClockwiseDistance <= clockwiseDistance) {
103
+ // 왼쪽 (반시계방향이 더 가깝거나 같음)
104
+ positionType = "left";
105
+ if (counterClockwiseDistance <= visibleCount) {
106
+ // visibleCount 범위 내: 거리에 따라 왼쪽으로 이동
107
+ point = {
108
+ x: -(width + gap) * counterClockwiseDistance,
109
+ y: 0
110
+ };
111
+ zIndex = length - counterClockwiseDistance;
112
+ } else {
113
+ // 범위 밖: 가장 바깥 위치에 고정
114
+ point = {
115
+ x: -(width + gap) * (visibleCount + 1),
116
+ y: 0
117
+ };
118
+ zIndex = 0;
119
+ }
120
+ } else {
121
+ // 오른쪽 (시계방향이 더 가까움)
122
+ positionType = "right";
123
+ if (clockwiseDistance <= visibleCount) {
124
+ // visibleCount 범위 내: 거리에 따라 오른쪽으로 이동
125
+ point = {
126
+ x: (width + gap) * clockwiseDistance,
127
+ y: 0
128
+ };
129
+ zIndex = length - clockwiseDistance;
130
+ } else {
131
+ // 범위 밖: 가장 바깥 위치에 고정
132
+ point = {
133
+ x: (width + gap) * (visibleCount + 1),
134
+ y: 0
135
+ };
136
+ zIndex = 0;
137
+ }
138
+ }
139
+ // 점프 여부 확인:
140
+ // 왼쪽 <> 오른쪽 이동 시만 점프 (애니메이션 비활성화)
141
+ // 범위 밖으로 나가거나 들어올 때는 애니메이션 유지
142
+ const isJumping = prevState && (prevState.positionType === "left" && positionType === "right" || prevState.positionType === "right" && positionType === "left");
143
+ // transition 계산: 중앙으로부터 떨어진 거리 비율 (0~1)
144
+ // 기준: 아이템 너비 + gap
145
+ const itemWidth = width + gap;
146
+ const transition = Math.min(1, Math.max(0, Math.abs(point.x) / itemWidth));
147
+ return {
148
+ enableAnimation: !isJumping,
149
+ originPoint: point,
150
+ point,
151
+ positionType,
152
+ transition,
153
+ zIndex
154
+ };
155
+ }, [
156
+ extendedItems,
157
+ gap,
158
+ visibleCount
159
+ ]);
160
+ const getNewStatesByItems = useCallback(({ centerIndex, enableAnimation = false, itemIndexs, prevStates })=>{
161
+ return itemIndexs.map((itemIndex)=>{
162
+ const prevState = prevStates?.[itemIndex];
163
+ const item = elementInfos.current.get(itemIndex)?.item;
164
+ if (!item) {
165
+ console.warn("Item element 를 찾을 수 없습니다.", {
166
+ centerIndex,
167
+ itemIndex,
168
+ prevState,
169
+ prevStates
170
+ });
171
+ }
172
+ const state = calcElementState({
173
+ centerIndex,
174
+ elementIndex: itemIndex,
175
+ itemSize: item?.getBoundingClientRect() ?? {
176
+ height: 0,
177
+ width: 0
178
+ },
179
+ prevState
180
+ });
181
+ return {
182
+ ...state,
183
+ // isJumping이면 애니메이션 비활성화
184
+ enableAnimation: enableAnimation && state.enableAnimation,
185
+ // originPoint도 point와 동일하게 설정
186
+ originPoint: state.point
187
+ };
188
+ });
189
+ }, [
190
+ calcElementState
191
+ ]);
192
+ /**
193
+ * - 페이지를 기준으로 element 의 상태를 업데이트 합니다.
194
+ * - currentIndex 도 함께 업데이트 합니다.
195
+ */ const updateStateByPageIndex = useCallback(({ centerIndex, withAnimate = true })=>{
196
+ setEnableScrollAnimator(withAnimate);
197
+ setSliderInfo((prev)=>({
198
+ ...prev,
199
+ currentIndex: centerIndex,
200
+ elementStates: getNewStatesByItems({
201
+ centerIndex,
202
+ enableAnimation: withAnimate,
203
+ itemIndexs: prev.elementStates.map((_, index)=>index),
204
+ prevStates: prev.elementStates
205
+ })
206
+ }));
207
+ }, [
208
+ getNewStatesByItems
209
+ ]);
210
+ /**
211
+ * - 포인터 드래그에 의해 레이아웃을 조정합니다.
212
+ * - 뷰포트에 보여질 엘리먼트만 영향을 받습니다. (visibleCount 기준)
213
+ */ const updateStateByDrag = useCallback((diff)=>{
214
+ setEnableScrollAnimator(false);
215
+ setSliderInfo((prev)=>{
216
+ const { currentIndex } = prev;
217
+ const { length } = extendedItems;
218
+ // visibleCount 기준으로 좌/우 인덱스들 계산
219
+ // visibleCount=1: 좌1 + 중앙 + 우1 = 3개
220
+ // visibleCount=2: 좌2 + 중앙 + 우2 = 5개
221
+ const targetIndexes = [
222
+ currentIndex
223
+ ];
224
+ for(let i = 1; i <= visibleCount + 1; i++){
225
+ targetIndexes.push((currentIndex - i + length) % length); // 왼쪽
226
+ targetIndexes.push((currentIndex + i) % length); // 오른쪽
227
+ }
228
+ const newElementStates = [
229
+ ...prev.elementStates
230
+ ];
231
+ for (const targetIndex of targetIndexes){
232
+ const state = newElementStates[targetIndex];
233
+ if (state) {
234
+ const newPoint = addPoint(state.originPoint, {
235
+ x: diff.x,
236
+ y: 0
237
+ });
238
+ // transition 계산: 아이템 너비 기준
239
+ const item = elementInfos.current.get(targetIndex)?.item;
240
+ if (item) {
241
+ const { width } = item.getBoundingClientRect();
242
+ const itemWidth = width + gap;
243
+ const transition = Math.min(1, Math.max(0, Math.abs(newPoint.x) / itemWidth));
244
+ newElementStates[targetIndex] = {
245
+ ...state,
246
+ enableAnimation: false,
247
+ point: newPoint,
248
+ transition
249
+ };
250
+ }
251
+ }
252
+ }
253
+ return {
254
+ ...prev,
255
+ elementStates: newElementStates
256
+ };
257
+ });
258
+ }, [
259
+ extendedItems,
260
+ gap,
261
+ visibleCount
262
+ ]);
263
+ const doNext = useCallback(()=>{
264
+ setSliderInfo((prev)=>{
265
+ const newIndex = (prev.currentIndex + 1) % extendedItems.length;
266
+ return {
267
+ ...prev,
268
+ currentIndex: newIndex
269
+ };
270
+ });
271
+ }, [
272
+ extendedItems.length
273
+ ]);
274
+ const doPrev = useCallback(()=>{
275
+ setSliderInfo((prev)=>{
276
+ const newIndex = (prev.currentIndex - 1 + extendedItems.length) % extendedItems.length;
277
+ return {
278
+ ...prev,
279
+ currentIndex: newIndex
280
+ };
281
+ });
282
+ }, [
283
+ extendedItems.length
284
+ ]);
285
+ /**
286
+ * - extendedItems 프롭스에 의한 값을 초기화 합니다.
287
+ */ useLayoutEffect(()=>{
288
+ setSliderInfo((prevSliderInfo)=>{
289
+ return {
290
+ ...prevSliderInfo,
291
+ elementStates: getNewStatesByItems({
292
+ centerIndex: prevSliderInfo.currentIndex,
293
+ itemIndexs: extendedItems.map((_, index)=>index),
294
+ prevStates: prevSliderInfo.elementStates
295
+ })
296
+ };
297
+ });
298
+ }, [
299
+ calcElementState,
300
+ extendedItems,
301
+ getNewStatesByItems
302
+ ]);
303
+ /**
304
+ * - 첫로드 또는 리사이즈 이벤트가 실행되면, 초기 높이와 위치를 초기화 합니다.
305
+ */ useLayoutEffect(()=>{
306
+ const handleResize = ()=>{
307
+ requestAnimationFrame(()=>{
308
+ if (!wrapRef.current) {
309
+ console.error("Slider 필수 요소에 접근할 수 없습니다.");
310
+ return;
311
+ }
312
+ // 높이, 드래그 임계값 계산
313
+ let estimatedHeight = 0;
314
+ let estimatedScrollThreshold = 0;
315
+ if (estimateSizeFromEveryElements) {
316
+ for (const [, { content }] of elementInfos.current){
317
+ if (content) {
318
+ const rect = content.getBoundingClientRect();
319
+ const { height } = rect;
320
+ if (height > estimatedHeight) {
321
+ estimatedHeight = height;
322
+ }
323
+ if (rect.width > estimatedScrollThreshold) {
324
+ estimatedScrollThreshold = rect.width * CAN_SCROLL_THRESHOLD_RATIO;
325
+ }
326
+ }
327
+ }
328
+ } else {
329
+ const firstElementInfo = elementInfos.current.get(0);
330
+ const rect = firstElementInfo?.content?.getBoundingClientRect();
331
+ estimatedHeight = rect?.height ?? 0;
332
+ estimatedScrollThreshold = (rect?.width ?? DEFAULT_SCROLL_THRESHOLD) * CAN_SCROLL_THRESHOLD_RATIO;
333
+ }
334
+ wrapRef.current.style.height = `${estimatedHeight}px`;
335
+ canScrollThresholdRef.current = estimatedScrollThreshold;
336
+ // 아이템 위치 계산
337
+ setSliderInfo((prevSliderInfo)=>{
338
+ return {
339
+ ...prevSliderInfo,
340
+ elementStates: getNewStatesByItems({
341
+ centerIndex: prevSliderInfo.currentIndex,
342
+ itemIndexs: prevSliderInfo.elementStates.map((_, index)=>index),
343
+ prevStates: prevSliderInfo.elementStates
344
+ })
345
+ };
346
+ });
347
+ });
348
+ };
349
+ // 초기 로드 시 높이 계산
350
+ handleResize();
351
+ window.addEventListener("resize", handleResize);
352
+ return ()=>{
353
+ window.removeEventListener("resize", handleResize);
354
+ };
355
+ }, [
356
+ calcElementState,
357
+ estimateSizeFromEveryElements,
358
+ getNewStatesByItems
359
+ ]);
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
+ }
395
+ }, [
396
+ extendedItems,
397
+ index,
398
+ items.length,
399
+ sliderInfo.currentIndex
400
+ ]);
401
+ /**
402
+ * - currentIndex 변경 시 애니메이션을 적용합니다.
403
+ * - 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
+ };
421
+ }
422
+ }, [
423
+ animationDuration,
424
+ sliderInfo.currentIndex,
425
+ updateStateByPageIndex
426
+ ]);
427
+ /**
428
+ * - currentIndex 변경 시 콜백을 호출합니다.
429
+ * - 원본 인덱스로 변환하여 전달합니다.
430
+ */ useEffect(()=>{
431
+ if (prevCallbackIndexRef.current !== sliderInfo.currentIndex) {
432
+ // 원본 인덱스로 변환
433
+ const originalIndex = items.length > 0 ? sliderInfo.currentIndex % items.length : 0;
434
+ stableOnIndexChange(originalIndex, lastSlideTriggerEvent.current);
435
+ prevCallbackIndexRef.current = sliderInfo.currentIndex;
436
+ }
437
+ }, [
438
+ items.length,
439
+ sliderInfo.currentIndex,
440
+ stableOnIndexChange
441
+ ]);
442
+ const { withPointerMove } = usePointerMove({
443
+ enabled: enableDrag,
444
+ target: wrapRef,
445
+ onDraggingNow: (isDragging)=>{
446
+ setEnableScrollAnimator(!isDragging);
447
+ onDraggingNow?.(isDragging);
448
+ },
449
+ onPointDrag: ({ isCancel, isEnd, transaction })=>{
450
+ if (animateNow.current) {
451
+ return;
452
+ }
453
+ const { primaryPointer } = transaction;
454
+ /**
455
+ * - 트랜잭션을 업데이트 합니다.
456
+ */ const calculate = primaryPointer?.calculate;
457
+ if (calculate) {
458
+ updateStateByDrag(calculate.diff);
459
+ if (isEnd) {
460
+ const diffX = Math.abs(calculate.diff.x);
461
+ if (!isCancel && diffX > canScrollThresholdRef.current) {
462
+ lastSlideTriggerEvent.current = "drag";
463
+ if (calculate.diff.x < 0) {
464
+ doNext();
465
+ } else {
466
+ doPrev();
467
+ }
468
+ } else {
469
+ // 제자리로 움직이게 합니다.
470
+ setEnableScrollAnimator(true);
471
+ setSliderInfo((prev)=>({
472
+ ...prev,
473
+ elementStates: getNewStatesByItems({
474
+ centerIndex: prev.currentIndex,
475
+ enableAnimation: true,
476
+ itemIndexs: prev.elementStates.map((_, index)=>index),
477
+ prevStates: prev.elementStates
478
+ })
479
+ }));
480
+ }
481
+ }
482
+ }
483
+ }
484
+ });
485
+ useAccessibilityHandler({
486
+ target: wrapRef,
487
+ handler: (event)=>{
488
+ if (event.key === "ArrowLeft") {
489
+ event.preventDefault();
490
+ lastSlideTriggerEvent.current = "swipe";
491
+ doPrev();
492
+ } else if (event.key === "ArrowRight") {
493
+ event.preventDefault();
494
+ lastSlideTriggerEvent.current = "swipe";
495
+ doNext();
496
+ }
497
+ }
498
+ });
499
+ return /*#__PURE__*/ _jsx("ul", {
500
+ "aria-roledescription": "carousel",
501
+ role: "region",
502
+ ...wrapProps,
503
+ ...withPointerMove,
504
+ className: [
505
+ "watcha-react-slider-wrap",
506
+ wrapProps?.className
507
+ ].join(" "),
508
+ style: {
509
+ ...wrapProps?.style,
510
+ ...withPointerMove.style,
511
+ touchAction: enableDrag ? "pan-y" : "none"
512
+ },
513
+ children: extendedItems.map(({ extendedIndex, item, originalIndex })=>{
514
+ const state = sliderInfo.elementStates[extendedIndex];
515
+ const isFocused = sliderInfo.currentIndex === extendedIndex;
516
+ return /*#__PURE__*/ _jsx(SliderItemContextProvider, {
517
+ immediate: !enableScrollAnimator,
518
+ isFocused: isFocused,
519
+ itemIndex: originalIndex,
520
+ slideTriggerEvent: lastSlideTriggerEvent.current,
521
+ transition: state ? state.transition : 0,
522
+ children: /*#__PURE__*/ _jsx("li", {
523
+ ...itemProps,
524
+ ref: (element)=>{
525
+ updateElement({
526
+ elementInfo: {
527
+ item: element
528
+ },
529
+ index: extendedIndex
530
+ });
531
+ },
532
+ "aria-current": isFocused ? "true" : undefined,
533
+ className: [
534
+ "watcha-react-slider-item",
535
+ itemProps?.className
536
+ ].join(" "),
537
+ style: state ? {
538
+ transform: `translate(${state.point.x}px, ${state.point.y}px)`,
539
+ transition: enableScrollAnimator && state.enableAnimation ? `transform ${animationDuration}ms ${animationTimingFunction}` : undefined,
540
+ zIndex: state.zIndex
541
+ } : undefined,
542
+ children: /*#__PURE__*/ _jsx("div", {
543
+ ...contentProps,
544
+ ref: (element)=>{
545
+ updateElement({
546
+ elementInfo: {
547
+ content: element
548
+ },
549
+ index: extendedIndex
550
+ });
551
+ },
552
+ className: [
553
+ "watcha-react-slider-content",
554
+ contentProps?.className
555
+ ].join(" "),
556
+ children: onCreateItemView(item, originalIndex)
557
+ })
558
+ })
559
+ }, `${onItemKey(item)}-${extendedIndex}`);
560
+ })
561
+ });
562
+ };
563
+ /**
564
+ * # 루프 슬라이더 컴포넌트
565
+ *
566
+ * ## Element 구조
567
+ * - wrap: 슬라이더를 감싸는 요소
568
+ * - item: 슬라이더 하위의 아이템 요소
569
+ * - content: 슬라이더 아이템 내부의 컨텐츠 요소 (높이영향)
570
+ *
571
+ * ## 아이템 복제 로직
572
+ * 아이템 개수가 부족하면 자동으로 복제하여 자연스러운 루프를 구현합니다.
573
+ *
574
+ * ### 최소 필요 개수 (visibleCount=1 기준): 5개
575
+ * - 좌측 (visibleCount + 1) = 2개 (보이는 1개 + 대기 1개)
576
+ * - 중앙 = 1개
577
+ * - 우측 (visibleCount + 1) = 2개 (보이는 1개 + 대기 1개)
578
+ *
579
+ * ### 아이템 갯수에 따른 복제 동작
580
+ * - 5개 이상: 복제 없음, 원본 그대로(x1)
581
+ * - 4개: 8개로 복제(x2)
582
+ * - 3개: 6개로 복제(x2)
583
+ * - 2개: 6개로 복제(x3)
584
+ * - 1개: 5개로 복제(x5)
585
+ */ export const Slider = /*#__PURE__*/ forwardRef(SliderComponent);
@@ -0,0 +1,5 @@
1
+ export * from "./component/context/slider-context.js";
2
+ export * from "./component/context/slider-context-provider.js";
3
+ export * from "./component/hook/use-slider-context.js";
4
+ export * from "./component/view/slider.js";
5
+ export * from "./script/type/slider-types.js";
@@ -0,0 +1,3 @@
1
+ /**
2
+ * - 슬라이드시 이벤트 트리거 종류
3
+ */ export { };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .watcha-react-slider-wrap{display:flex;justify-content:center;position:relative}.watcha-react-slider-item{inset:0;position:absolute}.watcha-react-slider-content{position:relative}
@@ -0,0 +1,18 @@
1
+ import { type SlideTriggerEvent } from "../../script/type/slider-types";
2
+ export type SliderItemContextProviderActions = {
3
+ /**
4
+ * - 즉각 반영 여부 (true: CSS transition 없이, false: CSS transition 적용)
5
+ */
6
+ immediate: boolean;
7
+ isFocused: boolean;
8
+ itemIndex: number;
9
+ slideTriggerEvent: SlideTriggerEvent;
10
+ /**
11
+ * - 중앙으로부터의 거리 비율 (0~1)
12
+ */
13
+ transition: number;
14
+ };
15
+ export type SliderItemContextProviderProps = {
16
+ children: React.ReactNode;
17
+ } & SliderItemContextProviderActions;
18
+ export declare const SliderItemContextProvider: ({ children, immediate, isFocused, itemIndex, slideTriggerEvent, transition, }: SliderItemContextProviderProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import type { SliderItemContextProviderActions } from "./slider-context-provider";
3
+ export declare const SliderItemContext: React.Context<SliderItemContextProviderActions | null>;
@@ -0,0 +1,13 @@
1
+ type UseSliderContextProps = {
2
+ onBlur?: () => void;
3
+ onFocus?: (isAutoSlide: boolean) => void;
4
+ onInitialState?: (focused: boolean) => void;
5
+ /**
6
+ * - 페이지 전환 시 호출되는 콜백입니다.
7
+ * - t: 0 ~ 1 사이의 값 (0: fade in, 1: fade out)
8
+ * - immediate: true 면 실시간으로 값이 바뀌고 있음을 의미하고, false 면 값이 완전히 바뀌었으므로 애니메이션을 트리거 할 수 있다는 의미 입니다.
9
+ */
10
+ onTransitionChange?: (t: number, immediate: boolean) => void;
11
+ };
12
+ export declare const useSliderContext: (callbacks: UseSliderContextProps) => void;
13
+ export {};