react-spring-carousel 3.0.0-beta090.51 → 3.0.0-beta090.53

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.
Files changed (58) hide show
  1. package/.storybook/main.ts +17 -0
  2. package/.storybook/preview.ts +15 -0
  3. package/README.md +50 -0
  4. package/eslint.config.js +28 -0
  5. package/index.html +13 -0
  6. package/lib/events.ts +39 -0
  7. package/lib/index.tsx +980 -0
  8. package/lib/types.ts +44 -0
  9. package/lib/useEventsModule.ts +51 -0
  10. package/lib/utils.ts +42 -0
  11. package/lib/vite-env.d.ts +1 -0
  12. package/package.json +36 -84
  13. package/src/stories/Button.stories.ts +52 -0
  14. package/src/stories/Button.tsx +48 -0
  15. package/src/stories/Configure.mdx +364 -0
  16. package/src/stories/Main.stories.ts +14 -0
  17. package/src/stories/Main.tsx +87 -0
  18. package/src/stories/assets/accessibility.png +0 -0
  19. package/src/stories/assets/accessibility.svg +1 -0
  20. package/src/stories/assets/addon-library.png +0 -0
  21. package/src/stories/assets/assets.png +0 -0
  22. package/src/stories/assets/avif-test-image.avif +0 -0
  23. package/src/stories/assets/context.png +0 -0
  24. package/src/stories/assets/discord.svg +1 -0
  25. package/src/stories/assets/docs.png +0 -0
  26. package/src/stories/assets/figma-plugin.png +0 -0
  27. package/src/stories/assets/github.svg +1 -0
  28. package/src/stories/assets/share.png +0 -0
  29. package/src/stories/assets/styling.png +0 -0
  30. package/src/stories/assets/testing.png +0 -0
  31. package/src/stories/assets/theming.png +0 -0
  32. package/src/stories/assets/tutorials.svg +1 -0
  33. package/src/stories/assets/youtube.svg +1 -0
  34. package/src/stories/button.css +30 -0
  35. package/src/stories/main.css +37 -0
  36. package/src/storybook.global.styles.css +5 -0
  37. package/tsconfig.app.json +24 -0
  38. package/tsconfig.json +7 -0
  39. package/tsconfig.node.json +22 -0
  40. package/vite.config.ts +30 -0
  41. package/dist/index.cjs.js +0 -28
  42. package/dist/index.cjs.js.map +0 -1
  43. package/dist/index.es.js +0 -1728
  44. package/dist/index.es.js.map +0 -1
  45. package/dist/types/index.d.ts +0 -3
  46. package/dist/types/modules/index.d.ts +0 -1
  47. package/dist/types/modules/useEventsModule.d.ts +0 -5
  48. package/dist/types/modules/useFullscreenModule.d.ts +0 -6
  49. package/dist/types/modules/useThumbsModule.d.ts +0 -6
  50. package/dist/types/types/common.d.ts +0 -36
  51. package/dist/types/types/index.d.ts +0 -4
  52. package/dist/types/types/useEventsModule.types.d.ts +0 -46
  53. package/dist/types/types/useFullscreenModule.types.d.ts +0 -6
  54. package/dist/types/types/useSpringCarousel.types.d.ts +0 -130
  55. package/dist/types/types/useThumbsModule.types.d.ts +0 -9
  56. package/dist/types/types/useTransitionCarousel.types.d.ts +0 -37
  57. package/dist/types/useSpringCarousel.d.ts +0 -12
  58. package/dist/types/useTransitionCarousel.d.ts +0 -5
package/lib/index.tsx ADDED
@@ -0,0 +1,980 @@
1
+ import { ElementRef, useEffect, useId, useRef } from "react";
2
+ import { Props } from "./types";
3
+ import { Controller, useSpring } from "@react-spring/web";
4
+ import { useEventsModule } from "./useEventsModule";
5
+ import { useDrag } from "@use-gesture/react";
6
+ import { SlideActionType } from "./events";
7
+ import { isOutOfViewport, pFloat, logWarn } from "./utils";
8
+
9
+ export function useSpringCarousel({
10
+ init = true,
11
+ items: _items,
12
+ slideType = "fixed",
13
+ scrollAmount: _scrollAmount,
14
+ withLoop = false,
15
+ enableGestures = true,
16
+ carouselAxis = "x",
17
+ slideWhenDragThresholdIsReached = true,
18
+ itemsPerSlide: _itemsPerSlide,
19
+ scrollAmountType: _scrollAmountType,
20
+ gutter = 0,
21
+ startEndGutter = 0,
22
+ fadeIn = false,
23
+ }: Props) {
24
+ const carouselIsInitialized = useRef(false);
25
+ const errorMessages = useRef<string[]>([]);
26
+ const windowIsHidden = useRef(false);
27
+
28
+ const itemsPerSlide = _itemsPerSlide ?? 1;
29
+ const scrollAmountType = _scrollAmountType ?? "slide";
30
+
31
+ const items = withLoop
32
+ ? [
33
+ ..._items.map((i) => ({
34
+ ...i,
35
+ id: `prev-repeated-item-${i.id}`,
36
+ })),
37
+ ..._items,
38
+ ..._items.map((i) => ({
39
+ ...i,
40
+ id: `next-repeated-item-${i.id}`,
41
+ })),
42
+ ]
43
+ : _items;
44
+
45
+ const scrollAmount = useRef(_scrollAmount);
46
+ const dragTreshold = useRef(0);
47
+ const carouselId = useId().replace(/:/g, "");
48
+ const carouselContainerRef = useRef<ElementRef<"div">>(null);
49
+ const carouselTrackRef = useRef<ElementRef<"div">>(null);
50
+ const currentSlidedValue = useRef(0);
51
+
52
+ const startReached = useRef<boolean | undefined>(true);
53
+ const endReached = useRef<boolean | undefined>(false);
54
+
55
+ const activeItem = useRef(0);
56
+
57
+ const [spring, setSpring] = useSpring(() => ({
58
+ value: 0,
59
+ onChange({ value }) {
60
+ if (slideType === "fixed" || slideType === "fluid") {
61
+ if (carouselAxis === "x") {
62
+ carouselTrackRef.current!.style.transform = `translate3d(${value.value}px, 0px, 0px)`;
63
+ } else {
64
+ carouselTrackRef.current!.style.transform = `translate3d(${value.value}px, 0px, 0px)`;
65
+ }
66
+ }
67
+ if (slideType === "freeScroll") {
68
+ carouselTrackRef.current![
69
+ carouselAxis === "x" ? "scrollLeft" : "scrollTop"
70
+ ] = Math.abs(value.value);
71
+ }
72
+ },
73
+ }));
74
+
75
+ const { useListenToCustomEvent, emitEvent } = useEventsModule();
76
+
77
+ /**
78
+ * Internal utility helpers
79
+ */
80
+ function getScrollAmount() {
81
+ return pFloat(scrollAmount.current ?? 0);
82
+ }
83
+ function getTotalScrollAvailableSpace() {
84
+ return (
85
+ carouselTrackRef.current![
86
+ carouselAxis === "x" ? "scrollWidth" : "scrollHeight"
87
+ ] -
88
+ carouselContainerRef.current!.getBoundingClientRect()[
89
+ carouselAxis === "x" ? "width" : "height"
90
+ ]
91
+ );
92
+ }
93
+ function handleAppNotInitialized() {
94
+ logWarn("The carousel can't be initialized. List of errors:");
95
+ console.table(errorMessages.current);
96
+ }
97
+ function getScrollHandlers() {
98
+ if (slideType === "freeScroll") {
99
+ return {
100
+ onWheel() {
101
+ spring.value.stop();
102
+ // setStartEndItemReachedOnFreeScroll()
103
+ },
104
+ onScroll(e: React.UIEvent<HTMLDivElement, UIEvent>) {
105
+ const target = e.currentTarget;
106
+ const scrollValue =
107
+ carouselAxis === "x" ? target.scrollLeft : target.scrollTop;
108
+ const availableScrollSpace =
109
+ carouselAxis === "x"
110
+ ? target.scrollWidth - target.clientWidth
111
+ : target.scrollHeight - target.clientHeight;
112
+
113
+ if (scrollValue === 0) {
114
+ startReached.current = true;
115
+ } else if (scrollValue === availableScrollSpace) {
116
+ endReached.current = true;
117
+ } else {
118
+ startReached.current = false;
119
+ endReached.current = false;
120
+ }
121
+ },
122
+ };
123
+ }
124
+ return {};
125
+ }
126
+ function getGutterCssVariable() {
127
+ let totalGutterCssVar = 0;
128
+ let totalStartEndGutterCssVar = 0;
129
+
130
+ const startEndGutterCssVar = getComputedStyle(
131
+ document.documentElement
132
+ ).getPropertyValue(`--${carouselId}-react-spring-carousel-item-gutter`);
133
+ const gutterCssVar = getComputedStyle(
134
+ document.documentElement
135
+ ).getPropertyValue(`--${carouselId}-react-spring-carousel-item-gutter`);
136
+
137
+ if (gutterCssVar.includes("px")) {
138
+ totalGutterCssVar = Number(gutterCssVar.replace("px", ""));
139
+ }
140
+ if (startEndGutterCssVar.includes("px")) {
141
+ totalStartEndGutterCssVar = Number(
142
+ startEndGutterCssVar.replace("px", "")
143
+ );
144
+ }
145
+
146
+ return { totalGutterCssVar, totalStartEndGutterCssVar };
147
+ }
148
+ function getCarouselItemDimension() {
149
+ if (itemsPerSlide > 1) {
150
+ return `calc(100% / ${itemsPerSlide} - var(--${carouselId}-react-spring-carousel-item-gutter) / ${itemsPerSlide} * ${itemsPerSlide - 1}) !important`;
151
+ }
152
+ return `100% !important`;
153
+ }
154
+
155
+ type SlideToItemProps = {
156
+ type: "prev" | "next";
157
+ actionType: SlideActionType;
158
+ newActiveItem?: number;
159
+ };
160
+ function slideToItemValue({
161
+ type,
162
+ actionType,
163
+ newActiveItem,
164
+ }: SlideToItemProps) {
165
+ let total = 0;
166
+ let from = spring.value.get();
167
+
168
+ startReached.current = false;
169
+ endReached.current = false;
170
+
171
+ if (slideType === "fixed") {
172
+ const currentActiveItemIndex = activeItem.current;
173
+
174
+ if (type === "prev") {
175
+ activeItem.current = newActiveItem ?? activeItem.current - 1;
176
+ }
177
+ if (type === "next") {
178
+ activeItem.current = newActiveItem ?? activeItem.current + 1;
179
+ }
180
+
181
+ if (
182
+ withLoop &&
183
+ scrollAmountType === "group" &&
184
+ itemsPerSlide > 1 &&
185
+ type === "next"
186
+ ) {
187
+ const totalGroups = _items.length / itemsPerSlide;
188
+ const nextGroupIsLastGroup =
189
+ Math.ceil(totalGroups) - 1 === activeItem.current;
190
+ const nextGroupIsFirstGroup =
191
+ Math.ceil(totalGroups) === activeItem.current;
192
+
193
+ if (nextGroupIsLastGroup) {
194
+ endReached.current = true;
195
+ }
196
+
197
+ total = -(activeItem.current * getScrollAmount());
198
+
199
+ if (nextGroupIsFirstGroup) {
200
+ activeItem.current = 0;
201
+
202
+ from = spring.value.get() + getScrollAmount() * totalGroups;
203
+ total = 0;
204
+
205
+ endReached.current = false;
206
+ startReached.current = true;
207
+ }
208
+ }
209
+
210
+ if (
211
+ withLoop &&
212
+ scrollAmountType === "group" &&
213
+ itemsPerSlide > 1 &&
214
+ type === "prev"
215
+ ) {
216
+ const totalGroups = _items.length / itemsPerSlide;
217
+ const isFirstGroup = activeItem.current === 0;
218
+ const nextGroupIsRepeatedLastGroup = activeItem.current === -1;
219
+
220
+ total = -(activeItem.current * getScrollAmount());
221
+
222
+ if (isFirstGroup) {
223
+ startReached.current = true;
224
+ }
225
+
226
+ if (nextGroupIsRepeatedLastGroup) {
227
+ startReached.current = false;
228
+ endReached.current = true;
229
+ activeItem.current = totalGroups - 1;
230
+
231
+ from = spring.value.get() - getScrollAmount() * totalGroups;
232
+ total = -getScrollAmount() * totalGroups + getScrollAmount();
233
+ }
234
+ }
235
+
236
+ if (!withLoop && scrollAmountType === "group" && itemsPerSlide > 1) {
237
+ const totalGroups = _items.length / itemsPerSlide;
238
+ const lastGroupIsNotFilled = 2 % totalGroups !== 0;
239
+ const nextGroupIsLastGroup =
240
+ Math.ceil(totalGroups - 1) === activeItem.current;
241
+ const nextGroupIsFirstGroup = activeItem.current === 0;
242
+
243
+ total = -(activeItem.current * getScrollAmount());
244
+
245
+ if (nextGroupIsFirstGroup) {
246
+ startReached.current = true;
247
+ }
248
+ if (nextGroupIsLastGroup) {
249
+ endReached.current = true;
250
+ if (lastGroupIsNotFilled) {
251
+ total = -getTotalScrollAvailableSpace();
252
+ }
253
+ }
254
+ }
255
+
256
+ if (
257
+ withLoop &&
258
+ type === "next" &&
259
+ (scrollAmountType === "slide" ||
260
+ (scrollAmountType === "group" && itemsPerSlide === 1))
261
+ ) {
262
+ const currentItemIsLastItem =
263
+ _items[activeItem.current]?.id === _items[_items.length - 1].id;
264
+ const nextItemIsRepeatedItem =
265
+ items[_items.length + activeItem.current].id.includes(
266
+ "repeated-item"
267
+ );
268
+
269
+ if (currentItemIsLastItem) {
270
+ endReached.current = true;
271
+ }
272
+ if (nextItemIsRepeatedItem) {
273
+ activeItem.current = 0;
274
+
275
+ from = spring.value.get() + getScrollAmount() * _items.length;
276
+
277
+ endReached.current = false;
278
+ startReached.current = true;
279
+ }
280
+
281
+ total = -(activeItem.current * getScrollAmount());
282
+ }
283
+
284
+ if (
285
+ withLoop &&
286
+ type === "prev" &&
287
+ (scrollAmountType === "slide" ||
288
+ (scrollAmountType === "group" && itemsPerSlide === 1))
289
+ ) {
290
+ const currentItemIndex = items.findIndex(
291
+ (i) => i.id === _items[currentActiveItemIndex].id
292
+ );
293
+ const currentItemIsFirstItem =
294
+ _items[activeItem.current]?.id === _items[0].id;
295
+ const nextItemIsRepeatedItem =
296
+ items[currentItemIndex - 1].id.includes("repeated-item");
297
+
298
+ if (currentItemIsFirstItem) {
299
+ startReached.current = true;
300
+ }
301
+ if (nextItemIsRepeatedItem) {
302
+ startReached.current = false;
303
+ endReached.current = true;
304
+ activeItem.current = _items.length - 1;
305
+
306
+ from = spring.value.get() - getScrollAmount() * _items.length;
307
+ }
308
+ total = -(activeItem.current * getScrollAmount());
309
+ }
310
+
311
+ if (!withLoop && scrollAmountType === "slide") {
312
+ const nextItemIsLastItem =
313
+ _items[activeItem.current + 1]?.id === _items[_items.length - 1].id;
314
+
315
+ if (nextItemIsLastItem) {
316
+ endReached.current = true;
317
+ } else if (activeItem.current === 0) {
318
+ startReached.current = true;
319
+ } else {
320
+ startReached.current = false;
321
+ endReached.current = false;
322
+ }
323
+
324
+ total = -(activeItem.current * getScrollAmount());
325
+
326
+ if (
327
+ type === "next" &&
328
+ Math.abs(total) > getTotalScrollAvailableSpace()
329
+ ) {
330
+ endReached.current = true;
331
+ total = -getTotalScrollAvailableSpace();
332
+ }
333
+ }
334
+
335
+ emitEvent({
336
+ eventName: "onSlideStartChange",
337
+ sliceActionType: actionType,
338
+ slideDirection: type,
339
+ nextItem: {
340
+ index: activeItem.current,
341
+ id: _items[activeItem.current].id,
342
+ startReached: startReached.current,
343
+ endReached: endReached.current,
344
+ },
345
+ });
346
+ }
347
+ if (slideType === "fluid") {
348
+ if (type === "prev") {
349
+ activeItem.current = newActiveItem ?? activeItem.current - 1;
350
+ }
351
+ if (type === "next") {
352
+ activeItem.current = newActiveItem ?? activeItem.current + 1;
353
+ }
354
+ total = -(activeItem.current * getScrollAmount());
355
+
356
+ if (type === "next" && withLoop) {
357
+ const nextItemIsLastItem =
358
+ _items[activeItem.current]?.id === _items[_items.length - 1].id;
359
+ const nextItemIsRepeatedItem =
360
+ items[_items.length + activeItem.current].id.includes(
361
+ "repeated-item"
362
+ );
363
+
364
+ if (nextItemIsLastItem) {
365
+ endReached.current = true;
366
+ }
367
+ if (nextItemIsRepeatedItem) {
368
+ activeItem.current = 0;
369
+ from = spring.value.get() + getScrollAmount() * _items.length;
370
+ total = 0;
371
+ endReached.current = false;
372
+ startReached.current = true;
373
+ }
374
+ }
375
+ if (type === "next" && !withLoop) {
376
+ const nextItemWillExceed =
377
+ Math.abs(total) > getTotalScrollAvailableSpace();
378
+
379
+ if (nextItemWillExceed) {
380
+ endReached.current = true;
381
+ total = -getTotalScrollAvailableSpace();
382
+ } else if (nextItemWillExceed) {
383
+ endReached.current = true;
384
+ total = -getTotalScrollAvailableSpace();
385
+ } else {
386
+ startReached.current = false;
387
+ endReached.current = false;
388
+ }
389
+ }
390
+ if (type === "prev" && !withLoop) {
391
+ const currentItemIsFirstItem =
392
+ _items[activeItem.current]?.id === _items[0].id;
393
+
394
+ if (currentItemIsFirstItem) {
395
+ startReached.current = true;
396
+ }
397
+ }
398
+ if (type === "prev" && withLoop) {
399
+ const currentItemIsFirstItem =
400
+ _items[activeItem.current]?.id === _items[0].id;
401
+ const nextItemIsRepeatedItem =
402
+ items[_items.length + activeItem.current]?.id.includes(
403
+ "repeated-item"
404
+ );
405
+
406
+ if (currentItemIsFirstItem) {
407
+ startReached.current = true;
408
+ }
409
+
410
+ if (nextItemIsRepeatedItem) {
411
+ activeItem.current = _items.length - 1;
412
+ from = spring.value.get() - getScrollAmount() * _items.length;
413
+ total = -(getScrollAmount() * _items.length - getScrollAmount());
414
+
415
+ startReached.current = false;
416
+ endReached.current = true;
417
+ }
418
+ }
419
+
420
+ emitEvent({
421
+ eventName: "onSlideStartChange",
422
+ sliceActionType: actionType,
423
+ slideDirection: type,
424
+ nextItem: {
425
+ startReached: startReached.current,
426
+ endReached: endReached.current,
427
+ index: 0,
428
+ id: "",
429
+ },
430
+ });
431
+ }
432
+ if (slideType === "freeScroll") {
433
+ const scrollValue =
434
+ carouselTrackRef.current![
435
+ carouselAxis === "x" ? "scrollLeft" : "scrollTop"
436
+ ];
437
+ const availableScrollSpace =
438
+ carouselAxis === "x"
439
+ ? carouselTrackRef.current!.scrollWidth -
440
+ carouselTrackRef.current!.clientWidth
441
+ : carouselTrackRef.current!.scrollHeight -
442
+ carouselTrackRef.current!.clientHeight;
443
+
444
+ from = scrollValue;
445
+
446
+ if (type === "prev") {
447
+ total = from - getScrollAmount();
448
+ if (total < 0) {
449
+ total = 0;
450
+ }
451
+ }
452
+ if (type === "next") {
453
+ total = from + getScrollAmount();
454
+ if (total > availableScrollSpace) {
455
+ total = availableScrollSpace;
456
+ }
457
+ }
458
+ }
459
+
460
+ const parsedFrom = pFloat(from);
461
+ const parsedTotal = pFloat(total);
462
+ currentSlidedValue.current = parsedTotal;
463
+
464
+ setSpring.start({
465
+ from: {
466
+ value: parsedFrom,
467
+ },
468
+ to: {
469
+ value: parsedTotal,
470
+ },
471
+ onRest({ finished }) {
472
+ if (finished && slideType === "fixed") {
473
+ emitEvent({
474
+ eventName: "onSlideChangeComplete",
475
+ sliceActionType: actionType,
476
+ slideDirection: type,
477
+ currentItem: {
478
+ index: activeItem.current,
479
+ id: _items[activeItem.current].id,
480
+ startReached: startReached.current,
481
+ endReached: endReached.current,
482
+ },
483
+ });
484
+ }
485
+ if (finished && slideType === "fluid") {
486
+ emitEvent({
487
+ eventName: "onSlideChangeComplete",
488
+ sliceActionType: actionType,
489
+ slideDirection: type,
490
+ currentItem: {
491
+ index: 0,
492
+ id: "",
493
+ startReached: startReached.current,
494
+ endReached: endReached.current,
495
+ },
496
+ });
497
+ }
498
+ },
499
+ });
500
+ }
501
+ function slideToNextItem(actionType: SlideActionType, index?: number) {
502
+ if (!carouselIsInitialized.current) {
503
+ handleAppNotInitialized();
504
+ return;
505
+ }
506
+
507
+ if (
508
+ (slideType === "fixed" && withLoop) ||
509
+ (slideType === "fixed" && !withLoop && !endReached.current)
510
+ ) {
511
+ const itemIndex = index ?? activeItem.current + 1;
512
+ slideToItemValue({
513
+ type: "next",
514
+ actionType,
515
+ newActiveItem: itemIndex,
516
+ });
517
+ }
518
+ if (
519
+ (slideType === "fluid" && withLoop) ||
520
+ (slideType === "fluid" && !withLoop && !endReached.current) ||
521
+ slideType === "freeScroll"
522
+ ) {
523
+ slideToItemValue({
524
+ type: "next",
525
+ actionType,
526
+ });
527
+ }
528
+ }
529
+ function slideToPrevItem(actionType: SlideActionType, index?: number) {
530
+ if (!carouselIsInitialized.current) {
531
+ handleAppNotInitialized();
532
+ return;
533
+ }
534
+
535
+ if (
536
+ (slideType === "fixed" && withLoop) ||
537
+ (slideType === "fixed" && !withLoop && !startReached.current)
538
+ ) {
539
+ const itemIndex = index ?? activeItem.current - 1;
540
+ slideToItemValue({
541
+ type: "prev",
542
+ actionType,
543
+ newActiveItem: itemIndex,
544
+ });
545
+ }
546
+ if (
547
+ (slideType === "fluid" && withLoop) ||
548
+ (slideType === "fluid" && !withLoop && !startReached.current) ||
549
+ slideType === "freeScroll"
550
+ ) {
551
+ slideToItemValue({
552
+ type: "prev",
553
+ actionType,
554
+ });
555
+ }
556
+ }
557
+ function handleSlideToItem(id: string | number) {
558
+ let itemIndex = 0;
559
+ if (typeof id === "string") {
560
+ itemIndex = _items.findIndex((i) => i.id === id);
561
+ } else {
562
+ itemIndex = id;
563
+ }
564
+
565
+ if (itemIndex > activeItem.current) {
566
+ slideToNextItem("click", itemIndex);
567
+ }
568
+ if (itemIndex < activeItem.current) {
569
+ slideToPrevItem("click", itemIndex);
570
+ }
571
+ }
572
+ type ThumbsContainerScrollProps = {
573
+ activeItem: number;
574
+ getContainer(): HTMLElement | null;
575
+ updateTotalValue?(props: {
576
+ from: number;
577
+ to: number;
578
+ itemOutOfViewport: {
579
+ isOut: boolean;
580
+ direction: "start" | "end" | null;
581
+ };
582
+ }): number;
583
+ };
584
+ function handleThumbsContainerScroll({
585
+ getContainer,
586
+ activeItem,
587
+ updateTotalValue,
588
+ }: ThumbsContainerScrollProps) {
589
+ const container = getContainer();
590
+
591
+ if (!(container instanceof HTMLElement)) {
592
+ console.warn(
593
+ `Container is not a valid html element: container is ${container}`
594
+ );
595
+ return;
596
+ }
597
+
598
+ const item = container.children[activeItem] as HTMLElement;
599
+
600
+ if (item) {
601
+ const availableScrollableSpace =
602
+ container[carouselAxis === "x" ? "scrollWidth" : "scrollHeight"] -
603
+ container.getBoundingClientRect()[
604
+ carouselAxis === "x" ? "width" : "height"
605
+ ];
606
+
607
+ const itemPosition = item.offsetLeft + item.offsetWidth / 2;
608
+ const to = itemPosition - container.clientWidth / 2;
609
+
610
+ const outOfViewport = isOutOfViewport(item);
611
+
612
+ if (outOfViewport.isOut) {
613
+ const totalScroll =
614
+ outOfViewport.direction === "start"
615
+ ? to < 0
616
+ ? 0
617
+ : to
618
+ : to > availableScrollableSpace
619
+ ? availableScrollableSpace
620
+ : to;
621
+
622
+ const fromValue =
623
+ container[carouselAxis === "x" ? "scrollLeft" : "scrollTop"];
624
+
625
+ new Controller({
626
+ from: {
627
+ value: fromValue,
628
+ },
629
+ to: {
630
+ value: updateTotalValue
631
+ ? updateTotalValue({
632
+ from: fromValue,
633
+ to: totalScroll,
634
+ itemOutOfViewport: outOfViewport,
635
+ })
636
+ : totalScroll,
637
+ },
638
+ onChange({ value }) {
639
+ container[carouselAxis === "x" ? "scrollLeft" : "scrollTop"] =
640
+ value.value;
641
+ },
642
+ });
643
+ }
644
+ }
645
+ }
646
+
647
+ const bindDrag = useDrag(
648
+ (state) => {
649
+ if (!carouselIsInitialized.current) {
650
+ handleAppNotInitialized();
651
+ return;
652
+ }
653
+
654
+ const isDragging = state.dragging;
655
+ const movement = state.offset[carouselAxis === "x" ? 0 : 1];
656
+ const currentMovement = state.movement[carouselAxis === "x" ? 0 : 1];
657
+
658
+ const prevItemTresholdReached = currentMovement > dragTreshold.current;
659
+ const nextItemTresholdReached = currentMovement < -dragTreshold.current;
660
+
661
+ const velocity = state.velocity;
662
+
663
+ if (isDragging) {
664
+ emitEvent({
665
+ ...state,
666
+ eventName: "onDrag",
667
+ slideActionType: "drag",
668
+ });
669
+
670
+ setSpring.start({
671
+ value: movement,
672
+ immediate: true,
673
+ config: {
674
+ velocity: velocity,
675
+ },
676
+ });
677
+
678
+ if (
679
+ slideWhenDragThresholdIsReached &&
680
+ (prevItemTresholdReached || nextItemTresholdReached)
681
+ ) {
682
+ state.cancel();
683
+ }
684
+ }
685
+
686
+ if (state.last) {
687
+ if (prevItemTresholdReached) {
688
+ slideToPrevItem("drag");
689
+ } else if (nextItemTresholdReached) {
690
+ slideToNextItem("drag");
691
+ } else {
692
+ setSpring.start({
693
+ value: currentSlidedValue.current,
694
+ config: {
695
+ velocity,
696
+ },
697
+ });
698
+ }
699
+ }
700
+ },
701
+ {
702
+ enabled: enableGestures && slideType !== "freeScroll",
703
+ axis: carouselAxis,
704
+ rubberband: !withLoop,
705
+ ...(!withLoop
706
+ ? {
707
+ bounds: () => {
708
+ return {
709
+ right: 0,
710
+ left: -getTotalScrollAvailableSpace(),
711
+ top: -getTotalScrollAvailableSpace(),
712
+ bottom: 0,
713
+ };
714
+ },
715
+ }
716
+ : {}),
717
+ from: () => {
718
+ return [spring.value.get(), spring.value.get()];
719
+ },
720
+ }
721
+ );
722
+
723
+ useEffect(() => {
724
+ function handleVisibilityChange() {
725
+ if (document.hidden) {
726
+ windowIsHidden.current = true;
727
+ carouselIsInitialized.current = false;
728
+ } else {
729
+ windowIsHidden.current = false;
730
+ carouselIsInitialized.current = true;
731
+ }
732
+ }
733
+ function handleSetBasicCarouselPosition() {
734
+ if ((slideType === "fixed" && !withLoop) || slideType === "freeScroll") {
735
+ return;
736
+ }
737
+ if (
738
+ slideType === "fixed" &&
739
+ scrollAmountType === "group" &&
740
+ itemsPerSlide > 1
741
+ ) {
742
+ const totalGroups = (_items.length * 3) / itemsPerSlide;
743
+ carouselTrackRef.current!.style[carouselAxis === "x" ? "left" : "top"] =
744
+ `-${pFloat((getScrollAmount() * totalGroups) / 3)}px`;
745
+ } else {
746
+ carouselTrackRef.current!.style[carouselAxis === "x" ? "left" : "top"] =
747
+ `-${pFloat((getScrollAmount() * items.length) / 3)}px`;
748
+ }
749
+ }
750
+ function handleResize() {
751
+ handleSetScrollAmount();
752
+ handleSetBasicCarouselPosition();
753
+
754
+ setSpring.start({
755
+ immediate: true,
756
+ value: -(activeItem.current * getScrollAmount()),
757
+ });
758
+ }
759
+ function handleSetScrollAmount() {
760
+ const firstItem = carouselTrackRef.current!.children[0] as HTMLElement;
761
+ let total = 0;
762
+
763
+ const isFixedGroup =
764
+ slideType === "fixed" &&
765
+ scrollAmountType === "group" &&
766
+ itemsPerSlide > 1;
767
+
768
+ if (isFixedGroup) {
769
+ total = pFloat(
770
+ carouselTrackRef.current!.getBoundingClientRect()[
771
+ carouselAxis === "x" ? "width" : "height"
772
+ ]
773
+ );
774
+ } else {
775
+ total = pFloat(
776
+ firstItem.getBoundingClientRect()[
777
+ carouselAxis === "x" ? "width" : "height"
778
+ ]
779
+ );
780
+ }
781
+
782
+ let { totalGutterCssVar, totalStartEndGutterCssVar } =
783
+ getGutterCssVariable();
784
+
785
+ total += totalGutterCssVar;
786
+ if (isFixedGroup) {
787
+ total -= totalStartEndGutterCssVar;
788
+ }
789
+ scrollAmount.current = total;
790
+
791
+ return total;
792
+ }
793
+ function initCarousel() {
794
+ errorMessages.current = [];
795
+ /**
796
+ * Initial checks
797
+ */
798
+ if (items.length === 0) {
799
+ logWarn(
800
+ "Init is true but no items are available; carousel will not be initialized"
801
+ );
802
+ }
803
+ if (
804
+ slideType === "fixed" &&
805
+ scrollAmountType === "group" &&
806
+ itemsPerSlide === 1
807
+ ) {
808
+ logWarn(
809
+ `Using scrollAmountType='group' and itemsPerSlide={1} makes no difference; itemsPerSlide must be greater than 1.`
810
+ );
811
+ }
812
+ if (
813
+ slideType === "fixed" &&
814
+ scrollAmountType === "group" &&
815
+ _items.length % itemsPerSlide !== 0
816
+ ) {
817
+ const errorMessage = `When using scrollAmountType='group' and itemsPerSlide={number>1} make sure that itemsPerSlides is divisible by the total quantity of items otherwise the carousel won't initialize.`;
818
+ errorMessages.current.push(errorMessage);
819
+ }
820
+ if (slideType === "fluid" && _scrollAmountType !== undefined) {
821
+ errorMessages.current.push(
822
+ `scrollAmountType="group" is not available for slideType="fluid"; please change one of them.`
823
+ );
824
+ }
825
+ if (slideType === "fluid" && _itemsPerSlide !== undefined) {
826
+ errorMessages.current.push(
827
+ `itemsPerSlide=${_itemsPerSlide} is not available for slideType="fluid"; please change one of them.`
828
+ );
829
+ }
830
+
831
+ if (errorMessages.current.length > 0) {
832
+ handleAppNotInitialized();
833
+ return;
834
+ }
835
+
836
+ /**
837
+ * Real carousel initialization
838
+ */
839
+ handleSetScrollAmount();
840
+
841
+ if (withLoop) {
842
+ /**
843
+ * For loop option we set the initial
844
+ * position of the carousel in the middle (having repeated items before and after)
845
+ */
846
+ handleSetBasicCarouselPosition();
847
+ }
848
+
849
+ /**
850
+ * Set drag treshold based on scroll amount
851
+ */
852
+ dragTreshold.current = getScrollAmount() / 4;
853
+
854
+ /**
855
+ * Initialize carousel
856
+ */
857
+ carouselIsInitialized.current = true;
858
+ }
859
+
860
+ if (init) {
861
+ initCarousel();
862
+ window.addEventListener("resize", handleResize);
863
+ document.addEventListener("visibilitychange", handleVisibilityChange);
864
+ return () => {
865
+ document.removeEventListener(
866
+ "visibilitychange",
867
+ handleVisibilityChange
868
+ );
869
+ window.removeEventListener("resize", handleResize);
870
+ };
871
+ } else {
872
+ carouselIsInitialized.current = false;
873
+ }
874
+ }, [scrollAmount, init, slideType, withLoop, carouselAxis]);
875
+
876
+ const carouselFragment = (
877
+ <>
878
+ <style
879
+ id={`carousel-container-${carouselId}`}
880
+ dangerouslySetInnerHTML={{
881
+ __html: `
882
+ :root {
883
+ --${carouselId}-react-spring-carousel-item-gutter: ${gutter}px;
884
+ --${carouselId}-react-spring-carousel-start-end-gutter: ${startEndGutter}px;
885
+ }
886
+ .carousel-${carouselId} {
887
+ display: flex;
888
+ width: 100%;
889
+ height: 100%;
890
+ overflow: hidden;
891
+ }
892
+ .carousel-${carouselId} .use-spring-carousel-track {
893
+ position: relative;
894
+ display: flex;
895
+ width: calc(100% - var(--${carouselId}-react-spring-carousel-start-end-gutter) * 2);
896
+ padding-left: var(--${carouselId}-react-spring-carousel-start-end-gutter);
897
+ touch-action: ${!enableGestures ? "auto" : carouselAxis === "x" ? "pan-y" : "pan-x"};
898
+ flex-direction: ${carouselAxis === "x" ? "row" : "column"};
899
+ overflow-x: ${slideType === "freeScroll" && carouselAxis === "x" ? "auto" : "initial"};
900
+ overflow-y: ${slideType === "freeScroll" && carouselAxis === "y" ? "auto" : "initial"};
901
+ }
902
+ .carousel-${carouselId} .use-spring-carousel-track::after {
903
+ content: "";
904
+ visibility: hidden;
905
+ display: block;
906
+ width: var(--${carouselId}-react-spring-carousel-start-end-gutter);
907
+ flex-shrink: 0;
908
+ }
909
+ .carousel-${carouselId} .use-spring-carousel-item {
910
+ position: relative;
911
+ display: flex;
912
+ flex: 1 0 ${slideType === "fixed" ? getCarouselItemDimension() : "auto"};
913
+ }
914
+ .carousel-${carouselId}[data-carousel-direction=x] .use-spring-carousel-item:not(:last-child) {
915
+ margin-right: var(--${carouselId}-react-spring-carousel-item-gutter);
916
+ }
917
+ .carousel-${carouselId}[data-carousel-direction=y] .use-spring-carousel-item:not(:last-child) {
918
+ margin-bottom: var(--${carouselId}-react-spring-carousel-item-gutter);
919
+ }
920
+ ${
921
+ fadeIn
922
+ ? `.carousel-${carouselId} .use-spring-carousel-item {
923
+ position: absolute;
924
+ top: 0;
925
+ left: 0;
926
+ width: 100%;
927
+ height: 100%;
928
+ opacity: 0;
929
+ }
930
+ .carousel-${carouselId} .use-spring-carousel-item:first-child {
931
+ opacity: 1;
932
+ }`.trim()
933
+ : ""
934
+ };
935
+ `.trim(),
936
+ }}
937
+ />
938
+ <div
939
+ className={`use-spring-carousel-container carousel-${carouselId}`}
940
+ ref={carouselContainerRef}
941
+ data-carousel-direction={carouselAxis}
942
+ >
943
+ <div
944
+ className={`use-spring-carousel-track`}
945
+ {...bindDrag()}
946
+ ref={carouselTrackRef}
947
+ {...getScrollHandlers()}
948
+ >
949
+ {items.map((item, index) => {
950
+ return (
951
+ <div
952
+ key={`${item.id}-${index}`}
953
+ className="use-spring-carousel-item"
954
+ id={item.id}
955
+ >
956
+ {item.renderItem}
957
+ </div>
958
+ );
959
+ })}
960
+ </div>
961
+ </div>
962
+ </>
963
+ );
964
+
965
+ return {
966
+ carouselFragment,
967
+ useListenToCustomEvent,
968
+ slideToNextItem: () => slideToNextItem("click"),
969
+ slideToPrevItem: () => slideToPrevItem("click"),
970
+ slideToIem: (id: string | number) => {
971
+ if (!carouselIsInitialized.current) {
972
+ handleAppNotInitialized();
973
+ return;
974
+ }
975
+ handleSlideToItem(id);
976
+ },
977
+ handleThumbsContainerScroll,
978
+ carouselId,
979
+ };
980
+ }