react-native-reanimated-carousel 5.0.0-beta.5 → 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/README.md +28 -6
- package/lib/commonjs/components/CarouselLayout.js +1 -1
- package/lib/commonjs/components/CarouselLayout.js.map +1 -1
- package/lib/commonjs/components/ItemLayout.js +1 -1
- package/lib/commonjs/components/ItemLayout.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/ItemLayout.js +1 -1
- package/lib/module/components/ItemLayout.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 +11 -0
- package/lib/typescript/components/CarouselLayout.d.ts.map +1 -1
- package/lib/typescript/components/ItemLayout.d.ts +5 -0
- package/lib/typescript/components/ItemLayout.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 +13 -4
- package/src/components/Carousel.test.tsx +152 -0
- package/src/components/CarouselLayout.tsx +52 -8
- package/src/components/ItemLayout.tsx +31 -7
- 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.test.tsx +29 -0
- package/src/hooks/useVisibleRanges.tsx +80 -49
- package/src/types.ts +1 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type {
|
|
4
|
+
GestureStateChangeEvent,
|
|
5
|
+
GestureUpdateEvent,
|
|
6
|
+
PanGestureHandlerEventPayload,
|
|
7
|
+
} from "react-native-gesture-handler";
|
|
8
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
9
|
+
|
|
10
|
+
import { act, render } from "@testing-library/react-native";
|
|
11
|
+
|
|
12
|
+
import { ScrollViewGesture } from "./ScrollViewGesture";
|
|
13
|
+
|
|
14
|
+
import { usePanGestureProxy } from "../hooks/usePanGestureProxy";
|
|
15
|
+
import { GlobalStateContext, type IContext } from "../store";
|
|
16
|
+
|
|
17
|
+
jest.mock("../hooks/usePanGestureProxy", () => ({
|
|
18
|
+
usePanGestureProxy: jest.fn(() => ({})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const mockUsePanGestureProxy = jest.mocked(usePanGestureProxy);
|
|
22
|
+
|
|
23
|
+
jest.mock("react-native-gesture-handler", () => {
|
|
24
|
+
const React = require("react");
|
|
25
|
+
const actual = jest.requireActual("react-native-gesture-handler");
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
GestureDetector: ({ children }: { children: React.ReactNode }) => {
|
|
30
|
+
const child = React.Children.only(children);
|
|
31
|
+
|
|
32
|
+
// Simulate the upstream web path that reads `element.ref`.
|
|
33
|
+
// React 19 warns only when the child element actually carries a ref.
|
|
34
|
+
// @ts-expect-error React 19 removed `element.ref` from the element type.
|
|
35
|
+
void child.ref;
|
|
36
|
+
|
|
37
|
+
return child;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
type RenderGestureOptions = {
|
|
43
|
+
vertical?: boolean;
|
|
44
|
+
size?: number;
|
|
45
|
+
dataLength?: number;
|
|
46
|
+
containerSize?: { width: number; height: number };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function renderGesture({
|
|
50
|
+
vertical = false,
|
|
51
|
+
size = 300,
|
|
52
|
+
dataLength = 3,
|
|
53
|
+
containerSize = { width: 300, height: 200 },
|
|
54
|
+
}: RenderGestureOptions = {}) {
|
|
55
|
+
const translation = { value: 0 } as SharedValue<number>;
|
|
56
|
+
const contextValue = {
|
|
57
|
+
props: {
|
|
58
|
+
onConfigurePanGesture: undefined,
|
|
59
|
+
vertical,
|
|
60
|
+
pagingEnabled: true,
|
|
61
|
+
snapEnabled: true,
|
|
62
|
+
loop: false,
|
|
63
|
+
scrollAnimationDuration: 500,
|
|
64
|
+
withAnimation: undefined,
|
|
65
|
+
enabled: true,
|
|
66
|
+
dataLength,
|
|
67
|
+
overscrollEnabled: false,
|
|
68
|
+
maxScrollDistancePerSwipe: undefined,
|
|
69
|
+
minScrollDistancePerSwipe: undefined,
|
|
70
|
+
fixedDirection: undefined,
|
|
71
|
+
},
|
|
72
|
+
common: {
|
|
73
|
+
size,
|
|
74
|
+
validLength: dataLength,
|
|
75
|
+
handlerOffset: { value: 0 },
|
|
76
|
+
resolvedSize: { value: size },
|
|
77
|
+
sizePhase: { value: "ready" },
|
|
78
|
+
sizeExplicit: true,
|
|
79
|
+
},
|
|
80
|
+
layout: {
|
|
81
|
+
containerSize: { value: containerSize },
|
|
82
|
+
updateContainerSize: jest.fn(),
|
|
83
|
+
itemDimensions: { value: {} },
|
|
84
|
+
updateItemDimensions: jest.fn(),
|
|
85
|
+
},
|
|
86
|
+
} as unknown as IContext;
|
|
87
|
+
|
|
88
|
+
render(
|
|
89
|
+
<GlobalStateContext.Provider value={contextValue}>
|
|
90
|
+
<ScrollViewGesture size={size} translation={translation} style={containerSize}>
|
|
91
|
+
<View testID="content" />
|
|
92
|
+
</ScrollViewGesture>
|
|
93
|
+
</GlobalStateContext.Provider>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const gestureCallbacks =
|
|
97
|
+
mockUsePanGestureProxy.mock.calls[mockUsePanGestureProxy.mock.calls.length - 1]?.[0];
|
|
98
|
+
|
|
99
|
+
if (!gestureCallbacks) {
|
|
100
|
+
throw new Error("Expected ScrollViewGesture to configure the pan gesture");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { gestureCallbacks, translation };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("issue #857 web regression", () => {
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
mockUsePanGestureProxy.mockClear();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not emit the React 19 element.ref warning from the gesture path", () => {
|
|
112
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
renderGesture();
|
|
116
|
+
|
|
117
|
+
const react19RefWarnings = errorSpy.mock.calls.filter(([message]) =>
|
|
118
|
+
String(message).includes("Accessing element.ref was removed in React 19")
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(react19RefWarnings).toHaveLength(0);
|
|
122
|
+
} finally {
|
|
123
|
+
errorSpy.mockRestore();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it.each([
|
|
128
|
+
["horizontal", false, 300, 3, { width: 300, height: 200 }, -600],
|
|
129
|
+
["vertical", true, 300, 3, { width: 300, height: 200 }, -700],
|
|
130
|
+
["unmeasured container", false, 300, 3, { width: 0, height: 0 }, 0],
|
|
131
|
+
["content smaller than its container", false, 100, 2, { width: 300, height: 200 }, 0],
|
|
132
|
+
] as const)(
|
|
133
|
+
"uses the %s main-axis container size for the non-loop boundary",
|
|
134
|
+
(_name, vertical, size, dataLength, containerSize, expectedTranslation) => {
|
|
135
|
+
const { gestureCallbacks, translation } = renderGesture({
|
|
136
|
+
vertical,
|
|
137
|
+
size,
|
|
138
|
+
dataLength,
|
|
139
|
+
containerSize,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
act(() => {
|
|
143
|
+
gestureCallbacks.onGestureStart(
|
|
144
|
+
{} as GestureStateChangeEvent<PanGestureHandlerEventPayload>
|
|
145
|
+
);
|
|
146
|
+
gestureCallbacks.onGestureUpdate({
|
|
147
|
+
translationX: -1000,
|
|
148
|
+
translationY: -1000,
|
|
149
|
+
} as GestureUpdateEvent<PanGestureHandlerEventPayload>);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(translation.value).toBeCloseTo(expectedTranslation);
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
});
|
|
@@ -8,9 +8,7 @@ import type {
|
|
|
8
8
|
import { GestureDetector } from "react-native-gesture-handler";
|
|
9
9
|
import Animated, {
|
|
10
10
|
cancelAnimation,
|
|
11
|
-
measure,
|
|
12
11
|
useAnimatedReaction,
|
|
13
|
-
useAnimatedRef,
|
|
14
12
|
useDerivedValue,
|
|
15
13
|
useSharedValue,
|
|
16
14
|
withDecay,
|
|
@@ -56,7 +54,7 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
|
|
|
56
54
|
fixedDirection,
|
|
57
55
|
},
|
|
58
56
|
common: { size, resolvedSize, sizePhase, sizeExplicit },
|
|
59
|
-
layout: { updateContainerSize },
|
|
57
|
+
layout: { containerSize, updateContainerSize },
|
|
60
58
|
} = useGlobalState();
|
|
61
59
|
|
|
62
60
|
const {
|
|
@@ -77,7 +75,6 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
|
|
|
77
75
|
const validStart = useSharedValue(false);
|
|
78
76
|
const scrollEndTranslation = useSharedValue(0);
|
|
79
77
|
const scrollEndVelocity = useSharedValue(0);
|
|
80
|
-
const containerRef = useAnimatedRef<Animated.View>();
|
|
81
78
|
const maxScrollDistancePerSwipeIsSet = typeof maxScrollDistancePerSwipe === "number";
|
|
82
79
|
const minScrollDistancePerSwipeIsSet = typeof minScrollDistancePerSwipe === "number";
|
|
83
80
|
const sizeReady = useDerivedValue(() => {
|
|
@@ -92,18 +89,19 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
|
|
|
92
89
|
if (size <= 0) return 0;
|
|
93
90
|
|
|
94
91
|
if (!loop && !overscrollEnabled) {
|
|
95
|
-
const
|
|
96
|
-
|
|
92
|
+
const containerMainAxisSize = vertical
|
|
93
|
+
? containerSize.value.height
|
|
94
|
+
: containerSize.value.width;
|
|
97
95
|
|
|
98
|
-
// If the item's total
|
|
99
|
-
if (dataLength * size <
|
|
96
|
+
// If the item's total size is less than the container's size, then there is no need to scroll.
|
|
97
|
+
if (containerMainAxisSize <= 0 || dataLength * size < containerMainAxisSize) return 0;
|
|
100
98
|
|
|
101
99
|
// Disable the "overscroll" effect
|
|
102
|
-
return dataLength * size -
|
|
100
|
+
return dataLength * size - containerMainAxisSize;
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
return dataLength * size;
|
|
106
|
-
}, [loop, size, dataLength, overscrollEnabled]);
|
|
104
|
+
}, [loop, size, dataLength, overscrollEnabled, vertical, containerSize]);
|
|
107
105
|
|
|
108
106
|
const withSpring = React.useCallback(
|
|
109
107
|
(toValue: number, onFinished?: () => void) => {
|
|
@@ -515,7 +513,6 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
|
|
|
515
513
|
return (
|
|
516
514
|
<GestureDetector gesture={gesture}>
|
|
517
515
|
<Animated.View
|
|
518
|
-
ref={containerRef}
|
|
519
516
|
testID={testID}
|
|
520
517
|
style={style}
|
|
521
518
|
onTouchStart={onTouchBegin}
|
|
@@ -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(
|
|
@@ -67,6 +67,35 @@ describe("useVisibleRanges", () => {
|
|
|
67
67
|
`);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
it.each([
|
|
71
|
+
{ total: 1, windowSize: 1, currentIndex: 0 },
|
|
72
|
+
{ total: 2, windowSize: 5, currentIndex: 1 },
|
|
73
|
+
{ total: 3, windowSize: 3, currentIndex: 2 },
|
|
74
|
+
{ total: 4, windowSize: 5, currentIndex: 1 },
|
|
75
|
+
])(
|
|
76
|
+
"should display every item when windowSize=$windowSize is at least total=$total in loop mode",
|
|
77
|
+
({ total, windowSize, currentIndex }) => {
|
|
78
|
+
const ranges = renderHook(() => {
|
|
79
|
+
const translation = useSharedValue(-currentIndex * viewSize);
|
|
80
|
+
return useVisibleRanges({
|
|
81
|
+
total,
|
|
82
|
+
translation,
|
|
83
|
+
viewSize,
|
|
84
|
+
windowSize,
|
|
85
|
+
loop: true,
|
|
86
|
+
});
|
|
87
|
+
}).result.current.value;
|
|
88
|
+
|
|
89
|
+
const visibleIndexes = Array.from({ length: total }, (_, index) => index).filter(
|
|
90
|
+
(index) =>
|
|
91
|
+
(index >= ranges.negativeRange[0] && index <= ranges.negativeRange[1]) ||
|
|
92
|
+
(index >= ranges.positiveRange[0] && index <= ranges.positiveRange[1])
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(visibleIndexes).toEqual(Array.from({ length: total }, (_, index) => index));
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
70
99
|
it("should shows the increased range of the list when the loop is false and swiped the carousel.", async () => {
|
|
71
100
|
const slide0hook = renderHook(() => {
|
|
72
101
|
const translation = useSharedValue(-0 * viewSize);
|