react-native-reanimated-carousel 5.0.0-beta.6 → 5.0.0-beta.7
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/lib/commonjs/components/CarouselLayout.js +1 -1
- package/lib/commonjs/components/CarouselLayout.js.map +1 -1
- package/lib/commonjs/components/ItemRenderer.js +1 -1
- package/lib/commonjs/components/ItemRenderer.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/useOnProgressChange.js +1 -1
- package/lib/commonjs/hooks/useOnProgressChange.js.map +1 -1
- package/lib/commonjs/hooks/usePropsErrorBoundary.js +1 -1
- package/lib/commonjs/hooks/usePropsErrorBoundary.js.map +1 -1
- package/lib/commonjs/hooks/useVisibleRanges.js +1 -1
- package/lib/commonjs/hooks/useVisibleRanges.js.map +1 -1
- package/lib/module/components/CarouselLayout.js +1 -1
- package/lib/module/components/CarouselLayout.js.map +1 -1
- package/lib/module/components/ItemRenderer.js +1 -1
- package/lib/module/components/ItemRenderer.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/useOnProgressChange.js +1 -1
- package/lib/module/hooks/useOnProgressChange.js.map +1 -1
- package/lib/module/hooks/usePropsErrorBoundary.js +1 -1
- package/lib/module/hooks/usePropsErrorBoundary.js.map +1 -1
- package/lib/module/hooks/useVisibleRanges.js +1 -1
- package/lib/module/hooks/useVisibleRanges.js.map +1 -1
- package/lib/typescript/components/CarouselLayout.d.ts.map +1 -1
- package/lib/typescript/components/ItemRenderer.d.ts +1 -0
- package/lib/typescript/components/ItemRenderer.d.ts.map +1 -1
- package/lib/typescript/components/ScrollViewGesture.d.ts.map +1 -1
- package/lib/typescript/hooks/usePropsErrorBoundary.d.ts.map +1 -1
- package/lib/typescript/hooks/useVisibleRanges.d.ts +6 -0
- package/lib/typescript/hooks/useVisibleRanges.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CarouselLayout.tsx +1 -0
- package/src/components/ItemRenderer.tsx +11 -6
- package/src/components/ScrollViewGesture.test.tsx +155 -0
- package/src/components/ScrollViewGesture.tsx +8 -11
- package/src/components/issue-899-parallax-reverse-loop.test.tsx +131 -0
- package/src/hooks/useOnProgressChange.test.tsx +71 -8
- package/src/hooks/useOnProgressChange.ts +1 -1
- package/src/hooks/usePropsErrorBoundary.test.tsx +57 -0
- package/src/hooks/usePropsErrorBoundary.ts +4 -3
- package/src/hooks/useVisibleRanges.tsx +80 -54
- package/src/types.ts +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import type { PanGesture } from "react-native-gesture-handler";
|
|
4
|
+
import { Gesture, State } from "react-native-gesture-handler";
|
|
5
|
+
import Animated, { useDerivedValue, useSharedValue } from "react-native-reanimated";
|
|
6
|
+
|
|
7
|
+
import { act, render, waitFor } from "@testing-library/react-native";
|
|
8
|
+
import { fireGestureHandler, getByGestureTestId } from "react-native-gesture-handler/jest-utils";
|
|
9
|
+
|
|
10
|
+
import Carousel from "./Carousel";
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
const cfg = (global as any).__reanimatedLoggerConfig as
|
|
14
|
+
| { logFunction: (data: { level: number; message: string }) => void }
|
|
15
|
+
| undefined;
|
|
16
|
+
if (cfg) {
|
|
17
|
+
const originalLog = cfg.logFunction;
|
|
18
|
+
cfg.logFunction = (data) => {
|
|
19
|
+
if (data.message.includes("measure() cannot be used with Jest")) return;
|
|
20
|
+
originalLog(data);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const slideWidth = 300;
|
|
26
|
+
const slideHeight = 200;
|
|
27
|
+
const gestureTestId = "rnrc-gesture-handler";
|
|
28
|
+
const realPan = Gesture.Pan();
|
|
29
|
+
|
|
30
|
+
jest.spyOn(Gesture, "Pan").mockImplementation(() => realPan.withTestId(gestureTestId));
|
|
31
|
+
|
|
32
|
+
describe("issue #899 parallax reverse loop regression", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.useFakeTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
act(() => {
|
|
39
|
+
jest.runOnlyPendingTimers();
|
|
40
|
+
});
|
|
41
|
+
jest.useRealTimers();
|
|
42
|
+
jest.clearAllTimers();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("preserves renderItem styles and props when looping backward from index 0 in parallax mode", async () => {
|
|
46
|
+
const progress = { current: 0 };
|
|
47
|
+
const data = Array.from({ length: 30 }, (_, index) => ({
|
|
48
|
+
id: `item-${index}`,
|
|
49
|
+
color: index === 29 ? "#111111" : `rgb(${index}, ${index}, ${index})`,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const Wrapper = () => {
|
|
53
|
+
const progressAnimVal = useSharedValue(progress.current);
|
|
54
|
+
|
|
55
|
+
useDerivedValue(() => {
|
|
56
|
+
progress.current = progressAnimVal.value;
|
|
57
|
+
}, [progressAnimVal]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Carousel
|
|
61
|
+
data={data}
|
|
62
|
+
defaultIndex={0}
|
|
63
|
+
loop
|
|
64
|
+
mode="parallax"
|
|
65
|
+
windowSize={5}
|
|
66
|
+
style={{ width: slideWidth, height: slideHeight }}
|
|
67
|
+
onProgressChange={(_, absoluteProgress) => {
|
|
68
|
+
progressAnimVal.value = absoluteProgress;
|
|
69
|
+
}}
|
|
70
|
+
renderItem={({ item, index }) => (
|
|
71
|
+
<Animated.View
|
|
72
|
+
testID={`issue-899-item-${index}`}
|
|
73
|
+
accessibilityLabel={`issue-899-label-${index}`}
|
|
74
|
+
style={{
|
|
75
|
+
width: slideWidth,
|
|
76
|
+
height: slideHeight,
|
|
77
|
+
backgroundColor: item.color,
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const { getByTestId, queryByTestId } = render(<Wrapper />);
|
|
86
|
+
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(getByTestId("issue-899-item-0")).toBeTruthy();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const prerenderedWrappedItem = getByTestId("issue-899-item-29");
|
|
92
|
+
const prerenderedWrappedStyle = StyleSheet.flatten(prerenderedWrappedItem.props.style);
|
|
93
|
+
|
|
94
|
+
expect(prerenderedWrappedItem.props.accessibilityLabel).toBe("issue-899-label-29");
|
|
95
|
+
expect(prerenderedWrappedStyle.backgroundColor).toBe("#111111");
|
|
96
|
+
|
|
97
|
+
fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
|
|
98
|
+
{ state: State.BEGAN, translationX: 0, velocityX: slideWidth },
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
|
|
102
|
+
{ state: State.ACTIVE, translationX: slideWidth * 0.7, velocityX: slideWidth },
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
expect(queryByTestId("issue-899-item-29")).toBeTruthy();
|
|
106
|
+
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(queryByTestId("issue-899-item-29")).toBeTruthy();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const activeWrappedItem = getByTestId("issue-899-item-29");
|
|
112
|
+
const activeWrappedStyle = StyleSheet.flatten(activeWrappedItem.props.style);
|
|
113
|
+
|
|
114
|
+
expect(activeWrappedItem.props.accessibilityLabel).toBe("issue-899-label-29");
|
|
115
|
+
expect(activeWrappedStyle.backgroundColor).toBe("#111111");
|
|
116
|
+
|
|
117
|
+
fireGestureHandler<PanGesture>(getByGestureTestId(gestureTestId), [
|
|
118
|
+
{ state: State.END, translationX: slideWidth, velocityX: slideWidth },
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(progress.current).toBe(29);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const wrappedItem = getByTestId("issue-899-item-29");
|
|
126
|
+
const flattenedStyle = StyleSheet.flatten(wrappedItem.props.style);
|
|
127
|
+
|
|
128
|
+
expect(wrappedItem.props.accessibilityLabel).toBe("issue-899-label-29");
|
|
129
|
+
expect(flattenedStyle.backgroundColor).toBe("#111111");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -6,19 +6,23 @@ import { useOnProgressChange } from "./useOnProgressChange";
|
|
|
6
6
|
|
|
7
7
|
// Mock Reanimated and Easing
|
|
8
8
|
jest.mock("react-native-reanimated", () => {
|
|
9
|
+
const React = jest.requireActual("react");
|
|
9
10
|
let reactionCallback: ((value: any) => void) | null = null;
|
|
11
|
+
let reactionRegistrationCount = 0;
|
|
10
12
|
|
|
11
13
|
return {
|
|
12
14
|
useSharedValue: jest.fn((initialValue) => ({
|
|
13
15
|
value: initialValue,
|
|
14
16
|
})),
|
|
15
|
-
useAnimatedReaction: jest.fn((
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
useAnimatedReaction: jest.fn((prepare, react, dependencies) => {
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
reactionRegistrationCount += 1;
|
|
20
|
+
reactionCallback = react;
|
|
21
|
+
react(prepare());
|
|
22
|
+
return () => {
|
|
23
|
+
if (reactionCallback === react) reactionCallback = null;
|
|
24
|
+
};
|
|
25
|
+
}, dependencies);
|
|
22
26
|
}),
|
|
23
27
|
Easing: {
|
|
24
28
|
bezier: () => ({
|
|
@@ -29,6 +33,10 @@ jest.mock("react-native-reanimated", () => {
|
|
|
29
33
|
__triggerReaction: (value: any) => {
|
|
30
34
|
if (reactionCallback) reactionCallback(value);
|
|
31
35
|
},
|
|
36
|
+
__getReactionRegistrationCount: () => reactionRegistrationCount,
|
|
37
|
+
__resetReactionRegistrationCount: () => {
|
|
38
|
+
reactionRegistrationCount = 0;
|
|
39
|
+
},
|
|
32
40
|
};
|
|
33
41
|
});
|
|
34
42
|
|
|
@@ -49,11 +57,14 @@ describe("useOnProgressChange", () => {
|
|
|
49
57
|
const mockOffsetX = useSharedValue(0);
|
|
50
58
|
const mockSizeReady = useSharedValue(true);
|
|
51
59
|
const mockOnProgressChange = jest.fn();
|
|
52
|
-
const { __triggerReaction } =
|
|
60
|
+
const { __getReactionRegistrationCount, __resetReactionRegistrationCount, __triggerReaction } =
|
|
61
|
+
jest.requireMock("react-native-reanimated");
|
|
53
62
|
|
|
54
63
|
beforeEach(() => {
|
|
55
64
|
jest.clearAllMocks();
|
|
65
|
+
__resetReactionRegistrationCount();
|
|
56
66
|
mockOffsetX.value = 0;
|
|
67
|
+
mockSizeReady.value = true;
|
|
57
68
|
});
|
|
58
69
|
|
|
59
70
|
it("should handle progress change with function callback", () => {
|
|
@@ -204,4 +215,56 @@ describe("useOnProgressChange", () => {
|
|
|
204
215
|
__triggerReaction({ offset: mockOffsetX.value, ready: mockSizeNotReady.value });
|
|
205
216
|
expect(mockOnProgressChange).not.toHaveBeenCalled();
|
|
206
217
|
});
|
|
218
|
+
|
|
219
|
+
it("should re-register the reaction when the offset shared value changes", () => {
|
|
220
|
+
const replacementOffsetX = useSharedValue(-300);
|
|
221
|
+
const { rerender } = renderHook(
|
|
222
|
+
({ offsetX }) =>
|
|
223
|
+
useOnProgressChange({
|
|
224
|
+
size: 300,
|
|
225
|
+
sizeReady: mockSizeReady,
|
|
226
|
+
autoFillData: false,
|
|
227
|
+
loop: false,
|
|
228
|
+
offsetX,
|
|
229
|
+
rawDataLength: 5,
|
|
230
|
+
onProgressChange: mockOnProgressChange,
|
|
231
|
+
}),
|
|
232
|
+
{ initialProps: { offsetX: mockOffsetX } }
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const initialRegistrationCount = __getReactionRegistrationCount();
|
|
236
|
+
rerender({ offsetX: mockOffsetX });
|
|
237
|
+
expect(__getReactionRegistrationCount()).toBe(initialRegistrationCount);
|
|
238
|
+
|
|
239
|
+
rerender({ offsetX: replacementOffsetX });
|
|
240
|
+
|
|
241
|
+
expect(__getReactionRegistrationCount()).toBe(initialRegistrationCount + 1);
|
|
242
|
+
expect(mockOnProgressChange).toHaveBeenLastCalledWith(-300, 1);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should re-register the reaction when the size-ready shared value changes", () => {
|
|
246
|
+
const initialSizeReady = useSharedValue(false);
|
|
247
|
+
const replacementSizeReady = useSharedValue(true);
|
|
248
|
+
const { rerender } = renderHook(
|
|
249
|
+
({ sizeReady }) =>
|
|
250
|
+
useOnProgressChange({
|
|
251
|
+
size: 300,
|
|
252
|
+
sizeReady,
|
|
253
|
+
autoFillData: false,
|
|
254
|
+
loop: false,
|
|
255
|
+
offsetX: mockOffsetX,
|
|
256
|
+
rawDataLength: 5,
|
|
257
|
+
onProgressChange: mockOnProgressChange,
|
|
258
|
+
}),
|
|
259
|
+
{ initialProps: { sizeReady: initialSizeReady } }
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const initialRegistrationCount = __getReactionRegistrationCount();
|
|
263
|
+
expect(mockOnProgressChange).not.toHaveBeenCalled();
|
|
264
|
+
|
|
265
|
+
rerender({ sizeReady: replacementSizeReady });
|
|
266
|
+
|
|
267
|
+
expect(__getReactionRegistrationCount()).toBe(initialRegistrationCount + 1);
|
|
268
|
+
expect(mockOnProgressChange).toHaveBeenCalledWith(0, 0);
|
|
269
|
+
});
|
|
207
270
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-hooks";
|
|
2
|
+
|
|
3
|
+
import { usePropsErrorBoundary } from "./usePropsErrorBoundary";
|
|
4
|
+
|
|
5
|
+
import type { TCarouselProps } from "../types";
|
|
6
|
+
|
|
7
|
+
function createProps(overrides: Partial<TCarouselProps<number>> = {}): TCarouselProps<number> & {
|
|
8
|
+
dataLength: number;
|
|
9
|
+
} {
|
|
10
|
+
return {
|
|
11
|
+
data: [0, 1, 2],
|
|
12
|
+
dataLength: 3,
|
|
13
|
+
renderItem: () => null,
|
|
14
|
+
...overrides,
|
|
15
|
+
} as TCarouselProps<number> & { dataLength: number };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("usePropsErrorBoundary", () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("accepts percentage and flex container sizing without contradictory warnings", () => {
|
|
24
|
+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
25
|
+
|
|
26
|
+
renderHook(() =>
|
|
27
|
+
usePropsErrorBoundary(
|
|
28
|
+
createProps({
|
|
29
|
+
style: { width: "100%", height: 190 },
|
|
30
|
+
itemWidth: 280,
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
renderHook(() =>
|
|
35
|
+
usePropsErrorBoundary(
|
|
36
|
+
createProps({
|
|
37
|
+
vertical: true,
|
|
38
|
+
style: { flex: 1 },
|
|
39
|
+
itemHeight: 190,
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("Horizontal mode"));
|
|
45
|
+
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("`itemWidth` sets"));
|
|
46
|
+
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("Vertical mode"));
|
|
47
|
+
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("`itemHeight` sets"));
|
|
48
|
+
|
|
49
|
+
warnSpy.mockClear();
|
|
50
|
+
renderHook(() => usePropsErrorBoundary(createProps({ style: { flex: 0, height: 190 } })));
|
|
51
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Horizontal mode"));
|
|
52
|
+
|
|
53
|
+
warnSpy.mockClear();
|
|
54
|
+
renderHook(() => usePropsErrorBoundary(createProps({ vertical: true })));
|
|
55
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Vertical mode"));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -16,7 +16,7 @@ export function usePropsErrorBoundary(props: TCarouselProps & { dataLength: numb
|
|
|
16
16
|
if (__DEV__) {
|
|
17
17
|
const { style, vertical, width, height, itemWidth, itemHeight, contentContainerStyle } =
|
|
18
18
|
props;
|
|
19
|
-
const { width: styleWidth, height: styleHeight } = StyleSheet.flatten(style) || {};
|
|
19
|
+
const { width: styleWidth, height: styleHeight, flex } = StyleSheet.flatten(style) || {};
|
|
20
20
|
|
|
21
21
|
// Deprecation warnings for width/height props
|
|
22
22
|
if (typeof width === "number" && !warnedRefs.width) {
|
|
@@ -42,10 +42,11 @@ export function usePropsErrorBoundary(props: TCarouselProps & { dataLength: numb
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Updated missing size warnings
|
|
45
|
+
const hasFlexibleContainerSize = typeof flex === "number" && flex > 0;
|
|
45
46
|
const hasHorizontalContainerSize =
|
|
46
|
-
|
|
47
|
+
styleWidth != null || typeof width === "number" || hasFlexibleContainerSize;
|
|
47
48
|
const hasVerticalContainerSize =
|
|
48
|
-
|
|
49
|
+
styleHeight != null || typeof height === "number" || hasFlexibleContainerSize;
|
|
49
50
|
|
|
50
51
|
if (!vertical && !hasHorizontalContainerSize && !warnedRefs.horizontal) {
|
|
51
52
|
console.warn(
|
|
@@ -11,6 +11,83 @@ export interface VisibleRanges {
|
|
|
11
11
|
|
|
12
12
|
export type IVisibleRanges = SharedValue<VisibleRanges>;
|
|
13
13
|
|
|
14
|
+
function normalizeWindowSize(total: number, windowSize?: number) {
|
|
15
|
+
"worklet";
|
|
16
|
+
|
|
17
|
+
return typeof windowSize === "number" && Number.isFinite(windowSize) && windowSize > 0
|
|
18
|
+
? windowSize
|
|
19
|
+
: total;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeLoopIndex(currentIndex: number, total: number) {
|
|
23
|
+
"worklet";
|
|
24
|
+
|
|
25
|
+
return currentIndex < 0 ? (currentIndex % total) + total : currentIndex;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function computeVisibleRanges(params: {
|
|
29
|
+
total: number;
|
|
30
|
+
windowSize?: number;
|
|
31
|
+
currentIndex: number;
|
|
32
|
+
loop?: boolean;
|
|
33
|
+
}): VisibleRanges {
|
|
34
|
+
"worklet";
|
|
35
|
+
|
|
36
|
+
const { total = 0, loop } = params;
|
|
37
|
+
const windowSize = normalizeWindowSize(total, params.windowSize);
|
|
38
|
+
|
|
39
|
+
if (total <= 0) {
|
|
40
|
+
return {
|
|
41
|
+
negativeRange: [0, 0],
|
|
42
|
+
positiveRange: [0, -1],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (loop && windowSize >= total) {
|
|
47
|
+
return {
|
|
48
|
+
negativeRange: [0, 0],
|
|
49
|
+
positiveRange: [0, total - 1],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const positiveCount = Math.round(windowSize / 2);
|
|
54
|
+
const negativeCount = windowSize - positiveCount;
|
|
55
|
+
|
|
56
|
+
let currentIndex = params.currentIndex;
|
|
57
|
+
if (!Number.isFinite(currentIndex)) currentIndex = 0;
|
|
58
|
+
|
|
59
|
+
if (!loop) {
|
|
60
|
+
currentIndex = Math.max(0, Math.min(total - 1, currentIndex));
|
|
61
|
+
return {
|
|
62
|
+
negativeRange: [Math.max(0, currentIndex - (windowSize - 1)), currentIndex],
|
|
63
|
+
positiveRange: [currentIndex, Math.min(total - 1, currentIndex + (windowSize - 1))],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
currentIndex = normalizeLoopIndex(currentIndex, total);
|
|
68
|
+
|
|
69
|
+
const negativeRange: Range = [
|
|
70
|
+
(currentIndex - negativeCount + total) % total,
|
|
71
|
+
(currentIndex - 1 + total) % total,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const positiveRange: Range = [
|
|
75
|
+
(currentIndex + total) % total,
|
|
76
|
+
(currentIndex + positiveCount + total) % total,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
if (negativeRange[0] < total && negativeRange[0] > negativeRange[1]) {
|
|
80
|
+
negativeRange[1] = total - 1;
|
|
81
|
+
positiveRange[0] = 0;
|
|
82
|
+
}
|
|
83
|
+
if (positiveRange[0] > positiveRange[1]) {
|
|
84
|
+
negativeRange[1] = total - 1;
|
|
85
|
+
positiveRange[0] = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { negativeRange, positiveRange };
|
|
89
|
+
}
|
|
90
|
+
|
|
14
91
|
export function useVisibleRanges(options: {
|
|
15
92
|
total: number;
|
|
16
93
|
viewSize: number;
|
|
@@ -20,18 +97,12 @@ export function useVisibleRanges(options: {
|
|
|
20
97
|
}): IVisibleRanges {
|
|
21
98
|
const { total = 0, viewSize, translation, windowSize: _windowSize, loop } = options;
|
|
22
99
|
|
|
23
|
-
const windowSize =
|
|
24
|
-
typeof _windowSize === "number" && Number.isFinite(_windowSize) && _windowSize > 0
|
|
25
|
-
? _windowSize
|
|
26
|
-
: total;
|
|
100
|
+
const windowSize = normalizeWindowSize(total, _windowSize);
|
|
27
101
|
const cachedRanges = useRef<VisibleRanges | null>(null);
|
|
28
102
|
|
|
29
103
|
const ranges = useDerivedValue(() => {
|
|
30
104
|
if (total <= 0) {
|
|
31
|
-
return {
|
|
32
|
-
negativeRange: [0, 0] as Range,
|
|
33
|
-
positiveRange: [0, -1] as Range,
|
|
34
|
-
};
|
|
105
|
+
return computeVisibleRanges({ total, currentIndex: 0, windowSize, loop });
|
|
35
106
|
}
|
|
36
107
|
|
|
37
108
|
// Prevent division by zero when viewSize is not yet measured
|
|
@@ -42,55 +113,10 @@ export function useVisibleRanges(options: {
|
|
|
42
113
|
};
|
|
43
114
|
}
|
|
44
115
|
|
|
45
|
-
const positiveCount = Math.round(windowSize / 2);
|
|
46
|
-
const negativeCount = windowSize - positiveCount;
|
|
47
|
-
|
|
48
116
|
let currentIndex = Math.round(-translation.value / viewSize);
|
|
49
117
|
if (!Number.isFinite(currentIndex)) currentIndex = 0;
|
|
50
118
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (loop && windowSize >= total) {
|
|
54
|
-
newRanges = {
|
|
55
|
-
negativeRange: [0, 0],
|
|
56
|
-
positiveRange: [0, total - 1],
|
|
57
|
-
};
|
|
58
|
-
} else if (!loop) {
|
|
59
|
-
// Clamp currentIndex to valid range [0, total-1] for non-loop mode
|
|
60
|
-
// When overdragging right, translation.value becomes positive, making currentIndex negative
|
|
61
|
-
currentIndex = Math.max(0, Math.min(total - 1, currentIndex));
|
|
62
|
-
|
|
63
|
-
// Adjusting negative range if the carousel is not loopable.
|
|
64
|
-
// So, It will be only displayed the positive items.
|
|
65
|
-
newRanges = {
|
|
66
|
-
negativeRange: [Math.max(0, currentIndex - (windowSize - 1)), currentIndex],
|
|
67
|
-
positiveRange: [currentIndex, Math.min(total - 1, currentIndex + (windowSize - 1))],
|
|
68
|
-
};
|
|
69
|
-
} else {
|
|
70
|
-
currentIndex = currentIndex < 0 ? (currentIndex % total) + total : currentIndex;
|
|
71
|
-
|
|
72
|
-
const negativeRange: Range = [
|
|
73
|
-
(currentIndex - negativeCount + total) % total,
|
|
74
|
-
(currentIndex - 1 + total) % total,
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
const positiveRange: Range = [
|
|
78
|
-
(currentIndex + total) % total,
|
|
79
|
-
(currentIndex + positiveCount + total) % total,
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
if (negativeRange[0] < total && negativeRange[0] > negativeRange[1]) {
|
|
83
|
-
negativeRange[1] = total - 1;
|
|
84
|
-
positiveRange[0] = 0;
|
|
85
|
-
}
|
|
86
|
-
if (positiveRange[0] > positiveRange[1]) {
|
|
87
|
-
negativeRange[1] = total - 1;
|
|
88
|
-
positiveRange[0] = 0;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// console.log({ negativeRange, positiveRange ,total,windowSize,a:total <= _windowSize})
|
|
92
|
-
newRanges = { negativeRange, positiveRange };
|
|
93
|
-
}
|
|
119
|
+
const newRanges = computeVisibleRanges({ total, windowSize, currentIndex, loop });
|
|
94
120
|
|
|
95
121
|
if (
|
|
96
122
|
cachedRanges.current &&
|
package/src/types.ts
CHANGED
|
@@ -242,7 +242,7 @@ export type TCarouselProps<T = any> = {
|
|
|
242
242
|
customConfig?: CustomConfig | (() => CustomConfig);
|
|
243
243
|
/**
|
|
244
244
|
* Custom animations.
|
|
245
|
-
* Must
|
|
245
|
+
* Must be worklet-compatible. See https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/
|
|
246
246
|
* @test_coverage ✅ tested in Carousel.test.tsx > should apply the custom animation
|
|
247
247
|
*/
|
|
248
248
|
customAnimation?: (value: number, index: number) => ViewStyle;
|