@react-navigation/stack 7.12.0 → 7.12.2
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/module/utils/gestureActivationCriteria.js +10 -8
- package/lib/module/utils/gestureActivationCriteria.js.map +1 -1
- package/lib/module/utils/useKeyboardManager.js +103 -9
- package/lib/module/utils/useKeyboardManager.js.map +1 -1
- package/lib/module/utils/viewIsDescendantOf.js +23 -0
- package/lib/module/utils/viewIsDescendantOf.js.map +1 -0
- package/lib/module/views/GestureHandler.js.map +1 -1
- package/lib/module/views/Header/Header.js +1 -1
- package/lib/module/views/InteractionManager.js +1 -0
- package/lib/module/views/InteractionManager.js.map +1 -1
- package/lib/module/views/Stack/Card.js +26 -15
- package/lib/module/views/Stack/Card.js.map +1 -1
- package/lib/module/views/Stack/CardContainer.js +7 -2
- package/lib/module/views/Stack/CardContainer.js.map +1 -1
- package/lib/module/views/Stack/CardContent.js +2 -2
- package/lib/module/views/Stack/CardStack.js +23 -7
- package/lib/module/views/Stack/CardStack.js.map +1 -1
- package/lib/module/views/Stack/StackView.js +37 -28
- package/lib/module/views/Stack/StackView.js.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/conditional.d.ts +1 -1
- package/lib/typescript/src/utils/conditional.d.ts.map +1 -1
- package/lib/typescript/src/utils/gestureActivationCriteria.d.ts +2 -49
- package/lib/typescript/src/utils/gestureActivationCriteria.d.ts.map +1 -1
- package/lib/typescript/src/utils/useKeyboardManager.d.ts +5 -1
- package/lib/typescript/src/utils/useKeyboardManager.d.ts.map +1 -1
- package/lib/typescript/src/utils/viewIsDescendantOf.d.ts +3 -0
- package/lib/typescript/src/utils/viewIsDescendantOf.d.ts.map +1 -0
- package/lib/typescript/src/views/GestureHandler.d.ts.map +1 -1
- package/lib/typescript/src/views/InteractionManager.d.ts.map +1 -1
- package/lib/typescript/src/views/Screens.d.ts +1 -1
- package/lib/typescript/src/views/Screens.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/CardStack.d.ts +1 -1
- package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/StackView.d.ts.map +1 -1
- package/package.json +15 -15
- package/src/types.tsx +16 -2
- package/src/utils/gestureActivationCriteria.tsx +13 -9
- package/src/utils/useKeyboardManager.tsx +144 -8
- package/src/utils/viewIsDescendantOf.tsx +33 -0
- package/src/views/GestureHandler.tsx +1 -1
- package/src/views/Header/Header.tsx +1 -1
- package/src/views/InteractionManager.tsx +1 -0
- package/src/views/Screens.tsx +1 -1
- package/src/views/Stack/Card.tsx +30 -12
- package/src/views/Stack/CardContainer.tsx +4 -3
- package/src/views/Stack/CardContent.tsx +2 -2
- package/src/views/Stack/CardStack.tsx +34 -11
- package/src/views/Stack/StackView.tsx +58 -39
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StackView.d.ts","sourceRoot":"","sources":["../../../../../src/views/Stack/StackView.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,SAAS,EAEd,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"StackView.d.ts","sourceRoot":"","sources":["../../../../../src/views/Stack/StackView.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,SAAS,EAEd,KAAK,oBAAoB,EAE1B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,gBAAa,CAAC;AASrB,KAAK,KAAK,GAAG,qBAAqB,GAAG;IACnC,SAAS,EAAE,eAAe,CAAC;IAC3B,KAAK,EAAE,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAC3C,UAAU,EAAE,sBAAsB,CAAC;IACnC,WAAW,EAAE,kBAAkB,CAAC;IAChC,QAAQ,EAAE,CACR,KAAK,EAAE,SAAS,CAAC,aAAa,CAAC,EAC/B,WAAW,EAAE,OAAO,KACjB,eAAe,CAAC;CACtB,CAAC;AAEF,KAAK,KAAK,GAAG;IAEX,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;IAExB,aAAa,EAAE,oBAAoB,CAAC,aAAa,CAAC,GAAG,SAAS,CAAC;IAE/D,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAE3B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAE3B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAG7B,WAAW,EAAE,kBAAkB,CAAC;CACjC,CAAC;AAWF,qBAAa,SAAU,SAAQ,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;IAC1D,MAAM,CAAC,wBAAwB,CAC7B,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EACtB,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;IA8QxB,KAAK,EAAE,KAAK,CAOV;IAEF,OAAO,CAAC,gBAAgB,CAYtB;IAEF,OAAO,CAAC,YAAY,CAElB;IAEF,OAAO,CAAC,eAAe,CAuDrB;IAEF,OAAO,CAAC,gBAAgB,CAwBtB;IAEF,OAAO,CAAC,qBAAqB,CAQxB;IAEL,OAAO,CAAC,mBAAmB,CAQtB;IAEL,OAAO,CAAC,kBAAkB,CAKxB;IAEF,OAAO,CAAC,gBAAgB,CAKtB;IAEF,OAAO,CAAC,mBAAmB,CAKzB;IAEF,MAAM;CA6DP"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-navigation/stack",
|
|
3
3
|
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
|
4
|
-
"version": "7.12.
|
|
4
|
+
"version": "7.12.2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native-component",
|
|
7
7
|
"react-component",
|
|
@@ -45,28 +45,28 @@
|
|
|
45
45
|
"clean": "del lib"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@react-navigation/elements": "^2.9.
|
|
48
|
+
"@react-navigation/elements": "^2.9.46",
|
|
49
49
|
"color": "^4.2.3",
|
|
50
50
|
"use-latest-callback": "^0.2.4"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@jest/globals": "^30.
|
|
54
|
-
"@react-navigation/native": "^7.5.
|
|
53
|
+
"@jest/globals": "^30.5.2",
|
|
54
|
+
"@react-navigation/native": "^7.5.2",
|
|
55
55
|
"@testing-library/react-native": "^13.2.1",
|
|
56
56
|
"@types/color": "^4.2.0",
|
|
57
|
-
"@types/react": "~19.
|
|
58
|
-
"del-cli": "^
|
|
59
|
-
"react": "19.
|
|
60
|
-
"react-native": "0.
|
|
61
|
-
"react-native-builder-bob": "^0.
|
|
62
|
-
"react-native-gesture-handler": "
|
|
63
|
-
"react-native-safe-area-context": "~5.
|
|
64
|
-
"react-native-screens": "^4.
|
|
65
|
-
"react-test-renderer": "19.
|
|
57
|
+
"@types/react": "~19.3.0",
|
|
58
|
+
"del-cli": "^8.0.1",
|
|
59
|
+
"react": "19.3.0",
|
|
60
|
+
"react-native": "0.88.1",
|
|
61
|
+
"react-native-builder-bob": "^0.44.1",
|
|
62
|
+
"react-native-gesture-handler": "^3.5.0",
|
|
63
|
+
"react-native-safe-area-context": "~5.11.1",
|
|
64
|
+
"react-native-screens": "^4.30.1",
|
|
65
|
+
"react-test-renderer": "19.3.0",
|
|
66
66
|
"typescript": "~6.0.3"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@react-navigation/native": "^7.5.
|
|
69
|
+
"@react-navigation/native": "^7.5.2",
|
|
70
70
|
"react": ">= 18.2.0",
|
|
71
71
|
"react-native": "*",
|
|
72
72
|
"react-native-gesture-handler": ">= 2.0.0",
|
|
@@ -91,5 +91,5 @@
|
|
|
91
91
|
]
|
|
92
92
|
]
|
|
93
93
|
},
|
|
94
|
-
"gitHead": "
|
|
94
|
+
"gitHead": "77fad70cd6d9ed492d7ce30177efa923e95294cb"
|
|
95
95
|
}
|
package/src/types.tsx
CHANGED
|
@@ -435,14 +435,28 @@ export type TransitionSpec =
|
|
|
435
435
|
animation: 'spring';
|
|
436
436
|
config: Omit<
|
|
437
437
|
Animated.SpringAnimationConfig,
|
|
438
|
-
'toValue'
|
|
438
|
+
| 'toValue'
|
|
439
|
+
| 'useNativeDriver'
|
|
440
|
+
| 'isInteraction'
|
|
441
|
+
| 'iterations'
|
|
442
|
+
| 'isLooping'
|
|
443
|
+
| 'onComplete'
|
|
444
|
+
| 'platformConfig'
|
|
445
|
+
| 'debugID'
|
|
439
446
|
>;
|
|
440
447
|
}
|
|
441
448
|
| {
|
|
442
449
|
animation: 'timing';
|
|
443
450
|
config: Omit<
|
|
444
451
|
Animated.TimingAnimationConfig,
|
|
445
|
-
'toValue'
|
|
452
|
+
| 'toValue'
|
|
453
|
+
| 'useNativeDriver'
|
|
454
|
+
| 'isInteraction'
|
|
455
|
+
| 'iterations'
|
|
456
|
+
| 'isLooping'
|
|
457
|
+
| 'onComplete'
|
|
458
|
+
| 'platformConfig'
|
|
459
|
+
| 'debugID'
|
|
446
460
|
>;
|
|
447
461
|
};
|
|
448
462
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LocaleDirection } from '@react-navigation/native';
|
|
2
|
+
import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
|
|
2
3
|
|
|
3
4
|
import type { GestureDirection, Layout } from '../types';
|
|
4
5
|
import { getInvertedMultiplier } from './getInvertedMultiplier';
|
|
@@ -19,7 +20,7 @@ export const gestureActivationCriteria = ({
|
|
|
19
20
|
gestureDirection: GestureDirection;
|
|
20
21
|
gestureResponseDistance?: number;
|
|
21
22
|
layout: Layout;
|
|
22
|
-
}) => {
|
|
23
|
+
}): PanGestureHandlerProperties => {
|
|
23
24
|
const enableTrackpadTwoFingerGesture = true;
|
|
24
25
|
|
|
25
26
|
const distance =
|
|
@@ -30,17 +31,20 @@ export const gestureActivationCriteria = ({
|
|
|
30
31
|
? GESTURE_RESPONSE_DISTANCE_VERTICAL
|
|
31
32
|
: GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
|
32
33
|
|
|
34
|
+
const failOffsetX: [number, number] = [-15, 15];
|
|
35
|
+
const failOffsetY: [number, number] = [-20, 20];
|
|
36
|
+
|
|
33
37
|
if (gestureDirection === 'vertical') {
|
|
34
38
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
failOffsetX,
|
|
40
|
+
activeOffsetY: 5,
|
|
37
41
|
hitSlop: { bottom: -layout.height + distance },
|
|
38
42
|
enableTrackpadTwoFingerGesture,
|
|
39
43
|
};
|
|
40
44
|
} else if (gestureDirection === 'vertical-inverted') {
|
|
41
45
|
return {
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
failOffsetX,
|
|
47
|
+
activeOffsetY: -5,
|
|
44
48
|
hitSlop: { top: -layout.height + distance },
|
|
45
49
|
enableTrackpadTwoFingerGesture,
|
|
46
50
|
};
|
|
@@ -53,15 +57,15 @@ export const gestureActivationCriteria = ({
|
|
|
53
57
|
|
|
54
58
|
if (invertedMultiplier === 1) {
|
|
55
59
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
activeOffsetX: 5,
|
|
61
|
+
failOffsetY,
|
|
58
62
|
hitSlop: { right: hitSlop },
|
|
59
63
|
enableTrackpadTwoFingerGesture,
|
|
60
64
|
};
|
|
61
65
|
} else {
|
|
62
66
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
activeOffsetX: -5,
|
|
68
|
+
failOffsetY,
|
|
65
69
|
hitSlop: { left: hitSlop },
|
|
66
70
|
enableTrackpadTwoFingerGesture,
|
|
67
71
|
};
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { type HostInstance, Keyboard, TextInput } from 'react-native';
|
|
2
|
+
import { type HostInstance, Keyboard, Platform, TextInput } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { viewIsDescendantOf } from './viewIsDescendantOf';
|
|
3
5
|
|
|
4
6
|
export function useKeyboardManager({
|
|
7
|
+
active,
|
|
5
8
|
enabled,
|
|
6
9
|
focused,
|
|
10
|
+
contentRef,
|
|
7
11
|
}: {
|
|
12
|
+
active: boolean;
|
|
8
13
|
enabled: boolean;
|
|
9
14
|
focused: boolean;
|
|
15
|
+
contentRef: React.RefObject<HostInstance | null>;
|
|
10
16
|
}) {
|
|
11
|
-
// Numeric id of the previously focused text input
|
|
12
|
-
// When a gesture didn't change the tab, we can restore the focused input with this
|
|
13
17
|
const previouslyFocusedTextInputRef = React.useRef<HostInstance>(undefined);
|
|
18
|
+
const unfocusedScreenTextInputRef = React.useRef<HostInstance>(undefined);
|
|
19
|
+
const closingGestureRef = React.useRef(false);
|
|
14
20
|
const startTimestampRef = React.useRef<number>(0);
|
|
15
21
|
const keyboardTimeoutRef = React.useRef<NodeJS.Timeout>(undefined);
|
|
16
22
|
const enabledRef = React.useRef(enabled);
|
|
23
|
+
const focusedRef = React.useRef(focused);
|
|
17
24
|
|
|
18
25
|
const clearKeyboardTimeout = React.useCallback(() => {
|
|
19
26
|
if (keyboardTimeoutRef.current !== undefined) {
|
|
@@ -23,12 +30,18 @@ export function useKeyboardManager({
|
|
|
23
30
|
}, []);
|
|
24
31
|
|
|
25
32
|
const onPageChangeStart = React.useCallback(() => {
|
|
26
|
-
if (!enabledRef.current) {
|
|
33
|
+
if (!enabledRef.current || !focusedRef.current) {
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
clearKeyboardTimeout();
|
|
31
38
|
|
|
39
|
+
// On Android, back button hides keyboard without blurring input,
|
|
40
|
+
// so we need to check if the keyboard is actually visible.
|
|
41
|
+
if (Platform.OS === 'android' && !Keyboard.isVisible()) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
const input = TextInput.State.currentlyFocusedInput();
|
|
33
46
|
|
|
34
47
|
// When a page change begins, blur the currently focused input
|
|
@@ -42,7 +55,7 @@ export function useKeyboardManager({
|
|
|
42
55
|
}, [clearKeyboardTimeout]);
|
|
43
56
|
|
|
44
57
|
const onPageChangeCancel = React.useCallback(() => {
|
|
45
|
-
if (!enabledRef.current) {
|
|
58
|
+
if (!enabledRef.current || !focusedRef.current) {
|
|
46
59
|
return;
|
|
47
60
|
}
|
|
48
61
|
|
|
@@ -81,11 +94,19 @@ export function useKeyboardManager({
|
|
|
81
94
|
active: boolean;
|
|
82
95
|
closing: boolean;
|
|
83
96
|
}) => {
|
|
84
|
-
if (!enabledRef.current) {
|
|
97
|
+
if (!enabledRef.current || !focusedRef.current) {
|
|
85
98
|
return;
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
if (!closing) {
|
|
102
|
+
// If the screen was reopened without losing focus,
|
|
103
|
+
// e.g. the swipe was blocked by preventing removal
|
|
104
|
+
// We should clear the saved input to avoid refocusing it incorrectly later.
|
|
105
|
+
if (closingGestureRef.current) {
|
|
106
|
+
unfocusedScreenTextInputRef.current = undefined;
|
|
107
|
+
closingGestureRef.current = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
onPageChangeCancel();
|
|
90
111
|
return;
|
|
91
112
|
}
|
|
@@ -105,6 +126,15 @@ export function useKeyboardManager({
|
|
|
105
126
|
input?.blur();
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
// If a closing swipe already blurred the input,
|
|
130
|
+
// we can't get it using currentlyFocusedInput() anymore.
|
|
131
|
+
// So we need to use the input that was saved before the blur.
|
|
132
|
+
if (gesture) {
|
|
133
|
+
unfocusedScreenTextInputRef.current =
|
|
134
|
+
previouslyFocusedTextInputRef.current;
|
|
135
|
+
closingGestureRef.current = true;
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
// Cleanup the ID on successful page change
|
|
109
139
|
previouslyFocusedTextInputRef.current = undefined;
|
|
110
140
|
},
|
|
@@ -115,13 +145,119 @@ export function useKeyboardManager({
|
|
|
115
145
|
// This handles the "navigate forward" case so we don't dismiss the new screen's
|
|
116
146
|
// auto-focused input from handleTransition.
|
|
117
147
|
React.useLayoutEffect(() => {
|
|
118
|
-
|
|
148
|
+
let cancelled = false;
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
enabledRef.current &&
|
|
152
|
+
// Check the previous focus value so we only dismiss when focus is lost,
|
|
153
|
+
// and not if the effect re-runs (e.g. because of StrictMode, Activity etc.)
|
|
154
|
+
focusedRef.current &&
|
|
155
|
+
!focused
|
|
156
|
+
) {
|
|
157
|
+
closingGestureRef.current = false;
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
Platform.OS !== 'web' &&
|
|
161
|
+
// On Android, back button hides keyboard without blurring input,
|
|
162
|
+
// so we need to check if the keyboard is actually visible.
|
|
163
|
+
(Platform.OS !== 'android' || Keyboard.isVisible())
|
|
164
|
+
) {
|
|
165
|
+
const input = TextInput.State.currentlyFocusedInput();
|
|
166
|
+
|
|
167
|
+
const content = contentRef.current;
|
|
168
|
+
|
|
169
|
+
if (input && content) {
|
|
170
|
+
viewIsDescendantOf(input, content, (isDescendant) => {
|
|
171
|
+
if (isDescendant && !cancelled && enabledRef.current) {
|
|
172
|
+
unfocusedScreenTextInputRef.current = input;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
119
178
|
Keyboard.dismiss();
|
|
120
179
|
}
|
|
121
|
-
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
cancelled = true;
|
|
183
|
+
};
|
|
184
|
+
}, [contentRef, enabled, focused]);
|
|
185
|
+
|
|
186
|
+
React.useEffect(() => {
|
|
187
|
+
let cancelled = false;
|
|
188
|
+
|
|
189
|
+
if (Platform.OS !== 'web' && focused && active) {
|
|
190
|
+
const input = unfocusedScreenTextInputRef.current;
|
|
191
|
+
const content = contentRef.current;
|
|
192
|
+
|
|
193
|
+
if (enabledRef.current && input && content) {
|
|
194
|
+
viewIsDescendantOf(input, content, (isDescendant) => {
|
|
195
|
+
if (
|
|
196
|
+
isDescendant &&
|
|
197
|
+
!cancelled &&
|
|
198
|
+
enabledRef.current &&
|
|
199
|
+
!TextInput.State.currentlyFocusedInput()
|
|
200
|
+
) {
|
|
201
|
+
input.focus();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
unfocusedScreenTextInputRef.current = undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return () => {
|
|
210
|
+
cancelled = true;
|
|
211
|
+
};
|
|
212
|
+
}, [active, contentRef, enabled, focused]);
|
|
213
|
+
|
|
214
|
+
// Dismiss keyboard if native focus is restored to an input in an unfocused screen.
|
|
215
|
+
// This can happen after dismissing a native alert when `autoFocus` is used on iOS.
|
|
216
|
+
React.useEffect(() => {
|
|
217
|
+
let cancelled = false;
|
|
218
|
+
|
|
219
|
+
const blurInputInScreen = () => {
|
|
220
|
+
if (focusedRef.current || !enabledRef.current) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const input = TextInput.State.currentlyFocusedInput();
|
|
225
|
+
|
|
226
|
+
const content = contentRef.current;
|
|
227
|
+
|
|
228
|
+
if (input && content) {
|
|
229
|
+
viewIsDescendantOf(input, content, (isDescendant) => {
|
|
230
|
+
if (
|
|
231
|
+
isDescendant &&
|
|
232
|
+
!cancelled &&
|
|
233
|
+
!focusedRef.current &&
|
|
234
|
+
enabledRef.current &&
|
|
235
|
+
TextInput.State.currentlyFocusedInput() === input
|
|
236
|
+
) {
|
|
237
|
+
input.blur();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const subscriptions = [
|
|
244
|
+
Keyboard.addListener('keyboardWillShow', blurInputInScreen),
|
|
245
|
+
Keyboard.addListener('keyboardDidShow', blurInputInScreen),
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
return () => {
|
|
249
|
+
cancelled = true;
|
|
250
|
+
subscriptions.forEach((subscription) => subscription.remove());
|
|
251
|
+
};
|
|
252
|
+
}, [contentRef]);
|
|
122
253
|
|
|
123
254
|
React.useLayoutEffect(() => {
|
|
255
|
+
if (!enabled) {
|
|
256
|
+
unfocusedScreenTextInputRef.current = undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
124
259
|
enabledRef.current = enabled;
|
|
260
|
+
focusedRef.current = focused;
|
|
125
261
|
});
|
|
126
262
|
|
|
127
263
|
React.useEffect(() => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { findNodeHandle, type HostInstance, UIManager } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export function viewIsDescendantOf(
|
|
4
|
+
view: HostInstance,
|
|
5
|
+
ancestor: HostInstance,
|
|
6
|
+
callback: (isDescendant: boolean) => void
|
|
7
|
+
) {
|
|
8
|
+
if (typeof ancestor.contains === 'function') {
|
|
9
|
+
callback(ancestor.contains(view));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (view.isConnected === false || ancestor.isConnected === false) {
|
|
14
|
+
callback(false);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const viewTag = findNodeHandle(view);
|
|
19
|
+
const ancestorTag = findNodeHandle(ancestor);
|
|
20
|
+
|
|
21
|
+
if (viewTag == null || ancestorTag == null) {
|
|
22
|
+
callback(false);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
UIManager.viewIsDescendantOf(
|
|
27
|
+
viewTag,
|
|
28
|
+
ancestorTag,
|
|
29
|
+
(result: boolean | boolean[]) => {
|
|
30
|
+
callback(Array.isArray(result) ? result[0] : result);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -9,7 +9,7 @@ const Dummy: any = ({ children }: { children: React.ReactNode }) => (
|
|
|
9
9
|
export const PanGestureHandler =
|
|
10
10
|
Dummy as React.ComponentType<PanGestureHandlerProperties>;
|
|
11
11
|
|
|
12
|
-
export const GestureHandlerRootView = View;
|
|
12
|
+
export const GestureHandlerRootView: typeof View = View;
|
|
13
13
|
|
|
14
14
|
export const GestureState = {
|
|
15
15
|
UNDETERMINED: 0,
|
|
@@ -29,7 +29,7 @@ export const Header = React.memo(function Header({
|
|
|
29
29
|
previousTitle = back.title;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// eslint-disable-next-line react
|
|
32
|
+
// eslint-disable-next-line @eslint-react/exhaustive-deps
|
|
33
33
|
const goBack = React.useCallback(
|
|
34
34
|
throttle(() => {
|
|
35
35
|
if (navigation.isFocused() && navigation.canGoBack()) {
|
|
@@ -9,6 +9,7 @@ type InteractionManagerType = {
|
|
|
9
9
|
let InteractionManager: InteractionManagerType | undefined;
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
|
+
// @ts-expect-error `InteractionManager` is removed from the types in newer React Native versions
|
|
12
13
|
InteractionManager = ReactNative.InteractionManager;
|
|
13
14
|
} catch (e) {
|
|
14
15
|
// On newer React Native versions, accessing InteractionManager throws an error
|
package/src/views/Screens.tsx
CHANGED
|
@@ -29,7 +29,7 @@ export const MaybeScreen = ({
|
|
|
29
29
|
...rest
|
|
30
30
|
}: ViewProps & {
|
|
31
31
|
enabled: boolean;
|
|
32
|
-
active: 0 | 1 | Animated.AnimatedInterpolation<0 | 1>;
|
|
32
|
+
active: 0 | 1 | 2 | Animated.AnimatedInterpolation<0 | 1 | 2>;
|
|
33
33
|
children: React.ReactNode;
|
|
34
34
|
freezeOnBlur?: boolean;
|
|
35
35
|
shouldFreeze: boolean;
|
package/src/views/Stack/Card.tsx
CHANGED
|
@@ -84,7 +84,11 @@ const hasOpacityStyle = (
|
|
|
84
84
|
if (style) {
|
|
85
85
|
const flattenedStyle = StyleSheet.flatten(style);
|
|
86
86
|
|
|
87
|
-
return
|
|
87
|
+
return (
|
|
88
|
+
flattenedStyle != null &&
|
|
89
|
+
'opacity' in flattenedStyle &&
|
|
90
|
+
flattenedStyle.opacity != null
|
|
91
|
+
);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
return false;
|
|
@@ -167,7 +171,7 @@ function Card({
|
|
|
167
171
|
React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
168
172
|
const pendingOnCloseCallbackRef =
|
|
169
173
|
React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
170
|
-
const
|
|
174
|
+
const initialAnimationHandleRef = React.useRef<number | undefined>(undefined);
|
|
171
175
|
|
|
172
176
|
const [isClosing] = React.useState(() => new Animated.Value(FALSE));
|
|
173
177
|
|
|
@@ -410,13 +414,23 @@ function Card({
|
|
|
410
414
|
gesture.stopAnimation();
|
|
411
415
|
onEndInteraction();
|
|
412
416
|
|
|
417
|
+
// In development, StrictMode re-runs effects without unmounting,
|
|
418
|
+
// e.g. when the card moves to a new position after rearranging.
|
|
419
|
+
// The stopped animation didn't reach its value, so reset the state,
|
|
420
|
+
// to make sure that `maybeAnimate` restarts it when effects re-run.
|
|
421
|
+
isAnimatingRef.current = false;
|
|
422
|
+
lastToValueRef.current = undefined;
|
|
423
|
+
|
|
413
424
|
if (animationHandleRef.current) {
|
|
414
425
|
cancelAnimationFrame(animationHandleRef.current);
|
|
415
426
|
}
|
|
416
427
|
|
|
417
428
|
clearTimeout(pendingGestureCallbackRef.current);
|
|
418
429
|
clearTimeout(pendingOnCloseCallbackRef.current);
|
|
419
|
-
|
|
430
|
+
|
|
431
|
+
if (initialAnimationHandleRef.current !== undefined) {
|
|
432
|
+
cancelAnimationFrame(initialAnimationHandleRef.current);
|
|
433
|
+
}
|
|
420
434
|
};
|
|
421
435
|
}, [gesture, onEndInteraction]);
|
|
422
436
|
|
|
@@ -426,16 +440,20 @@ function Card({
|
|
|
426
440
|
|
|
427
441
|
if (!didInitiallyAnimate.current) {
|
|
428
442
|
// Animate the card in on initial mount
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
// in the new architecture
|
|
443
|
+
// Wait after the next frame to ensure animation starts after mount.
|
|
444
|
+
// Otherwise, the animation can drop frames.
|
|
445
|
+
// This is especially important in the new architecture
|
|
432
446
|
// cf., https://github.com/react-navigation/react-navigation/issues/12401
|
|
433
|
-
|
|
447
|
+
if (initialAnimationHandleRef.current !== undefined) {
|
|
448
|
+
cancelAnimationFrame(initialAnimationHandleRef.current);
|
|
449
|
+
}
|
|
434
450
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
451
|
+
initialAnimationHandleRef.current = requestAnimationFrame(() => {
|
|
452
|
+
initialAnimationHandleRef.current = requestAnimationFrame(() => {
|
|
453
|
+
didInitiallyAnimate.current = true;
|
|
454
|
+
animate({ closing });
|
|
455
|
+
});
|
|
456
|
+
});
|
|
439
457
|
} else {
|
|
440
458
|
const previousOpening = previousPropsRef.current?.opening;
|
|
441
459
|
const previousToValue = previousPropsRef.current
|
|
@@ -561,7 +579,7 @@ function Card({
|
|
|
561
579
|
[gesture, gestureDirection, gestureEnabled]
|
|
562
580
|
);
|
|
563
581
|
|
|
564
|
-
const
|
|
582
|
+
const backgroundColor = StyleSheet.flatten(contentStyle)?.backgroundColor;
|
|
565
583
|
|
|
566
584
|
const isTransparent =
|
|
567
585
|
typeof backgroundColor === 'string'
|
|
@@ -102,10 +102,11 @@ function CardContainerInner({
|
|
|
102
102
|
const parentHeaderHeight = React.useContext(HeaderHeightContext);
|
|
103
103
|
|
|
104
104
|
const { options } = scene.descriptor;
|
|
105
|
-
const enabled =
|
|
105
|
+
const enabled = options.keyboardHandlingEnabled !== false;
|
|
106
|
+
const contentRef = React.useRef<React.ComponentRef<typeof View>>(null);
|
|
106
107
|
|
|
107
108
|
const { onPageChangeStart, onPageChangeCancel, onPageChangeConfirm } =
|
|
108
|
-
useKeyboardManager({ enabled, focused });
|
|
109
|
+
useKeyboardManager({ active, enabled, focused, contentRef });
|
|
109
110
|
|
|
110
111
|
const handleOpen = () => {
|
|
111
112
|
const { route } = scene.descriptor;
|
|
@@ -289,7 +290,7 @@ function CardContainerInner({
|
|
|
289
290
|
cardStyle,
|
|
290
291
|
]}
|
|
291
292
|
>
|
|
292
|
-
<View style={styles.container}>
|
|
293
|
+
<View ref={contentRef} collapsable={false} style={styles.container}>
|
|
293
294
|
<ModalPresentationContext.Provider value={modal}>
|
|
294
295
|
{headerMode !== 'float'
|
|
295
296
|
? renderHeader({
|
|
@@ -55,7 +55,7 @@ export function CardContent({ enabled, layout, style, ...rest }: Props) {
|
|
|
55
55
|
document.head.appendChild(style);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// eslint-disable-next-line @eslint-react/web-api
|
|
58
|
+
// eslint-disable-next-line @eslint-react/web-api-no-leaked-event-listener
|
|
59
59
|
window.addEventListener('resize', updateStyle);
|
|
60
60
|
|
|
61
61
|
unsubscribe = () => {
|
|
@@ -67,7 +67,7 @@ export function CardContent({ enabled, layout, style, ...rest }: Props) {
|
|
|
67
67
|
document.getElementById(id)?.remove();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// eslint-disable-next-line @eslint-react/
|
|
70
|
+
// eslint-disable-next-line @eslint-react/set-state-in-effect
|
|
71
71
|
setFill(width === layout.width && height === layout.height);
|
|
72
72
|
|
|
73
73
|
return unsubscribe;
|
|
@@ -90,7 +90,7 @@ type State = {
|
|
|
90
90
|
scenes: (Scene & { __memo: unknown[] })[];
|
|
91
91
|
gestures: GestureValues;
|
|
92
92
|
layout: Layout;
|
|
93
|
-
activeStates: (0 | 1 | Animated.AnimatedInterpolation<0 | 1>)[];
|
|
93
|
+
activeStates: (0 | 1 | 2 | Animated.AnimatedInterpolation<0 | 1 | 2>)[];
|
|
94
94
|
headerHeights: Record<string, number>;
|
|
95
95
|
};
|
|
96
96
|
|
|
@@ -178,10 +178,10 @@ const getHeaderHeights = (
|
|
|
178
178
|
headerStyle,
|
|
179
179
|
} = curr.descriptor.options;
|
|
180
180
|
|
|
181
|
-
const style = StyleSheet.flatten(headerStyle
|
|
181
|
+
const style = StyleSheet.flatten(headerStyle);
|
|
182
182
|
|
|
183
183
|
const height =
|
|
184
|
-
'height' in style && typeof style.height === 'number'
|
|
184
|
+
style != null && 'height' in style && typeof style.height === 'number'
|
|
185
185
|
? style.height
|
|
186
186
|
: previous[curr.route.key];
|
|
187
187
|
|
|
@@ -287,7 +287,7 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
287
287
|
new Animated.Value(
|
|
288
288
|
(props.openingRouteKeys.includes(curr.key) &&
|
|
289
289
|
getAnimationEnabled(animation)) ||
|
|
290
|
-
|
|
290
|
+
props.state.preloadedRoutes.includes(curr)
|
|
291
291
|
? getDistanceFromOptions(
|
|
292
292
|
state.layout,
|
|
293
293
|
descriptor?.options,
|
|
@@ -383,9 +383,10 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
383
383
|
? transitionPreset.cardStyleInterpolator
|
|
384
384
|
: forNoAnimationCard,
|
|
385
385
|
headerStyleInterpolator = transitionPreset.headerStyleInterpolator,
|
|
386
|
-
cardOverlayEnabled =
|
|
387
|
-
|
|
388
|
-
|
|
386
|
+
cardOverlayEnabled = descriptor.options.presentation !==
|
|
387
|
+
'transparentModal' &&
|
|
388
|
+
(Platform.OS !== 'ios' ||
|
|
389
|
+
getIsModalPresentation(cardStyleInterpolator)),
|
|
389
390
|
} = optionsForTransitionConfig;
|
|
390
391
|
|
|
391
392
|
const headerMode: StackHeaderMode =
|
|
@@ -471,7 +472,10 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
471
472
|
|
|
472
473
|
let activeStates = state.activeStates;
|
|
473
474
|
|
|
474
|
-
if (
|
|
475
|
+
if (
|
|
476
|
+
props.routes.length !== state.routes.length ||
|
|
477
|
+
props.routes.some((route, i) => route.key !== state.routes[i].key)
|
|
478
|
+
) {
|
|
475
479
|
let activeScreensLimit = 1;
|
|
476
480
|
|
|
477
481
|
for (let i = props.routes.length - 1; i >= 0; i--) {
|
|
@@ -517,12 +521,31 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
517
521
|
| 1
|
|
518
522
|
| 2;
|
|
519
523
|
|
|
520
|
-
const lastActiveState = state.activeStates[index];
|
|
521
524
|
const activeAfterTransition = index >= self.length - activeScreensLimit;
|
|
522
525
|
|
|
523
|
-
if (
|
|
524
|
-
|
|
526
|
+
if (
|
|
527
|
+
index < self.length - activeScreensLimit - 1 &&
|
|
528
|
+
!props.openingRouteKeys.includes(self[index + 1].key)
|
|
529
|
+
) {
|
|
530
|
+
// screen should be inactive because it is too deep in the stack
|
|
531
|
+
// activating it during the transition makes the screen container re-attach
|
|
532
|
+
// all screens above it to keep the order, which can show a blank frame on Android
|
|
525
533
|
activityState = STATE_INACTIVE;
|
|
534
|
+
} else if (index < self.length - activeScreensLimit - 1) {
|
|
535
|
+
// When a card is pushed before the previous one finishes opening,
|
|
536
|
+
// the screen below the previous card is still visible
|
|
537
|
+
// So it needs to stay active until the card above it finishes opening
|
|
538
|
+
activityState = scenes[index + 1].progress.current.interpolate({
|
|
539
|
+
inputRange: [0, 1 - EPSILON, 1],
|
|
540
|
+
outputRange: [1, 1, STATE_INACTIVE],
|
|
541
|
+
extrapolate: 'clamp',
|
|
542
|
+
});
|
|
543
|
+
} else if (Platform.OS === 'android' && index === self.length - 1) {
|
|
544
|
+
// On Android, the screen container switches all screens to hardware layers
|
|
545
|
+
// when no screen is on top, i.e. during the transition
|
|
546
|
+
// Doing this at the start of a transition can show a blank frame
|
|
547
|
+
// So we keep the topmost screen on top during the transition
|
|
548
|
+
activityState = STATE_ON_TOP;
|
|
526
549
|
} else {
|
|
527
550
|
const sceneForActivity = scenes[self.length - 1];
|
|
528
551
|
const outputValue =
|