react-native-screen-transitions 3.0.0-rc.5 → 3.1.0
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 +360 -97
- package/lib/commonjs/blank-stack/components/overlay.js +1 -1
- package/lib/commonjs/blank-stack/components/overlay.js.map +1 -1
- package/lib/commonjs/shared/components/create-transition-aware-component.js +2 -0
- package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
- package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +9 -4
- package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
- package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +29 -5
- package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
- package/lib/commonjs/shared/hooks/gestures/use-screen-gesture.js +26 -0
- package/lib/commonjs/shared/hooks/gestures/use-screen-gesture.js.map +1 -0
- package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +32 -60
- package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
- package/lib/commonjs/shared/index.js +7 -0
- package/lib/commonjs/shared/index.js.map +1 -1
- package/lib/commonjs/shared/providers/gestures.provider.js +8 -18
- package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
- package/lib/commonjs/shared/utils/animation/derivations.js +7 -0
- package/lib/commonjs/shared/utils/animation/derivations.js.map +1 -1
- package/lib/commonjs/shared/utils/bounds/helpers/interpolate-style.js +30 -0
- package/lib/commonjs/shared/utils/bounds/helpers/interpolate-style.js.map +1 -0
- package/lib/commonjs/shared/utils/bounds/index.js +29 -1
- package/lib/commonjs/shared/utils/bounds/index.js.map +1 -1
- package/lib/commonjs/shared/utils/create-provider.js +16 -0
- package/lib/commonjs/shared/utils/create-provider.js.map +1 -1
- package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +4 -0
- package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
- package/lib/module/blank-stack/components/overlay.js +1 -1
- package/lib/module/blank-stack/components/overlay.js.map +1 -1
- package/lib/module/shared/components/create-transition-aware-component.js +2 -0
- package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
- package/lib/module/shared/hooks/animation/use-screen-animation.js +9 -4
- package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
- package/lib/module/shared/hooks/gestures/use-build-gestures.js +30 -6
- package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
- package/lib/module/shared/hooks/gestures/use-screen-gesture.js +22 -0
- package/lib/module/shared/hooks/gestures/use-screen-gesture.js.map +1 -0
- package/lib/module/shared/hooks/gestures/use-scroll-registry.js +32 -60
- package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
- package/lib/module/shared/index.js +1 -0
- package/lib/module/shared/index.js.map +1 -1
- package/lib/module/shared/providers/gestures.provider.js +9 -19
- package/lib/module/shared/providers/gestures.provider.js.map +1 -1
- package/lib/module/shared/utils/animation/derivations.js +7 -0
- package/lib/module/shared/utils/animation/derivations.js.map +1 -1
- package/lib/module/shared/utils/bounds/helpers/interpolate-style.js +26 -0
- package/lib/module/shared/utils/bounds/helpers/interpolate-style.js.map +1 -0
- package/lib/module/shared/utils/bounds/index.js +29 -1
- package/lib/module/shared/utils/bounds/index.js.map +1 -1
- package/lib/module/shared/utils/create-provider.js +17 -1
- package/lib/module/shared/utils/create-provider.js.map +1 -1
- package/lib/module/shared/utils/gesture/check-gesture-activation.js +4 -4
- package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
- package/lib/typescript/blank-stack/types.d.ts +2 -14
- package/lib/typescript/blank-stack/types.d.ts.map +1 -1
- package/lib/typescript/shared/components/create-transition-aware-component.d.ts +1 -0
- package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
- package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
- package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +1 -0
- package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
- package/lib/typescript/shared/hooks/gestures/use-screen-gesture.d.ts +15 -0
- package/lib/typescript/shared/hooks/gestures/use-screen-gesture.d.ts.map +1 -0
- package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +1 -0
- package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
- package/lib/typescript/shared/index.d.ts +4 -2
- package/lib/typescript/shared/index.d.ts.map +1 -1
- package/lib/typescript/shared/providers/gestures.provider.d.ts +2 -6
- package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
- package/lib/typescript/shared/types/animation.types.d.ts +51 -0
- package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
- package/lib/typescript/shared/types/bounds.types.d.ts +6 -0
- package/lib/typescript/shared/types/bounds.types.d.ts.map +1 -1
- package/lib/typescript/shared/types/core.types.d.ts +7 -0
- package/lib/typescript/shared/types/core.types.d.ts.map +1 -1
- package/lib/typescript/shared/utils/animation/derivations.d.ts +3 -1
- package/lib/typescript/shared/utils/animation/derivations.d.ts.map +1 -1
- package/lib/typescript/shared/utils/bounds/helpers/interpolate-style.d.ts +17 -0
- package/lib/typescript/shared/utils/bounds/helpers/interpolate-style.d.ts.map +1 -0
- package/lib/typescript/shared/utils/bounds/index.d.ts.map +1 -1
- package/lib/typescript/shared/utils/create-provider.d.ts +5 -1
- package/lib/typescript/shared/utils/create-provider.d.ts.map +1 -1
- package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +49 -1
- package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/blank-stack/components/overlay.tsx +1 -1
- package/src/blank-stack/types.ts +2 -15
- package/src/shared/__tests__/derivations.test.ts +155 -0
- package/src/shared/__tests__/gesture-activation.test.ts +251 -0
- package/src/shared/components/create-transition-aware-component.tsx +2 -1
- package/src/shared/hooks/animation/use-screen-animation.tsx +9 -2
- package/src/shared/hooks/gestures/use-build-gestures.tsx +35 -8
- package/src/shared/hooks/gestures/use-screen-gesture.ts +19 -0
- package/src/shared/hooks/gestures/use-scroll-registry.tsx +39 -59
- package/src/shared/index.ts +2 -0
- package/src/shared/providers/gestures.provider.tsx +15 -27
- package/src/shared/types/animation.types.ts +57 -0
- package/src/shared/types/bounds.types.ts +11 -0
- package/src/shared/types/core.types.ts +8 -0
- package/src/shared/utils/animation/derivations.ts +8 -1
- package/src/shared/utils/bounds/helpers/interpolate-style.ts +38 -0
- package/src/shared/utils/bounds/index.ts +31 -1
- package/src/shared/utils/create-provider.tsx +31 -1
- package/src/shared/utils/gesture/check-gesture-activation.ts +4 -4
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
normalizeSides,
|
|
4
|
+
computeEdgeConstraints,
|
|
5
|
+
calculateSwipeDirs,
|
|
6
|
+
shouldActivateOrFail,
|
|
7
|
+
} from "../utils/gesture/check-gesture-activation";
|
|
8
|
+
|
|
9
|
+
describe("normalizeSides", () => {
|
|
10
|
+
it("returns all sides as 'screen' when no area provided", () => {
|
|
11
|
+
const result = normalizeSides();
|
|
12
|
+
expect(result).toEqual({
|
|
13
|
+
left: "screen",
|
|
14
|
+
right: "screen",
|
|
15
|
+
top: "screen",
|
|
16
|
+
bottom: "screen",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("normalizes string input to all sides", () => {
|
|
21
|
+
const result = normalizeSides("edge");
|
|
22
|
+
expect(result).toEqual({
|
|
23
|
+
left: "edge",
|
|
24
|
+
right: "edge",
|
|
25
|
+
top: "edge",
|
|
26
|
+
bottom: "edge",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("handles per-side object input", () => {
|
|
31
|
+
const result = normalizeSides({
|
|
32
|
+
left: "edge",
|
|
33
|
+
right: "screen",
|
|
34
|
+
top: "edge",
|
|
35
|
+
});
|
|
36
|
+
expect(result).toEqual({
|
|
37
|
+
left: "edge",
|
|
38
|
+
right: "screen",
|
|
39
|
+
top: "edge",
|
|
40
|
+
bottom: "screen", // defaults to screen
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("defaults missing sides to screen", () => {
|
|
45
|
+
const result = normalizeSides({ left: "edge" });
|
|
46
|
+
expect(result.left).toBe("edge");
|
|
47
|
+
expect(result.right).toBe("screen");
|
|
48
|
+
expect(result.top).toBe("screen");
|
|
49
|
+
expect(result.bottom).toBe("screen");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("computeEdgeConstraints", () => {
|
|
54
|
+
const dimensions = { width: 375, height: 812 };
|
|
55
|
+
const allScreen = { left: "screen", right: "screen", top: "screen", bottom: "screen" } as const;
|
|
56
|
+
const allEdge = { left: "edge", right: "edge", top: "edge", bottom: "edge" } as const;
|
|
57
|
+
|
|
58
|
+
it("allows all directions when all sides are 'screen'", () => {
|
|
59
|
+
const result = computeEdgeConstraints({ x: 200, y: 400 }, dimensions, allScreen);
|
|
60
|
+
expect(result.horizontalRight).toBe(true);
|
|
61
|
+
expect(result.horizontalLeft).toBe(true);
|
|
62
|
+
expect(result.verticalDown).toBe(true);
|
|
63
|
+
expect(result.verticalUp).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("restricts to left edge for horizontal-right when edge mode", () => {
|
|
67
|
+
// Touch at x=200 (center) should NOT allow right swipe with edge activation
|
|
68
|
+
const center = computeEdgeConstraints({ x: 200, y: 400 }, dimensions, allEdge);
|
|
69
|
+
expect(center.horizontalRight).toBe(false);
|
|
70
|
+
|
|
71
|
+
// Touch at x=30 (within 50px edge) should allow right swipe
|
|
72
|
+
const edge = computeEdgeConstraints({ x: 30, y: 400 }, dimensions, allEdge);
|
|
73
|
+
expect(edge.horizontalRight).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("restricts to right edge for horizontal-left when edge mode", () => {
|
|
77
|
+
// Touch at x=200 (center) should NOT allow left swipe
|
|
78
|
+
const center = computeEdgeConstraints({ x: 200, y: 400 }, dimensions, allEdge);
|
|
79
|
+
expect(center.horizontalLeft).toBe(false);
|
|
80
|
+
|
|
81
|
+
// Touch at x=350 (within 50px of right edge) should allow left swipe
|
|
82
|
+
const edge = computeEdgeConstraints({ x: 350, y: 400 }, dimensions, allEdge);
|
|
83
|
+
expect(edge.horizontalLeft).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("restricts to top edge for vertical-down when edge mode", () => {
|
|
87
|
+
// Touch at y=400 (center) should NOT allow down swipe
|
|
88
|
+
const center = computeEdgeConstraints({ x: 200, y: 400 }, dimensions, allEdge);
|
|
89
|
+
expect(center.verticalDown).toBe(false);
|
|
90
|
+
|
|
91
|
+
// Touch at y=100 (within 135px edge) should allow down swipe
|
|
92
|
+
const edge = computeEdgeConstraints({ x: 200, y: 100 }, dimensions, allEdge);
|
|
93
|
+
expect(edge.verticalDown).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("uses custom responseDistance", () => {
|
|
97
|
+
// With custom distance of 100px
|
|
98
|
+
const result = computeEdgeConstraints({ x: 80, y: 400 }, dimensions, allEdge, 100);
|
|
99
|
+
expect(result.horizontalRight).toBe(true); // 80 < 100
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("calculateSwipeDirs", () => {
|
|
104
|
+
it("detects horizontal swipe right", () => {
|
|
105
|
+
const result = calculateSwipeDirs(50, 10);
|
|
106
|
+
expect(result.isHorizontalSwipe).toBe(true);
|
|
107
|
+
expect(result.isVerticalSwipe).toBe(false);
|
|
108
|
+
expect(result.isSwipingRight).toBe(true);
|
|
109
|
+
expect(result.isSwipingLeft).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("detects horizontal swipe left", () => {
|
|
113
|
+
const result = calculateSwipeDirs(-50, 10);
|
|
114
|
+
expect(result.isHorizontalSwipe).toBe(true);
|
|
115
|
+
expect(result.isSwipingLeft).toBe(true);
|
|
116
|
+
expect(result.isSwipingRight).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("detects vertical swipe down", () => {
|
|
120
|
+
const result = calculateSwipeDirs(10, 50);
|
|
121
|
+
expect(result.isVerticalSwipe).toBe(true);
|
|
122
|
+
expect(result.isHorizontalSwipe).toBe(false);
|
|
123
|
+
expect(result.isSwipingDown).toBe(true);
|
|
124
|
+
expect(result.isSwipingUp).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("detects vertical swipe up", () => {
|
|
128
|
+
const result = calculateSwipeDirs(10, -50);
|
|
129
|
+
expect(result.isVerticalSwipe).toBe(true);
|
|
130
|
+
expect(result.isSwipingUp).toBe(true);
|
|
131
|
+
expect(result.isSwipingDown).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("horizontal wins when equal deltas", () => {
|
|
135
|
+
// When deltaX === deltaY, neither wins (both false)
|
|
136
|
+
const result = calculateSwipeDirs(50, 50);
|
|
137
|
+
expect(result.isHorizontalSwipe).toBe(false);
|
|
138
|
+
expect(result.isVerticalSwipe).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("shouldActivateOrFail", () => {
|
|
143
|
+
const baseParams = {
|
|
144
|
+
deltaX: 0,
|
|
145
|
+
deltaY: 0,
|
|
146
|
+
hasHorizontal: true,
|
|
147
|
+
hasVertical: true,
|
|
148
|
+
isHorizontalSwipe: false,
|
|
149
|
+
isVerticalSwipe: false,
|
|
150
|
+
allowedRight: true,
|
|
151
|
+
allowedLeft: true,
|
|
152
|
+
allowedUp: true,
|
|
153
|
+
allowedDown: true,
|
|
154
|
+
horizontalGateRight: true,
|
|
155
|
+
horizontalGateLeft: true,
|
|
156
|
+
verticalGateUp: true,
|
|
157
|
+
verticalGateDown: true,
|
|
158
|
+
isSwipingRight: false,
|
|
159
|
+
isSwipingLeft: false,
|
|
160
|
+
isSwipingUp: false,
|
|
161
|
+
isSwipingDown: false,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
it("activates on valid horizontal right swipe", () => {
|
|
165
|
+
const result = shouldActivateOrFail({
|
|
166
|
+
...baseParams,
|
|
167
|
+
deltaX: 15, // above threshold (10)
|
|
168
|
+
deltaY: 5, // within tolerance (15)
|
|
169
|
+
isHorizontalSwipe: true,
|
|
170
|
+
isSwipingRight: true,
|
|
171
|
+
});
|
|
172
|
+
expect(result.shouldActivate).toBe(true);
|
|
173
|
+
expect(result.shouldFail).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("activates on valid vertical down swipe", () => {
|
|
177
|
+
const result = shouldActivateOrFail({
|
|
178
|
+
...baseParams,
|
|
179
|
+
deltaX: 5, // within tolerance (20)
|
|
180
|
+
deltaY: 15, // above threshold (10)
|
|
181
|
+
isVerticalSwipe: true,
|
|
182
|
+
isSwipingDown: true,
|
|
183
|
+
});
|
|
184
|
+
expect(result.shouldActivate).toBe(true);
|
|
185
|
+
expect(result.shouldFail).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("fails when swiping in disallowed direction", () => {
|
|
189
|
+
const result = shouldActivateOrFail({
|
|
190
|
+
...baseParams,
|
|
191
|
+
deltaX: 15,
|
|
192
|
+
deltaY: 5,
|
|
193
|
+
isHorizontalSwipe: true,
|
|
194
|
+
isSwipingRight: true,
|
|
195
|
+
allowedRight: false, // direction not allowed
|
|
196
|
+
});
|
|
197
|
+
expect(result.shouldActivate).toBe(false);
|
|
198
|
+
expect(result.shouldFail).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("fails when edge gate blocks the swipe", () => {
|
|
202
|
+
const result = shouldActivateOrFail({
|
|
203
|
+
...baseParams,
|
|
204
|
+
deltaX: 15,
|
|
205
|
+
deltaY: 5,
|
|
206
|
+
isHorizontalSwipe: true,
|
|
207
|
+
isSwipingRight: true,
|
|
208
|
+
horizontalGateRight: false, // edge gate blocks
|
|
209
|
+
});
|
|
210
|
+
expect(result.shouldActivate).toBe(false);
|
|
211
|
+
expect(result.shouldFail).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("fails when vertical deviation exceeds tolerance during horizontal swipe", () => {
|
|
215
|
+
const result = shouldActivateOrFail({
|
|
216
|
+
...baseParams,
|
|
217
|
+
deltaX: 15,
|
|
218
|
+
deltaY: 20, // exceeds GESTURE_FAIL_TOLERANCE_X (15)
|
|
219
|
+
isHorizontalSwipe: true,
|
|
220
|
+
isSwipingRight: true,
|
|
221
|
+
});
|
|
222
|
+
expect(result.shouldActivate).toBe(false);
|
|
223
|
+
expect(result.shouldFail).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("does not activate when movement is below threshold", () => {
|
|
227
|
+
const result = shouldActivateOrFail({
|
|
228
|
+
...baseParams,
|
|
229
|
+
deltaX: 5, // below threshold (10)
|
|
230
|
+
deltaY: 2,
|
|
231
|
+
isHorizontalSwipe: true,
|
|
232
|
+
isSwipingRight: true,
|
|
233
|
+
});
|
|
234
|
+
expect(result.shouldActivate).toBe(false);
|
|
235
|
+
expect(result.shouldFail).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("handles bidirectional gesture activation", () => {
|
|
239
|
+
// Both horizontal and vertical allowed, vertical swipe detected
|
|
240
|
+
const result = shouldActivateOrFail({
|
|
241
|
+
...baseParams,
|
|
242
|
+
hasHorizontal: true,
|
|
243
|
+
hasVertical: true,
|
|
244
|
+
deltaX: 5,
|
|
245
|
+
deltaY: 15,
|
|
246
|
+
isVerticalSwipe: true,
|
|
247
|
+
isSwipingDown: true,
|
|
248
|
+
});
|
|
249
|
+
expect(result.shouldActivate).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** biome-ignore-all lint/style/noNonNullAssertion: <This helper is usually being used inside a transitionable stack> */
|
|
1
2
|
import type React from "react";
|
|
2
3
|
import { type ComponentType, forwardRef, memo } from "react";
|
|
3
4
|
import type { View } from "react-native";
|
|
@@ -26,7 +27,7 @@ export function createTransitionAwareComponent<P extends object>(
|
|
|
26
27
|
React.ComponentRef<typeof Wrapped>,
|
|
27
28
|
TransitionAwareProps<P>
|
|
28
29
|
>((props: Any, ref) => {
|
|
29
|
-
const { nativeGesture } = useGestureContext()
|
|
30
|
+
const { nativeGesture } = useGestureContext()!;
|
|
30
31
|
const { scrollHandler, onContentSizeChange, onLayout } = useScrollRegistry({
|
|
31
32
|
onScroll: props.onScroll,
|
|
32
33
|
onContentSizeChange: props.onContentSizeChange,
|
|
@@ -28,11 +28,13 @@ type BuiltState = {
|
|
|
28
28
|
animating: SharedValue<number>;
|
|
29
29
|
gesture: GestureStoreMap;
|
|
30
30
|
route: RouteProp<ParamListBase>;
|
|
31
|
+
meta?: Record<string, unknown>;
|
|
31
32
|
unwrapped: ScreenTransitionState;
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const createScreenTransitionState = (
|
|
35
36
|
route: RouteProp<ParamListBase>,
|
|
37
|
+
meta?: Record<string, unknown>,
|
|
36
38
|
): ScreenTransitionState => ({
|
|
37
39
|
progress: 0,
|
|
38
40
|
closing: 0,
|
|
@@ -47,6 +49,7 @@ const createScreenTransitionState = (
|
|
|
47
49
|
direction: null,
|
|
48
50
|
},
|
|
49
51
|
route,
|
|
52
|
+
meta,
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
const unwrapInto = (s: BuiltState): ScreenTransitionState => {
|
|
@@ -62,6 +65,7 @@ const unwrapInto = (s: BuiltState): ScreenTransitionState => {
|
|
|
62
65
|
out.gesture.isDismissing = s.gesture.isDismissing.value;
|
|
63
66
|
out.gesture.isDragging = s.gesture.isDragging.value;
|
|
64
67
|
out.gesture.direction = s.gesture.direction.value;
|
|
68
|
+
out.meta = s.meta;
|
|
65
69
|
|
|
66
70
|
return out;
|
|
67
71
|
};
|
|
@@ -70,6 +74,7 @@ const useBuildScreenTransitionState = (
|
|
|
70
74
|
descriptor: TransitionDescriptor | undefined,
|
|
71
75
|
): BuiltState | undefined => {
|
|
72
76
|
const key = descriptor?.route.key;
|
|
77
|
+
const meta = descriptor?.options?.meta;
|
|
73
78
|
|
|
74
79
|
return useMemo(() => {
|
|
75
80
|
if (!key) return undefined;
|
|
@@ -80,9 +85,10 @@ const useBuildScreenTransitionState = (
|
|
|
80
85
|
animating: AnimationStore.getAnimation(key, "animating"),
|
|
81
86
|
gesture: GestureStore.getRouteGestures(key),
|
|
82
87
|
route: descriptor.route,
|
|
83
|
-
|
|
88
|
+
meta,
|
|
89
|
+
unwrapped: createScreenTransitionState(descriptor.route, meta),
|
|
84
90
|
};
|
|
85
|
-
}, [key, descriptor?.route]);
|
|
91
|
+
}, [key, descriptor?.route, meta]);
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
const hasTransitionsEnabled = (
|
|
@@ -131,6 +137,7 @@ export function _useScreenAnimation() {
|
|
|
131
137
|
: DEFAULT_SCREEN_TRANSITION_STATE;
|
|
132
138
|
|
|
133
139
|
const { progress, ...helpers } = derivations({
|
|
140
|
+
previous,
|
|
134
141
|
current,
|
|
135
142
|
next,
|
|
136
143
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StackActions } from "@react-navigation/native";
|
|
2
|
-
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
3
3
|
import { useWindowDimensions } from "react-native";
|
|
4
4
|
import {
|
|
5
5
|
Gesture,
|
|
@@ -48,6 +48,7 @@ export const useBuildGestures = ({
|
|
|
48
48
|
ancestorContext,
|
|
49
49
|
}: BuildGesturesHookProps): {
|
|
50
50
|
panGesture: GestureType;
|
|
51
|
+
panGestureRef: React.MutableRefObject<GestureType | undefined>;
|
|
51
52
|
nativeGesture: GestureType;
|
|
52
53
|
gestureAnimationValues: GestureStoreMap;
|
|
53
54
|
} => {
|
|
@@ -63,6 +64,9 @@ export const useBuildGestures = ({
|
|
|
63
64
|
GestureOffsetState.PENDING,
|
|
64
65
|
);
|
|
65
66
|
|
|
67
|
+
// Ref for external gesture coordination (e.g., swipeable lists)
|
|
68
|
+
const panGestureRef = useRef<GestureType | undefined>(undefined);
|
|
69
|
+
|
|
66
70
|
const gestureAnimationValues = GestureStore.getRouteGestures(
|
|
67
71
|
current.route.key,
|
|
68
72
|
);
|
|
@@ -325,26 +329,49 @@ export const useBuildGestures = ({
|
|
|
325
329
|
},
|
|
326
330
|
);
|
|
327
331
|
|
|
332
|
+
// Memoize gestures to keep stable references - critical for RNGH
|
|
333
|
+
// Child gestures reference ancestor's pan via requireExternalGestureToFail,
|
|
334
|
+
// so the pan gesture MUST be stable or children will reference stale objects
|
|
328
335
|
return useMemo(() => {
|
|
329
|
-
const nativeGesture = Gesture.Native();
|
|
330
|
-
|
|
331
336
|
const panGesture = Gesture.Pan()
|
|
337
|
+
.withRef(panGestureRef)
|
|
332
338
|
.enabled(gestureEnabled)
|
|
333
339
|
.manualActivation(true)
|
|
334
340
|
.onTouchesDown(onTouchesDown)
|
|
335
341
|
.onTouchesMove(onTouchesMove)
|
|
336
342
|
.onStart(onStart)
|
|
337
343
|
.onUpdate(onUpdate)
|
|
338
|
-
.onEnd(onEnd)
|
|
339
|
-
|
|
344
|
+
.onEnd(onEnd);
|
|
345
|
+
|
|
346
|
+
// Native gesture setup depends on whether this screen has gestures
|
|
347
|
+
let nativeGesture: GestureType;
|
|
348
|
+
|
|
349
|
+
if (gestureEnabled) {
|
|
350
|
+
// This screen has gestures - set up normal pan/native relationship
|
|
351
|
+
nativeGesture = Gesture.Native().requireExternalGestureToFail(panGesture);
|
|
352
|
+
panGesture.blocksExternalGesture(nativeGesture);
|
|
353
|
+
} else {
|
|
354
|
+
// This screen has no gestures
|
|
355
|
+
// Find nearest ancestor with gestureEnabled=true (attached pan)
|
|
356
|
+
let activePanAncestor = ancestorContext;
|
|
357
|
+
while (activePanAncestor && !activePanAncestor.gestureEnabled) {
|
|
358
|
+
activePanAncestor = activePanAncestor.ancestorContext;
|
|
359
|
+
}
|
|
340
360
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
361
|
+
if (activePanAncestor?.panGesture) {
|
|
362
|
+
// Found an ancestor with enabled pan - wait for it
|
|
363
|
+
nativeGesture = Gesture.Native().requireExternalGestureToFail(
|
|
364
|
+
activePanAncestor.panGesture,
|
|
365
|
+
);
|
|
366
|
+
} else {
|
|
367
|
+
// No ancestor with enabled pan - plain native
|
|
368
|
+
nativeGesture = Gesture.Native();
|
|
369
|
+
}
|
|
344
370
|
}
|
|
345
371
|
|
|
346
372
|
return {
|
|
347
373
|
panGesture,
|
|
374
|
+
panGestureRef,
|
|
348
375
|
nativeGesture,
|
|
349
376
|
gestureAnimationValues,
|
|
350
377
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useGestureContext } from "../../providers/gestures.provider";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a ref to the screen's navigation pan gesture.
|
|
5
|
+
* Use this to coordinate child gestures with the navigation gesture.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const screenGesture = useScreenGesture();
|
|
10
|
+
*
|
|
11
|
+
* const myPanGesture = Gesture.Pan()
|
|
12
|
+
* .waitFor(screenGesture) // Wait for navigation gesture to fail first
|
|
13
|
+
* .onUpdate(...);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const useScreenGesture = () => {
|
|
17
|
+
const ctx = useGestureContext();
|
|
18
|
+
return ctx?.panGestureRef ?? null;
|
|
19
|
+
};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** biome-ignore-all lint/style/noNonNullAssertion: <Will always consume context from GestureProvider> */
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
1
4
|
import type { LayoutChangeEvent } from "react-native";
|
|
2
5
|
import { useAnimatedScrollHandler } from "react-native-reanimated";
|
|
3
6
|
import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes";
|
|
@@ -12,13 +15,26 @@ interface ScrollProgressHookProps {
|
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export const useScrollRegistry = (props: ScrollProgressHookProps) => {
|
|
15
|
-
const
|
|
18
|
+
const context = useGestureContext()!;
|
|
19
|
+
const { scrollConfig, ancestorContext } = context;
|
|
20
|
+
|
|
21
|
+
const ancestorScrollConfigs = useMemo(() => {
|
|
22
|
+
const configs: (typeof scrollConfig)[] = [];
|
|
23
|
+
let current = ancestorContext;
|
|
24
|
+
while (current) {
|
|
25
|
+
if (current.scrollConfig) {
|
|
26
|
+
configs.push(current.scrollConfig);
|
|
27
|
+
}
|
|
28
|
+
current = current.ancestorContext;
|
|
29
|
+
}
|
|
30
|
+
return configs;
|
|
31
|
+
}, [ancestorContext]);
|
|
16
32
|
|
|
17
33
|
const scrollHandler = useAnimatedScrollHandler({
|
|
18
34
|
onScroll: (event) => {
|
|
19
35
|
props.onScroll?.(event);
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
const updateScrollPosition = (v: Any) => {
|
|
22
38
|
"worklet";
|
|
23
39
|
if (v === null) {
|
|
24
40
|
return {
|
|
@@ -33,25 +49,13 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
|
|
|
33
49
|
v.x = event.contentOffset.x;
|
|
34
50
|
v.y = event.contentOffset.y;
|
|
35
51
|
return v;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
x: event.contentOffset.x,
|
|
44
|
-
y: event.contentOffset.y,
|
|
45
|
-
contentHeight: 0,
|
|
46
|
-
contentWidth: 0,
|
|
47
|
-
layoutHeight: 0,
|
|
48
|
-
layoutWidth: 0,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
v.x = event.contentOffset.x;
|
|
52
|
-
v.y = event.contentOffset.y;
|
|
53
|
-
return v;
|
|
54
|
-
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
scrollConfig.modify(updateScrollPosition);
|
|
55
|
+
|
|
56
|
+
// Sync to ALL ancestors, not just immediate parent
|
|
57
|
+
for (const ancestorConfig of ancestorScrollConfigs) {
|
|
58
|
+
ancestorConfig.modify(updateScrollPosition);
|
|
55
59
|
}
|
|
56
60
|
},
|
|
57
61
|
});
|
|
@@ -60,7 +64,7 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
|
|
|
60
64
|
(width: number, height: number) => {
|
|
61
65
|
props.onContentSizeChange?.(width, height);
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
const updateContentSize = (v: Any) => {
|
|
64
68
|
"worklet";
|
|
65
69
|
if (v === null) {
|
|
66
70
|
return {
|
|
@@ -75,24 +79,12 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
|
|
|
75
79
|
v.contentWidth = width;
|
|
76
80
|
v.contentHeight = height;
|
|
77
81
|
return v;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
x: 0,
|
|
85
|
-
y: 0,
|
|
86
|
-
layoutHeight: 0,
|
|
87
|
-
layoutWidth: 0,
|
|
88
|
-
contentWidth: width,
|
|
89
|
-
contentHeight: height,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
v.contentWidth = width;
|
|
93
|
-
v.contentHeight = height;
|
|
94
|
-
return v;
|
|
95
|
-
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
scrollConfig.modify(updateContentSize);
|
|
85
|
+
|
|
86
|
+
for (const ancestorConfig of ancestorScrollConfigs) {
|
|
87
|
+
ancestorConfig.modify(updateContentSize);
|
|
96
88
|
}
|
|
97
89
|
},
|
|
98
90
|
);
|
|
@@ -101,7 +93,7 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
|
|
|
101
93
|
props.onLayout?.(event);
|
|
102
94
|
const { width, height } = event.nativeEvent.layout;
|
|
103
95
|
|
|
104
|
-
|
|
96
|
+
const updateLayout = (v: Any) => {
|
|
105
97
|
"worklet";
|
|
106
98
|
if (v === null) {
|
|
107
99
|
return {
|
|
@@ -116,24 +108,12 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
|
|
|
116
108
|
v.layoutHeight = height;
|
|
117
109
|
v.layoutWidth = width;
|
|
118
110
|
return v;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
x: 0,
|
|
126
|
-
y: 0,
|
|
127
|
-
contentHeight: 0,
|
|
128
|
-
contentWidth: 0,
|
|
129
|
-
layoutHeight: height,
|
|
130
|
-
layoutWidth: width,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
v.layoutHeight = height;
|
|
134
|
-
v.layoutWidth = width;
|
|
135
|
-
return v;
|
|
136
|
-
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
scrollConfig.modify(updateLayout);
|
|
114
|
+
|
|
115
|
+
for (const ancestorConfig of ancestorScrollConfigs) {
|
|
116
|
+
ancestorConfig.modify(updateLayout);
|
|
137
117
|
}
|
|
138
118
|
});
|
|
139
119
|
|
package/src/shared/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export default {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export { useScreenAnimation } from "./hooks/animation/use-screen-animation";
|
|
22
|
+
export { useScreenGesture } from "./hooks/gestures/use-screen-gesture";
|
|
22
23
|
|
|
23
24
|
export type {
|
|
24
25
|
AnimationConfig,
|
|
@@ -26,4 +27,5 @@ export type {
|
|
|
26
27
|
ScreenInterpolationProps,
|
|
27
28
|
ScreenStyleInterpolator,
|
|
28
29
|
} from "./types/animation.types";
|
|
30
|
+
export type { BoundEntry, BoundsLink } from "./types/bounds.types";
|
|
29
31
|
export type { ScreenTransitionConfig } from "./types/core.types";
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
1
|
import { StyleSheet, View } from "react-native";
|
|
3
2
|
import {
|
|
4
|
-
Gesture,
|
|
5
3
|
GestureDetector,
|
|
6
4
|
type GestureType,
|
|
7
5
|
} from "react-native-gesture-handler";
|
|
@@ -23,18 +21,14 @@ export type ScrollConfig = {
|
|
|
23
21
|
|
|
24
22
|
export interface GestureContextType {
|
|
25
23
|
panGesture: GestureType;
|
|
24
|
+
panGestureRef: React.MutableRefObject<GestureType | undefined>;
|
|
26
25
|
nativeGesture: GestureType;
|
|
27
26
|
scrollConfig: SharedValue<ScrollConfig | null>;
|
|
28
27
|
gestureAnimationValues: GestureStoreMap;
|
|
29
28
|
ancestorContext: GestureContextType | null;
|
|
29
|
+
gestureEnabled: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
* Provider that creates gesture handling for a screen.
|
|
34
|
-
* If the current screen doesn't have gestures enabled but an ancestor does,
|
|
35
|
-
* we pass through the ancestor's context so scrollable children can coordinate
|
|
36
|
-
* with the ancestor's gestures.
|
|
37
|
-
*/
|
|
38
32
|
export const {
|
|
39
33
|
ScreenGestureProvider,
|
|
40
34
|
useScreenGestureContext: useGestureContext,
|
|
@@ -42,38 +36,32 @@ export const {
|
|
|
42
36
|
{ children: React.ReactNode },
|
|
43
37
|
GestureContextType
|
|
44
38
|
>(({ children }) => {
|
|
45
|
-
const ancestorContext = useGestureContext();
|
|
46
39
|
const { current } = useKeys();
|
|
40
|
+
const ancestorContext = useGestureContext();
|
|
47
41
|
const scrollConfig = useSharedValue<ScrollConfig | null>(null);
|
|
48
42
|
|
|
49
|
-
const
|
|
50
|
-
const shouldPassthrough = !hasOwnGestures && !!ancestorContext;
|
|
43
|
+
const hasGestures = current.options.gestureEnabled === true;
|
|
51
44
|
|
|
52
|
-
const { panGesture, nativeGesture, gestureAnimationValues } =
|
|
45
|
+
const { panGesture, panGestureRef, nativeGesture, gestureAnimationValues } =
|
|
53
46
|
useBuildGestures({
|
|
54
47
|
scrollConfig,
|
|
55
48
|
ancestorContext,
|
|
56
49
|
});
|
|
57
50
|
|
|
58
|
-
const value: GestureContextType =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// When passing through, use a no-op gesture to avoid conflicts.
|
|
69
|
-
// Attaching the same gesture to multiple GestureDetectors causes issues.
|
|
70
|
-
const noOpGesture = useMemo(() => Gesture.Pan().enabled(false), []);
|
|
71
|
-
const activeGesture = shouldPassthrough ? noOpGesture : panGesture;
|
|
51
|
+
const value: GestureContextType = {
|
|
52
|
+
panGesture,
|
|
53
|
+
panGestureRef,
|
|
54
|
+
scrollConfig,
|
|
55
|
+
nativeGesture,
|
|
56
|
+
gestureAnimationValues,
|
|
57
|
+
ancestorContext,
|
|
58
|
+
gestureEnabled: hasGestures,
|
|
59
|
+
};
|
|
72
60
|
|
|
73
61
|
return {
|
|
74
62
|
value,
|
|
75
63
|
children: (
|
|
76
|
-
<GestureDetector gesture={
|
|
64
|
+
<GestureDetector gesture={panGesture}>
|
|
77
65
|
<View style={styles.container}>{children}</View>
|
|
78
66
|
</GestureDetector>
|
|
79
67
|
),
|