@react-navigation/native-stack 8.0.0-alpha.2 → 8.0.0-alpha.21

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.
@@ -6,7 +6,7 @@ export const AnimatedHeaderHeightContext = React.createContext<
6
6
  >(undefined);
7
7
 
8
8
  export function useAnimatedHeaderHeight() {
9
- const animatedValue = React.useContext(AnimatedHeaderHeightContext);
9
+ const animatedValue = React.use(AnimatedHeaderHeightContext);
10
10
 
11
11
  if (animatedValue === undefined) {
12
12
  throw new Error(
@@ -6,7 +6,10 @@ import {
6
6
  HeaderShownContext,
7
7
  useFrameSize,
8
8
  } from '@react-navigation/elements';
9
- import { SafeAreaProviderCompat } from '@react-navigation/elements/internal';
9
+ import {
10
+ ActivityView,
11
+ SafeAreaProviderCompat,
12
+ } from '@react-navigation/elements/internal';
10
13
  import {
11
14
  NavigationProvider,
12
15
  type ParamListBase,
@@ -44,19 +47,16 @@ import { useHeaderConfigProps } from './useHeaderConfigProps';
44
47
 
45
48
  const ANDROID_DEFAULT_HEADER_HEIGHT = 56;
46
49
 
47
- function isFabric() {
48
- return 'nativeFabricUIManager' in global;
49
- }
50
-
51
50
  type SceneViewProps = {
52
51
  index: number;
53
52
  focused: boolean;
54
- shouldFreeze: boolean;
55
53
  descriptor: NativeStackDescriptor;
56
- previousDescriptor?: NativeStackDescriptor;
57
- nextDescriptor?: NativeStackDescriptor;
58
- isPresentationModal?: boolean;
59
- isPreloaded?: boolean;
54
+ previousDescriptor?: NativeStackDescriptor | undefined;
55
+ nextDescriptor?: NativeStackDescriptor | undefined;
56
+ isPresentationModal: boolean;
57
+ isNextScreenTransparent: boolean;
58
+ isPreloaded: boolean;
59
+ isBeforeLast: boolean;
60
60
  onWillDisappear: () => void;
61
61
  onWillAppear: () => void;
62
62
  onAppear: () => void;
@@ -70,14 +70,20 @@ type SceneViewProps = {
70
70
 
71
71
  const useNativeDriver = Platform.OS !== 'web';
72
72
 
73
+ const TRANSPARENT_PRESENTATIONS = [
74
+ 'transparentModal',
75
+ 'containedTransparentModal',
76
+ ];
77
+
73
78
  const SceneView = ({
74
79
  index,
75
80
  focused,
76
- shouldFreeze,
77
81
  descriptor,
78
82
  previousDescriptor,
79
83
  isPresentationModal,
84
+ isNextScreenTransparent,
80
85
  isPreloaded,
86
+ isBeforeLast,
81
87
  onWillDisappear,
82
88
  onWillAppear,
83
89
  onAppear,
@@ -91,6 +97,7 @@ const SceneView = ({
91
97
  const { route, navigation, options, render } = descriptor;
92
98
 
93
99
  const {
100
+ inactiveBehavior = 'pause',
94
101
  animation,
95
102
  animationDuration,
96
103
  animationMatchesGesture,
@@ -116,12 +123,13 @@ const SceneView = ({
116
123
  sheetElevation = 24,
117
124
  sheetExpandsWhenScrolledToEdge = true,
118
125
  sheetInitialDetentIndex = 0,
126
+ sheetShouldOverflowTopInset = false,
127
+ sheetResizeAnimationEnabled = true,
119
128
  statusBarAnimation,
120
129
  statusBarHidden,
121
130
  statusBarStyle,
122
131
  unstable_sheetFooter,
123
132
  scrollEdgeEffects,
124
- freezeOnBlur,
125
133
  contentStyle,
126
134
  } = options;
127
135
 
@@ -145,9 +153,9 @@ const SceneView = ({
145
153
  // Modals are fullscreen in landscape only on iPhone
146
154
  const isIPhone = Platform.OS === 'ios' && !(Platform.isPad || Platform.isTV);
147
155
 
148
- const isParentHeaderShown = React.useContext(HeaderShownContext);
149
- const parentHeaderHeight = React.useContext(HeaderHeightContext);
150
- const parentHeaderBack = React.useContext(HeaderBackContext);
156
+ const isParentHeaderShown = React.use(HeaderShownContext);
157
+ const parentHeaderHeight = React.use(HeaderHeightContext);
158
+ const parentHeaderBack = React.use(HeaderBackContext);
151
159
 
152
160
  const isLandscape = useFrameSize((frame) => frame.width > frame.height);
153
161
 
@@ -263,7 +271,8 @@ const SceneView = ({
263
271
  if (
264
272
  Platform.OS === 'android' &&
265
273
  headerHeight !== 0 &&
266
- headerHeight <= ANDROID_DEFAULT_HEADER_HEIGHT
274
+ // On some devices, height maybe slightly off (e.g. 56.17 instead of 56)
275
+ Math.round(headerHeight) <= ANDROID_DEFAULT_HEADER_HEIGHT
267
276
  ) {
268
277
  // FIXME: On Android, events may get delivered out-of-order
269
278
  // https://github.com/facebook/react-native/issues/54636
@@ -283,6 +292,71 @@ const SceneView = ({
283
292
  }
284
293
  );
285
294
 
295
+ const activityMode =
296
+ // Render focused screens normally
297
+ // Unpause preloaded screens so updates are visible
298
+ // This lets effects on preloaded screens run
299
+ // We don't need to handle inert as it'll be handled natively
300
+ inactiveBehavior === 'none' ||
301
+ focused ||
302
+ isPreloaded ||
303
+ isNextScreenTransparent
304
+ ? 'normal'
305
+ : inactiveBehavior === 'unmount' &&
306
+ !isBeforeLast &&
307
+ !('state' in route && route.state)
308
+ ? 'unmounted'
309
+ : 'paused';
310
+
311
+ const content = (
312
+ <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
313
+ <HeaderHeightContext.Provider
314
+ value={headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)}
315
+ >
316
+ {headerBackground != null ? (
317
+ /**
318
+ * To show a custom header background, we render it at the top of the screen below the header
319
+ * The header also needs to be positioned absolutely (with `translucent` style)
320
+ */
321
+ <View
322
+ style={[
323
+ styles.background,
324
+ headerTransparent ? styles.translucent : null,
325
+ { height: headerHeight },
326
+ ]}
327
+ >
328
+ {headerBackground()}
329
+ </View>
330
+ ) : null}
331
+ {header != null && headerShown !== false ? (
332
+ <View
333
+ onLayout={(e) => {
334
+ const headerHeight = e.nativeEvent.layout.height;
335
+
336
+ animatedHeaderHeight.setValue(headerHeight);
337
+ setHeaderHeight(headerHeight);
338
+ }}
339
+ style={[styles.header, headerTransparent ? styles.absolute : null]}
340
+ >
341
+ {header({
342
+ back: headerBack,
343
+ options,
344
+ route,
345
+ navigation,
346
+ })}
347
+ </View>
348
+ ) : null}
349
+ <HeaderShownContext.Provider
350
+ value={isParentHeaderShown || headerShown !== false}
351
+ >
352
+ <HeaderBackContext.Provider value={headerBack}>
353
+ {render()}
354
+ </HeaderBackContext.Provider>
355
+ </HeaderShownContext.Provider>
356
+ </HeaderHeightContext.Provider>
357
+ </AnimatedHeaderHeightContext.Provider>
358
+ );
359
+
286
360
  return (
287
361
  <NavigationProvider navigation={navigation} route={route}>
288
362
  <ScreenStackItem
@@ -294,7 +368,6 @@ const SceneView = ({
294
368
  customAnimationOnSwipe={animationMatchesGesture}
295
369
  fullScreenSwipeEnabled={fullScreenGestureEnabled}
296
370
  fullScreenSwipeShadowEnabled={fullScreenGestureShadowEnabled}
297
- freezeOnBlur={freezeOnBlur}
298
371
  gestureEnabled={
299
372
  Platform.OS === 'android'
300
373
  ? // This prop enables handling of system back gestures on Android
@@ -316,6 +389,8 @@ const SceneView = ({
316
389
  sheetCornerRadius={sheetCornerRadius}
317
390
  sheetElevation={sheetElevation}
318
391
  sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge}
392
+ sheetShouldOverflowTopInset={sheetShouldOverflowTopInset}
393
+ sheetDefaultResizeAnimationEnabled={sheetResizeAnimationEnabled}
319
394
  statusBarAnimation={statusBarAnimation}
320
395
  statusBarHidden={statusBarHidden}
321
396
  statusBarStyle={statusBarStyle}
@@ -349,62 +424,21 @@ const SceneView = ({
349
424
  ]}
350
425
  headerConfig={headerConfig}
351
426
  unstable_sheetFooter={unstable_sheetFooter}
352
- // When ts-expect-error is added, it affects all the props below it
353
- // So we keep any props that need it at the end
354
- // Otherwise invalid props may not be caught by TypeScript
355
- shouldFreeze={shouldFreeze}
356
427
  >
357
- <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
358
- <HeaderHeightContext.Provider
359
- value={
360
- headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)
428
+ {activityMode === 'unmounted' ? null : (
429
+ <ActivityView
430
+ mode={activityMode}
431
+ visible={
432
+ // We don't need to hide the content since it's handled natively
433
+ // Hiding may also cause flash due to lag after native tab switch
434
+ // So we leave it always visible
435
+ true
361
436
  }
437
+ style={StyleSheet.absoluteFill}
362
438
  >
363
- {headerBackground != null ? (
364
- /**
365
- * To show a custom header background, we render it at the top of the screen below the header
366
- * The header also needs to be positioned absolutely (with `translucent` style)
367
- */
368
- <View
369
- style={[
370
- styles.background,
371
- headerTransparent ? styles.translucent : null,
372
- { height: headerHeight },
373
- ]}
374
- >
375
- {headerBackground()}
376
- </View>
377
- ) : null}
378
- {header != null && headerShown !== false ? (
379
- <View
380
- onLayout={(e) => {
381
- const headerHeight = e.nativeEvent.layout.height;
382
-
383
- animatedHeaderHeight.setValue(headerHeight);
384
- setHeaderHeight(headerHeight);
385
- }}
386
- style={[
387
- styles.header,
388
- headerTransparent ? styles.absolute : null,
389
- ]}
390
- >
391
- {header({
392
- back: headerBack,
393
- options,
394
- route,
395
- navigation,
396
- })}
397
- </View>
398
- ) : null}
399
- <HeaderShownContext.Provider
400
- value={isParentHeaderShown || headerShown !== false}
401
- >
402
- <HeaderBackContext.Provider value={headerBack}>
403
- {render()}
404
- </HeaderBackContext.Provider>
405
- </HeaderShownContext.Provider>
406
- </HeaderHeightContext.Provider>
407
- </AnimatedHeaderHeightContext.Provider>
439
+ {content}
440
+ </ActivityView>
441
+ )}
408
442
  </ScreenStackItem>
409
443
  </NavigationProvider>
410
444
  );
@@ -429,7 +463,6 @@ export function NativeStackView({ state, navigation, descriptors }: Props) {
429
463
  {state.routes.concat(state.preloadedRoutes).map((route, index) => {
430
464
  const descriptor = descriptors[route.key];
431
465
  const isFocused = state.index === index;
432
- const isBelowFocused = state.index - 1 === index;
433
466
  const previousKey = state.routes[index - 1]?.key;
434
467
  const nextKey = state.routes[index + 1]?.key;
435
468
  const previousDescriptor = previousKey
@@ -437,30 +470,30 @@ export function NativeStackView({ state, navigation, descriptors }: Props) {
437
470
  : undefined;
438
471
  const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
439
472
 
473
+ const nextPresentation = nextDescriptor?.options.presentation;
474
+
475
+ const isNextScreenTransparent =
476
+ nextPresentation != null &&
477
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation);
478
+
440
479
  const isModal = modalRouteKeys.includes(route.key);
441
- const isModalOnIos = isModal && Platform.OS === 'ios';
442
480
 
443
481
  const isPreloaded = state.preloadedRoutes.some(
444
482
  (r) => r.key === route.key
445
483
  );
446
484
 
447
- // On Fabric, when screen is frozen, animated and reanimated values are not updated
448
- // due to component being unmounted. To avoid this, we don't freeze the previous screen there
449
- const shouldFreeze = isFabric()
450
- ? !isPreloaded && !isFocused && !isBelowFocused && !isModalOnIos
451
- : !isPreloaded && !isFocused && !isModalOnIos;
452
-
453
485
  return (
454
486
  <SceneView
455
487
  key={route.key}
456
488
  index={index}
457
489
  focused={isFocused}
458
- shouldFreeze={shouldFreeze}
459
490
  descriptor={descriptor}
460
491
  previousDescriptor={previousDescriptor}
461
492
  nextDescriptor={nextDescriptor}
462
493
  isPresentationModal={isModal}
494
+ isNextScreenTransparent={isNextScreenTransparent}
463
495
  isPreloaded={isPreloaded}
496
+ isBeforeLast={index === state.routes.length - 2}
464
497
  onWillDisappear={() => {
465
498
  navigation.emit({
466
499
  type: 'transitionStart',
@@ -6,6 +6,7 @@ import {
6
6
  useHeaderHeight,
7
7
  } from '@react-navigation/elements';
8
8
  import {
9
+ ActivityView,
9
10
  SafeAreaProviderCompat,
10
11
  Screen,
11
12
  } from '@react-navigation/elements/internal';
@@ -15,7 +16,7 @@ import {
15
16
  useLinkBuilder,
16
17
  } from '@react-navigation/native';
17
18
  import * as React from 'react';
18
- import { Animated, Image, StyleSheet, View } from 'react-native';
19
+ import { Animated, StyleSheet, View } from 'react-native';
19
20
 
20
21
  import type {
21
22
  NativeStackDescriptorMap,
@@ -35,7 +36,7 @@ const TRANSPARENT_PRESENTATIONS = [
35
36
  ];
36
37
 
37
38
  export function NativeStackView({ state, descriptors }: Props) {
38
- const parentHeaderBack = React.useContext(HeaderBackContext);
39
+ const parentHeaderBack = React.use(HeaderBackContext);
39
40
  const { buildHref } = useLinkBuilder();
40
41
 
41
42
  return (
@@ -66,6 +67,7 @@ export function NativeStackView({ state, descriptors }: Props) {
66
67
  const canGoBack = headerBack != null;
67
68
 
68
69
  const {
70
+ inactiveBehavior = 'pause',
69
71
  header,
70
72
  headerShown,
71
73
  headerBackIcon,
@@ -79,11 +81,35 @@ export function NativeStackView({ state, descriptors }: Props) {
79
81
 
80
82
  const nextPresentation = nextDescriptor?.options.presentation;
81
83
 
84
+ const isNextScreenTransparent =
85
+ nextPresentation != null &&
86
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation);
87
+
82
88
  const isPreloaded = state.preloadedRoutes.some(
83
89
  (r) => r.key === route.key
84
90
  );
85
91
 
86
- return (
92
+ const isBeforeLast = i === state.routes.length - 2;
93
+
94
+ const activityMode =
95
+ // Render focused screens normally
96
+ isFocused
97
+ ? 'normal'
98
+ : // Unpause preloaded screens so updates are visible
99
+ // This lets effects on preloaded screens run
100
+ inactiveBehavior === 'none' ||
101
+ isPreloaded ||
102
+ isNextScreenTransparent
103
+ ? 'inert'
104
+ : inactiveBehavior === 'unmount' && !isBeforeLast && !route.state
105
+ ? 'unmounted'
106
+ : 'paused';
107
+
108
+ if (activityMode === 'unmounted') {
109
+ return null;
110
+ }
111
+
112
+ const content = (
87
113
  <Screen
88
114
  key={route.key}
89
115
  focused={isFocused}
@@ -117,18 +143,7 @@ export function NativeStackView({ state, descriptors }: Props) {
117
143
  {...rest}
118
144
  label={headerBackTitle ?? label}
119
145
  tintColor={tintColor}
120
- backImage={
121
- headerBackIcon !== undefined
122
- ? () => (
123
- <Image
124
- source={headerBackIcon.source}
125
- resizeMode="contain"
126
- tintColor={tintColor}
127
- style={styles.backImage}
128
- />
129
- )
130
- : undefined
131
- }
146
+ icon={headerBackIcon}
132
147
  onPress={navigation.goBack}
133
148
  />
134
149
  )
@@ -138,22 +153,13 @@ export function NativeStackView({ state, descriptors }: Props) {
138
153
  />
139
154
  )
140
155
  }
141
- style={[
142
- StyleSheet.absoluteFill,
143
- {
144
- display:
145
- (isFocused ||
146
- (nextPresentation != null &&
147
- TRANSPARENT_PRESENTATIONS.includes(nextPresentation))) &&
148
- !isPreloaded
149
- ? 'flex'
150
- : 'none',
151
- },
152
- presentation != null &&
156
+ style={{
157
+ ...StyleSheet.absoluteFill,
158
+ ...(presentation != null &&
153
159
  TRANSPARENT_PRESENTATIONS.includes(presentation)
154
160
  ? { backgroundColor: 'transparent' }
155
- : null,
156
- ]}
161
+ : null),
162
+ }}
157
163
  >
158
164
  <HeaderBackContext.Provider value={headerBack}>
159
165
  <AnimatedHeaderHeightProvider>
@@ -164,6 +170,17 @@ export function NativeStackView({ state, descriptors }: Props) {
164
170
  </HeaderBackContext.Provider>
165
171
  </Screen>
166
172
  );
173
+
174
+ return (
175
+ <ActivityView
176
+ key={route.key}
177
+ mode={activityMode}
178
+ visible={isFocused || isPreloaded || isNextScreenTransparent}
179
+ style={StyleSheet.absoluteFill}
180
+ >
181
+ {content}
182
+ </ActivityView>
183
+ );
167
184
  })}
168
185
  </SafeAreaProviderCompat>
169
186
  );
@@ -194,9 +211,4 @@ const styles = StyleSheet.create({
194
211
  contentContainer: {
195
212
  flex: 1,
196
213
  },
197
- backImage: {
198
- height: 24,
199
- width: 24,
200
- margin: 3,
201
- },
202
214
  });