@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.
Files changed (52) hide show
  1. package/lib/module/utils/gestureActivationCriteria.js +10 -8
  2. package/lib/module/utils/gestureActivationCriteria.js.map +1 -1
  3. package/lib/module/utils/useKeyboardManager.js +103 -9
  4. package/lib/module/utils/useKeyboardManager.js.map +1 -1
  5. package/lib/module/utils/viewIsDescendantOf.js +23 -0
  6. package/lib/module/utils/viewIsDescendantOf.js.map +1 -0
  7. package/lib/module/views/GestureHandler.js.map +1 -1
  8. package/lib/module/views/Header/Header.js +1 -1
  9. package/lib/module/views/InteractionManager.js +1 -0
  10. package/lib/module/views/InteractionManager.js.map +1 -1
  11. package/lib/module/views/Stack/Card.js +26 -15
  12. package/lib/module/views/Stack/Card.js.map +1 -1
  13. package/lib/module/views/Stack/CardContainer.js +7 -2
  14. package/lib/module/views/Stack/CardContainer.js.map +1 -1
  15. package/lib/module/views/Stack/CardContent.js +2 -2
  16. package/lib/module/views/Stack/CardStack.js +23 -7
  17. package/lib/module/views/Stack/CardStack.js.map +1 -1
  18. package/lib/module/views/Stack/StackView.js +37 -28
  19. package/lib/module/views/Stack/StackView.js.map +1 -1
  20. package/lib/typescript/src/types.d.ts +2 -2
  21. package/lib/typescript/src/types.d.ts.map +1 -1
  22. package/lib/typescript/src/utils/conditional.d.ts +1 -1
  23. package/lib/typescript/src/utils/conditional.d.ts.map +1 -1
  24. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts +2 -49
  25. package/lib/typescript/src/utils/gestureActivationCriteria.d.ts.map +1 -1
  26. package/lib/typescript/src/utils/useKeyboardManager.d.ts +5 -1
  27. package/lib/typescript/src/utils/useKeyboardManager.d.ts.map +1 -1
  28. package/lib/typescript/src/utils/viewIsDescendantOf.d.ts +3 -0
  29. package/lib/typescript/src/utils/viewIsDescendantOf.d.ts.map +1 -0
  30. package/lib/typescript/src/views/GestureHandler.d.ts.map +1 -1
  31. package/lib/typescript/src/views/InteractionManager.d.ts.map +1 -1
  32. package/lib/typescript/src/views/Screens.d.ts +1 -1
  33. package/lib/typescript/src/views/Screens.d.ts.map +1 -1
  34. package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
  35. package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
  36. package/lib/typescript/src/views/Stack/CardStack.d.ts +1 -1
  37. package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
  38. package/lib/typescript/src/views/Stack/StackView.d.ts.map +1 -1
  39. package/package.json +15 -15
  40. package/src/types.tsx +16 -2
  41. package/src/utils/gestureActivationCriteria.tsx +13 -9
  42. package/src/utils/useKeyboardManager.tsx +144 -8
  43. package/src/utils/viewIsDescendantOf.tsx +33 -0
  44. package/src/views/GestureHandler.tsx +1 -1
  45. package/src/views/Header/Header.tsx +1 -1
  46. package/src/views/InteractionManager.tsx +1 -0
  47. package/src/views/Screens.tsx +1 -1
  48. package/src/views/Stack/Card.tsx +30 -12
  49. package/src/views/Stack/CardContainer.tsx +4 -3
  50. package/src/views/Stack/CardContent.tsx +2 -2
  51. package/src/views/Stack/CardStack.tsx +34 -11
  52. 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,EAC1B,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;;;;;;;;;;;;;;;IAqQxB,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;CAoDP"}
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.0",
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.44",
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.0.0",
54
- "@react-navigation/native": "^7.5.0",
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.2.4",
58
- "del-cli": "^6.0.0",
59
- "react": "19.2.3",
60
- "react-native": "0.85.3",
61
- "react-native-builder-bob": "^0.42.1",
62
- "react-native-gesture-handler": "~2.31.1",
63
- "react-native-safe-area-context": "~5.10.0",
64
- "react-native-screens": "^4.26.2",
65
- "react-test-renderer": "19.2.3",
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.0",
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": "47f787dc7580c092238e17af8a86cad32573bd32"
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' | keyof Animated.AnimationConfig
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' | keyof Animated.AnimationConfig
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
- maxDeltaX: 15,
36
- minOffsetY: 5,
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
- maxDeltaX: 15,
43
- minOffsetY: -5,
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
- minOffsetX: 5,
57
- maxDeltaY: 20,
60
+ activeOffsetX: 5,
61
+ failOffsetY,
58
62
  hitSlop: { right: hitSlop },
59
63
  enableTrackpadTwoFingerGesture,
60
64
  };
61
65
  } else {
62
66
  return {
63
- minOffsetX: -5,
64
- maxDeltaY: 20,
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
- if (enabledRef.current && !focused) {
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
- }, [focused]);
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-hooks/exhaustive-deps
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
@@ -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;
@@ -84,7 +84,11 @@ const hasOpacityStyle = (
84
84
  if (style) {
85
85
  const flattenedStyle = StyleSheet.flatten(style);
86
86
 
87
- return 'opacity' in flattenedStyle && flattenedStyle.opacity != null;
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 timeoutRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
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
- clearTimeout(timeoutRef.current);
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
- // Wrap in setTimeout to ensure animation starts after
430
- // rending of the screen is done. This is especially important
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
- clearTimeout(timeoutRef.current);
447
+ if (initialAnimationHandleRef.current !== undefined) {
448
+ cancelAnimationFrame(initialAnimationHandleRef.current);
449
+ }
434
450
 
435
- timeoutRef.current = setTimeout(() => {
436
- didInitiallyAnimate.current = true;
437
- animate({ closing });
438
- }, 0);
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 { backgroundColor } = StyleSheet.flatten(contentStyle || {});
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 = focused && options.keyboardHandlingEnabled !== false;
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/no-leaked-event-listener
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/hooks-extra/no-direct-set-state-in-use-effect
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
- props.state.preloadedRoutes.includes(curr)
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 = (Platform.OS !== 'ios' &&
387
- optionsForTransitionConfig.presentation !== 'transparentModal') ||
388
- getIsModalPresentation(cardStyleInterpolator),
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 (props.routes.length !== state.routes.length) {
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 (lastActiveState === STATE_INACTIVE && !activeAfterTransition) {
524
- // screen was inactive before and it will still be inactive after the transition
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 =