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