react-native-reanimated-carousel 5.0.0-beta.3 → 5.0.0-beta.5

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 (74) hide show
  1. package/README.md +2 -0
  2. package/lib/commonjs/components/ItemLayout.js +1 -1
  3. package/lib/commonjs/components/ItemLayout.js.map +1 -1
  4. package/lib/commonjs/components/Pagination/Basic/PaginationItem.js +1 -1
  5. package/lib/commonjs/components/Pagination/Basic/PaginationItem.js.map +1 -1
  6. package/lib/commonjs/components/Pagination/Basic/index.js +1 -1
  7. package/lib/commonjs/components/Pagination/Basic/index.js.map +1 -1
  8. package/lib/commonjs/components/Pagination/Custom/PaginationItem.js +1 -1
  9. package/lib/commonjs/components/Pagination/Custom/PaginationItem.js.map +1 -1
  10. package/lib/commonjs/components/Pagination/Custom/index.js +1 -1
  11. package/lib/commonjs/components/Pagination/Custom/index.js.map +1 -1
  12. package/lib/commonjs/components/ScrollViewGesture.js +1 -1
  13. package/lib/commonjs/components/ScrollViewGesture.js.map +1 -1
  14. package/lib/commonjs/hooks/useCarouselController.js +1 -1
  15. package/lib/commonjs/hooks/useCarouselController.js.map +1 -1
  16. package/lib/commonjs/hooks/useVisibleRanges.js +1 -1
  17. package/lib/commonjs/hooks/useVisibleRanges.js.map +1 -1
  18. package/lib/commonjs/utils/compute-gesture-translation.js +2 -0
  19. package/lib/commonjs/utils/compute-gesture-translation.js.map +1 -0
  20. package/lib/commonjs/utils/sanitize-animation-style.js +2 -0
  21. package/lib/commonjs/utils/sanitize-animation-style.js.map +1 -0
  22. package/lib/module/components/ItemLayout.js +1 -1
  23. package/lib/module/components/ItemLayout.js.map +1 -1
  24. package/lib/module/components/Pagination/Basic/PaginationItem.js +1 -1
  25. package/lib/module/components/Pagination/Basic/PaginationItem.js.map +1 -1
  26. package/lib/module/components/Pagination/Basic/index.js +1 -1
  27. package/lib/module/components/Pagination/Basic/index.js.map +1 -1
  28. package/lib/module/components/Pagination/Custom/PaginationItem.js +1 -1
  29. package/lib/module/components/Pagination/Custom/PaginationItem.js.map +1 -1
  30. package/lib/module/components/Pagination/Custom/index.js +1 -1
  31. package/lib/module/components/Pagination/Custom/index.js.map +1 -1
  32. package/lib/module/components/ScrollViewGesture.js +1 -1
  33. package/lib/module/components/ScrollViewGesture.js.map +1 -1
  34. package/lib/module/hooks/useCarouselController.js +1 -1
  35. package/lib/module/hooks/useCarouselController.js.map +1 -1
  36. package/lib/module/hooks/useVisibleRanges.js +1 -1
  37. package/lib/module/hooks/useVisibleRanges.js.map +1 -1
  38. package/lib/module/utils/compute-gesture-translation.js +2 -0
  39. package/lib/module/utils/compute-gesture-translation.js.map +1 -0
  40. package/lib/module/utils/sanitize-animation-style.js +2 -0
  41. package/lib/module/utils/sanitize-animation-style.js.map +1 -0
  42. package/lib/typescript/components/ItemLayout.d.ts.map +1 -1
  43. package/lib/typescript/components/Pagination/Basic/PaginationItem.d.ts +10 -1
  44. package/lib/typescript/components/Pagination/Basic/PaginationItem.d.ts.map +1 -1
  45. package/lib/typescript/components/Pagination/Basic/index.d.ts +6 -1
  46. package/lib/typescript/components/Pagination/Basic/index.d.ts.map +1 -1
  47. package/lib/typescript/components/Pagination/Custom/PaginationItem.d.ts +10 -0
  48. package/lib/typescript/components/Pagination/Custom/PaginationItem.d.ts.map +1 -1
  49. package/lib/typescript/components/Pagination/Custom/index.d.ts +6 -1
  50. package/lib/typescript/components/Pagination/Custom/index.d.ts.map +1 -1
  51. package/lib/typescript/components/ScrollViewGesture.d.ts.map +1 -1
  52. package/lib/typescript/hooks/useCarouselController.d.ts.map +1 -1
  53. package/lib/typescript/hooks/useVisibleRanges.d.ts.map +1 -1
  54. package/lib/typescript/utils/compute-gesture-translation.d.ts +9 -0
  55. package/lib/typescript/utils/compute-gesture-translation.d.ts.map +1 -0
  56. package/lib/typescript/utils/sanitize-animation-style.d.ts +3 -0
  57. package/lib/typescript/utils/sanitize-animation-style.d.ts.map +1 -0
  58. package/package.json +5 -2
  59. package/src/components/Carousel.test.tsx +86 -0
  60. package/src/components/ItemLayout.tsx +2 -1
  61. package/src/components/Pagination/Basic/PaginationItem.tsx +23 -5
  62. package/src/components/Pagination/Basic/index.tsx +21 -2
  63. package/src/components/Pagination/Custom/PaginationItem.tsx +23 -4
  64. package/src/components/Pagination/Custom/index.tsx +21 -2
  65. package/src/components/Pagination/Pagination.test.tsx +73 -4
  66. package/src/components/ScrollViewGesture.tsx +11 -16
  67. package/src/hooks/useCarouselController.test.tsx +93 -6
  68. package/src/hooks/useCarouselController.tsx +23 -25
  69. package/src/hooks/useVisibleRanges.test.tsx +39 -0
  70. package/src/hooks/useVisibleRanges.tsx +14 -3
  71. package/src/utils/compute-gesture-translation.test.ts +70 -0
  72. package/src/utils/compute-gesture-translation.ts +29 -0
  73. package/src/utils/sanitize-animation-style.test.ts +40 -0
  74. package/src/utils/sanitize-animation-style.ts +20 -0
@@ -352,34 +352,32 @@ export function useCarouselController(options: IOpts): ICarouselController {
352
352
  if (!canSliding()) return;
353
353
 
354
354
  onScrollStart?.();
355
- // direction -> 1 | -1
356
- let direction: -1 | 1;
357
- if (fixedDirection === "positive") direction = 1;
358
- else if (fixedDirection === "negative") direction = -1;
359
- else if (!loop) {
360
- const currentPage = currentFixedPage();
361
- direction = i >= currentPage ? -1 : 1;
362
- } else {
363
- direction = handlerOffsetDirection(handlerOffset);
364
- }
365
-
366
- // target offset
367
- const offset = i * size * direction;
368
- // page width size * page count
369
- const totalSize = dataInfo.length * size;
370
-
371
- let isCloseToNextLoop = false;
355
+ let finalOffset: number;
372
356
 
373
- if (loop) {
374
- isCloseToNextLoop = Math.abs(handlerOffset.value % totalSize) / totalSize >= 0.5;
357
+ if (!loop) {
358
+ // Non-loop mode always uses direct page-to-offset mapping.
359
+ // This avoids sign flips when moving to lower indices.
360
+ finalOffset = -i * size;
361
+ } else {
362
+ // direction -> 1 | -1
363
+ let direction: -1 | 1;
364
+ if (fixedDirection === "positive") direction = 1;
365
+ else if (fixedDirection === "negative") direction = -1;
366
+ else direction = handlerOffsetDirection(handlerOffset);
367
+
368
+ // target offset
369
+ const offset = i * size * direction;
370
+ // page width size * page count
371
+ const totalSize = dataInfo.length * size;
372
+ const isCloseToNextLoop = Math.abs(handlerOffset.value % totalSize) / totalSize >= 0.5;
373
+
374
+ finalOffset =
375
+ (Math.floor(Math.abs(handlerOffset.value / totalSize)) + (isCloseToNextLoop ? 1 : 0)) *
376
+ totalSize *
377
+ direction +
378
+ offset;
375
379
  }
376
380
 
377
- const finalOffset =
378
- (Math.floor(Math.abs(handlerOffset.value / totalSize)) + (isCloseToNextLoop ? 1 : 0)) *
379
- totalSize *
380
- direction +
381
- offset;
382
-
383
381
  if (animated) {
384
382
  index.value = i;
385
383
  handlerOffset.value = scrollWithTiming(finalOffset, onFinished);
@@ -198,7 +198,46 @@ describe("useVisibleRanges", () => {
198
198
  });
199
199
  });
200
200
 
201
+ it("should return empty positive range when total is 0", () => {
202
+ const hook = renderHook(() => {
203
+ const translation = useSharedValue(0);
204
+ const range = useVisibleRanges({
205
+ total: 0,
206
+ translation,
207
+ viewSize,
208
+ windowSize: 4,
209
+ loop: false,
210
+ });
211
+ return range;
212
+ });
213
+
214
+ expect(hook.result.current.value).toEqual({
215
+ negativeRange: [0, 0],
216
+ positiveRange: [0, -1],
217
+ });
218
+ });
219
+
201
220
  describe("viewSize boundary conditions", () => {
221
+ it("should return safe defaults when viewSize is NaN", () => {
222
+ const hook = renderHook(() => {
223
+ const translation = useSharedValue(0);
224
+ const range = useVisibleRanges({
225
+ total: 6,
226
+ translation,
227
+ // Invalid runtime value should not break range calculation
228
+ viewSize: Number.NaN,
229
+ windowSize: 4,
230
+ loop: false,
231
+ });
232
+ return range;
233
+ });
234
+
235
+ expect(hook.result.current.value).toEqual({
236
+ negativeRange: [0, 0],
237
+ positiveRange: [0, 3],
238
+ });
239
+ });
240
+
202
241
  it("should return safe defaults when viewSize is 0", () => {
203
242
  const hook = renderHook(() => {
204
243
  const translation = useSharedValue(0);
@@ -20,12 +20,22 @@ export function useVisibleRanges(options: {
20
20
  }): IVisibleRanges {
21
21
  const { total = 0, viewSize, translation, windowSize: _windowSize, loop } = options;
22
22
 
23
- const windowSize = _windowSize ?? total;
23
+ const windowSize =
24
+ typeof _windowSize === "number" && Number.isFinite(_windowSize) && _windowSize > 0
25
+ ? _windowSize
26
+ : total;
24
27
  const cachedRanges = useRef<VisibleRanges | null>(null);
25
28
 
26
29
  const ranges = useDerivedValue(() => {
30
+ if (total <= 0) {
31
+ return {
32
+ negativeRange: [0, 0] as Range,
33
+ positiveRange: [0, -1] as Range,
34
+ };
35
+ }
36
+
27
37
  // Prevent division by zero when viewSize is not yet measured
28
- if (viewSize <= 0) {
38
+ if (!Number.isFinite(viewSize) || viewSize <= 0) {
29
39
  return {
30
40
  negativeRange: [0, 0] as Range,
31
41
  positiveRange: [0, Math.min(total - 1, windowSize - 1)] as Range,
@@ -36,6 +46,7 @@ export function useVisibleRanges(options: {
36
46
  const negativeCount = windowSize - positiveCount;
37
47
 
38
48
  let currentIndex = Math.round(-translation.value / viewSize);
49
+ if (!Number.isFinite(currentIndex)) currentIndex = 0;
39
50
 
40
51
  let newRanges: VisibleRanges;
41
52
 
@@ -86,7 +97,7 @@ export function useVisibleRanges(options: {
86
97
 
87
98
  cachedRanges.current = newRanges;
88
99
  return newRanges;
89
- }, [loop, total, windowSize, translation]);
100
+ }, [loop, total, windowSize, translation, viewSize]);
90
101
 
91
102
  return ranges;
92
103
  }
@@ -0,0 +1,70 @@
1
+ import { computeGestureTranslation } from "./compute-gesture-translation";
2
+
3
+ describe("computeGestureTranslation", () => {
4
+ it("should clamp right overdrag when overscrollEnabled is false", () => {
5
+ const next = computeGestureTranslation({
6
+ loop: false,
7
+ overscrollEnabled: false,
8
+ currentTranslation: 0,
9
+ panOffset: 0,
10
+ panTranslation: 120,
11
+ max: 500,
12
+ });
13
+
14
+ expect(next).toBe(0);
15
+ });
16
+
17
+ it("should keep damping behavior when overscrollEnabled is true and dragging past start", () => {
18
+ const next = computeGestureTranslation({
19
+ loop: false,
20
+ overscrollEnabled: true,
21
+ currentTranslation: 20,
22
+ panOffset: 0,
23
+ panTranslation: 120,
24
+ max: 500,
25
+ });
26
+
27
+ expect(next).toBe(60);
28
+ });
29
+
30
+ it("should apply damping when overscrollEnabled is true and dragging past end", () => {
31
+ const next = computeGestureTranslation({
32
+ loop: false,
33
+ overscrollEnabled: true,
34
+ currentTranslation: -600,
35
+ panOffset: -500,
36
+ panTranslation: -100,
37
+ max: 500,
38
+ });
39
+
40
+ // boundary = -max = -500, fixed = -500 - (-500) = 0, dynamic = -100 - 0 = -100
41
+ // result = -500 + (-100 * 0.5) = -550
42
+ expect(next).toBe(-550);
43
+ });
44
+
45
+ it("should pass through raw value in loop mode", () => {
46
+ const next = computeGestureTranslation({
47
+ loop: true,
48
+ overscrollEnabled: false,
49
+ currentTranslation: 0,
50
+ panOffset: 0,
51
+ panTranslation: 300,
52
+ max: 500,
53
+ });
54
+
55
+ expect(next).toBe(300);
56
+ });
57
+
58
+ it("should return raw value when within bounds and not looping", () => {
59
+ const next = computeGestureTranslation({
60
+ loop: false,
61
+ overscrollEnabled: true,
62
+ currentTranslation: -200,
63
+ panOffset: -100,
64
+ panTranslation: -50,
65
+ max: 500,
66
+ });
67
+
68
+ expect(next).toBe(-150);
69
+ });
70
+ });
@@ -0,0 +1,29 @@
1
+ export function computeGestureTranslation(options: {
2
+ loop: boolean;
3
+ overscrollEnabled: boolean;
4
+ currentTranslation: number;
5
+ panOffset: number;
6
+ panTranslation: number;
7
+ max: number;
8
+ }): number {
9
+ "worklet";
10
+ const { loop, overscrollEnabled, currentTranslation, panOffset, panTranslation, max } = options;
11
+ const raw = panOffset + panTranslation;
12
+
13
+ if (loop) return raw;
14
+
15
+ if (!overscrollEnabled) {
16
+ // In non-loop mode with overscroll disabled, translation must stay in [-max, 0].
17
+ return Math.min(0, Math.max(-max, raw));
18
+ }
19
+
20
+ // Keep resistance behavior only when overscroll is enabled.
21
+ if (currentTranslation > 0 || currentTranslation < -max) {
22
+ const boundary = currentTranslation > 0 ? 0 : -max;
23
+ const fixed = boundary - panOffset;
24
+ const dynamic = panTranslation - fixed;
25
+ return boundary + dynamic * 0.5;
26
+ }
27
+
28
+ return raw;
29
+ }
@@ -0,0 +1,40 @@
1
+ import { sanitizeAnimationStyle } from "./sanitize-animation-style";
2
+
3
+ describe("sanitizeAnimationStyle", () => {
4
+ it("rounds zIndex to an integer", () => {
5
+ const style = sanitizeAnimationStyle({
6
+ transform: [{ translateX: 12 }],
7
+ zIndex: 3.7,
8
+ });
9
+
10
+ expect(style.zIndex).toBe(4);
11
+ });
12
+
13
+ it("drops invalid zIndex values", () => {
14
+ const style = sanitizeAnimationStyle({
15
+ // invalid value from userland worklet should not flow to native styles
16
+ zIndex: Number.NaN,
17
+ });
18
+
19
+ expect(style).not.toHaveProperty("zIndex");
20
+ });
21
+
22
+ it("returns empty object for null/undefined input", () => {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ expect(sanitizeAnimationStyle(null as any)).toEqual({});
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ expect(sanitizeAnimationStyle(undefined as any)).toEqual({});
27
+ });
28
+
29
+ it("strips non-number zIndex (e.g. string)", () => {
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ const style = sanitizeAnimationStyle({ opacity: 1, zIndex: "auto" as any });
32
+ expect(style).not.toHaveProperty("zIndex");
33
+ expect(style).toHaveProperty("opacity", 1);
34
+ });
35
+
36
+ it("preserves style without zIndex unchanged", () => {
37
+ const style = sanitizeAnimationStyle({ opacity: 0.5 });
38
+ expect(style).toEqual({ opacity: 0.5 });
39
+ });
40
+ });
@@ -0,0 +1,20 @@
1
+ import type { ViewStyle } from "react-native";
2
+
3
+ type MutableViewStyle = ViewStyle & { zIndex?: unknown };
4
+
5
+ export function sanitizeAnimationStyle(style: ViewStyle): ViewStyle {
6
+ "worklet";
7
+
8
+ if (!style || typeof style !== "object") return {};
9
+
10
+ const nextStyle: MutableViewStyle = { ...style };
11
+ const { zIndex, ...restStyle } = nextStyle;
12
+
13
+ if (typeof zIndex === "number") {
14
+ // Native layers are sensitive to invalid/non-integer zIndex values.
15
+ if (Number.isFinite(zIndex)) return { ...restStyle, zIndex: Math.round(zIndex) };
16
+ return restStyle;
17
+ }
18
+
19
+ return typeof zIndex === "undefined" ? nextStyle : restStyle;
20
+ }