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.
- package/README.md +2 -0
- package/lib/commonjs/components/ItemLayout.js +1 -1
- package/lib/commonjs/components/ItemLayout.js.map +1 -1
- package/lib/commonjs/components/Pagination/Basic/PaginationItem.js +1 -1
- package/lib/commonjs/components/Pagination/Basic/PaginationItem.js.map +1 -1
- package/lib/commonjs/components/Pagination/Basic/index.js +1 -1
- package/lib/commonjs/components/Pagination/Basic/index.js.map +1 -1
- package/lib/commonjs/components/Pagination/Custom/PaginationItem.js +1 -1
- package/lib/commonjs/components/Pagination/Custom/PaginationItem.js.map +1 -1
- package/lib/commonjs/components/Pagination/Custom/index.js +1 -1
- package/lib/commonjs/components/Pagination/Custom/index.js.map +1 -1
- package/lib/commonjs/components/ScrollViewGesture.js +1 -1
- package/lib/commonjs/components/ScrollViewGesture.js.map +1 -1
- package/lib/commonjs/hooks/useCarouselController.js +1 -1
- package/lib/commonjs/hooks/useCarouselController.js.map +1 -1
- package/lib/commonjs/hooks/useVisibleRanges.js +1 -1
- package/lib/commonjs/hooks/useVisibleRanges.js.map +1 -1
- package/lib/commonjs/utils/compute-gesture-translation.js +2 -0
- package/lib/commonjs/utils/compute-gesture-translation.js.map +1 -0
- package/lib/commonjs/utils/sanitize-animation-style.js +2 -0
- package/lib/commonjs/utils/sanitize-animation-style.js.map +1 -0
- package/lib/module/components/ItemLayout.js +1 -1
- package/lib/module/components/ItemLayout.js.map +1 -1
- package/lib/module/components/Pagination/Basic/PaginationItem.js +1 -1
- package/lib/module/components/Pagination/Basic/PaginationItem.js.map +1 -1
- package/lib/module/components/Pagination/Basic/index.js +1 -1
- package/lib/module/components/Pagination/Basic/index.js.map +1 -1
- package/lib/module/components/Pagination/Custom/PaginationItem.js +1 -1
- package/lib/module/components/Pagination/Custom/PaginationItem.js.map +1 -1
- package/lib/module/components/Pagination/Custom/index.js +1 -1
- package/lib/module/components/Pagination/Custom/index.js.map +1 -1
- package/lib/module/components/ScrollViewGesture.js +1 -1
- package/lib/module/components/ScrollViewGesture.js.map +1 -1
- package/lib/module/hooks/useCarouselController.js +1 -1
- package/lib/module/hooks/useCarouselController.js.map +1 -1
- package/lib/module/hooks/useVisibleRanges.js +1 -1
- package/lib/module/hooks/useVisibleRanges.js.map +1 -1
- package/lib/module/utils/compute-gesture-translation.js +2 -0
- package/lib/module/utils/compute-gesture-translation.js.map +1 -0
- package/lib/module/utils/sanitize-animation-style.js +2 -0
- package/lib/module/utils/sanitize-animation-style.js.map +1 -0
- package/lib/typescript/components/ItemLayout.d.ts.map +1 -1
- package/lib/typescript/components/Pagination/Basic/PaginationItem.d.ts +10 -1
- package/lib/typescript/components/Pagination/Basic/PaginationItem.d.ts.map +1 -1
- package/lib/typescript/components/Pagination/Basic/index.d.ts +6 -1
- package/lib/typescript/components/Pagination/Basic/index.d.ts.map +1 -1
- package/lib/typescript/components/Pagination/Custom/PaginationItem.d.ts +10 -0
- package/lib/typescript/components/Pagination/Custom/PaginationItem.d.ts.map +1 -1
- package/lib/typescript/components/Pagination/Custom/index.d.ts +6 -1
- package/lib/typescript/components/Pagination/Custom/index.d.ts.map +1 -1
- package/lib/typescript/components/ScrollViewGesture.d.ts.map +1 -1
- package/lib/typescript/hooks/useCarouselController.d.ts.map +1 -1
- package/lib/typescript/hooks/useVisibleRanges.d.ts.map +1 -1
- package/lib/typescript/utils/compute-gesture-translation.d.ts +9 -0
- package/lib/typescript/utils/compute-gesture-translation.d.ts.map +1 -0
- package/lib/typescript/utils/sanitize-animation-style.d.ts +3 -0
- package/lib/typescript/utils/sanitize-animation-style.d.ts.map +1 -0
- package/package.json +5 -2
- package/src/components/Carousel.test.tsx +86 -0
- package/src/components/ItemLayout.tsx +2 -1
- package/src/components/Pagination/Basic/PaginationItem.tsx +23 -5
- package/src/components/Pagination/Basic/index.tsx +21 -2
- package/src/components/Pagination/Custom/PaginationItem.tsx +23 -4
- package/src/components/Pagination/Custom/index.tsx +21 -2
- package/src/components/Pagination/Pagination.test.tsx +73 -4
- package/src/components/ScrollViewGesture.tsx +11 -16
- package/src/hooks/useCarouselController.test.tsx +93 -6
- package/src/hooks/useCarouselController.tsx +23 -25
- package/src/hooks/useVisibleRanges.test.tsx +39 -0
- package/src/hooks/useVisibleRanges.tsx +14 -3
- package/src/utils/compute-gesture-translation.test.ts +70 -0
- package/src/utils/compute-gesture-translation.ts +29 -0
- package/src/utils/sanitize-animation-style.test.ts +40 -0
- 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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
+
}
|