@react-navigation/bottom-tabs 8.0.0-alpha.13 → 8.0.0-alpha.14

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.
@@ -1,5 +1,6 @@
1
1
  import {
2
- Lazy,
2
+ ActivityView,
3
+ Container,
3
4
  SafeAreaProviderCompat,
4
5
  } from '@react-navigation/elements/internal';
5
6
  import {
@@ -16,7 +17,6 @@ import {
16
17
  StyleSheet,
17
18
  type ViewStyle,
18
19
  } from 'react-native';
19
- import { Screen, ScreenContainer } from 'react-native-screens';
20
20
 
21
21
  import {
22
22
  FadeTransition,
@@ -44,11 +44,6 @@ type Props = BottomTabNavigationConfig & {
44
44
  descriptors: BottomTabDescriptorMap;
45
45
  };
46
46
 
47
- const EPSILON = 1e-5;
48
- const STATE_INACTIVE = 0;
49
- const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
50
- const STATE_ON_TOP = 2;
51
-
52
47
  const NAMED_TRANSITIONS_PRESETS = {
53
48
  fade: FadeTransition,
54
49
  shift: ShiftTransition,
@@ -82,22 +77,43 @@ export function BottomTabViewCustom({
82
77
  state,
83
78
  navigation,
84
79
  descriptors,
85
- detachInactiveScreens = Platform.OS === 'web' ||
86
- Platform.OS === 'android' ||
87
- Platform.OS === 'ios',
88
80
  }: Props) {
89
81
  const { routes } = state;
90
82
  const focusedRouteKey = routes[state.index].key;
91
83
 
92
- const previousRouteKeyRef = React.useRef(focusedRouteKey);
84
+ const [loaded, setLoaded] = React.useState([focusedRouteKey]);
85
+
86
+ if (!loaded.includes(focusedRouteKey)) {
87
+ setLoaded([...loaded, focusedRouteKey]);
88
+ }
89
+
90
+ const [lastUpdate, setLastUpdate] = React.useState<{
91
+ current: string;
92
+ previous?: string;
93
+ }>({
94
+ current: focusedRouteKey,
95
+ });
96
+
97
+ if (lastUpdate.current !== focusedRouteKey) {
98
+ setLastUpdate({
99
+ current: focusedRouteKey,
100
+ previous: lastUpdate.current,
101
+ });
102
+ }
103
+
93
104
  const tabAnims = useAnimatedHashMap(state);
94
105
 
106
+ const [isAnimating, setIsAnimating] = React.useState(false);
107
+
108
+ const previousRouteKeyRef = React.useRef(focusedRouteKey);
109
+
95
110
  React.useEffect(() => {
96
111
  const previousRouteKey = previousRouteKeyRef.current;
97
112
 
98
113
  let popToTopAction: NavigationAction | undefined;
99
114
 
100
115
  if (
116
+ previousRouteKey &&
101
117
  previousRouteKey !== focusedRouteKey &&
102
118
  descriptors[previousRouteKey]?.options.popToTopOnBlur
103
119
  ) {
@@ -105,6 +121,12 @@ export function BottomTabViewCustom({
105
121
  (route) => route.key === previousRouteKey
106
122
  );
107
123
 
124
+ console.log(
125
+ 'checking if we need to pop to top for route',
126
+ previousRouteKey,
127
+ focusedRouteKey
128
+ );
129
+
108
130
  if (prevRoute?.state?.type === 'stack' && prevRoute.state.key) {
109
131
  popToTopAction = {
110
132
  ...StackActions.popToTop(),
@@ -113,6 +135,8 @@ export function BottomTabViewCustom({
113
135
  }
114
136
  }
115
137
 
138
+ let timer: ReturnType<typeof setTimeout>;
139
+
116
140
  const animateToIndex = () => {
117
141
  if (previousRouteKey !== focusedRouteKey) {
118
142
  navigation.emit({
@@ -121,42 +145,45 @@ export function BottomTabViewCustom({
121
145
  });
122
146
  }
123
147
 
124
- Animated.parallel(
125
- state.routes
126
- .map((route, index) => {
127
- const { options } = descriptors[route.key];
128
- const {
129
- animation = 'none',
130
- transitionSpec = NAMED_TRANSITIONS_PRESETS[animation]
131
- .transitionSpec,
132
- } = options;
133
-
134
- let spec = transitionSpec;
135
-
136
- if (
137
- route.key !== previousRouteKey &&
138
- route.key !== focusedRouteKey
139
- ) {
140
- // Don't animate if the screen is not previous one or new one
141
- // This will avoid flicker for screens not involved in the transition
142
- spec = NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
143
- }
144
-
145
- spec = spec ?? NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
146
-
147
- const toValue =
148
- index === state.index ? 0 : index >= state.index ? 1 : -1;
149
-
150
- return Animated[spec.animation](tabAnims[route.key], {
151
- ...spec.config,
152
- toValue,
153
- useNativeDriver,
154
- });
155
- })
156
- .filter(Boolean) as Animated.CompositeAnimation[]
157
- ).start(({ finished }) => {
148
+ const animations = state.routes
149
+ .map((route, index) => {
150
+ const { options } = descriptors[route.key];
151
+ const {
152
+ animation = 'none',
153
+ transitionSpec = NAMED_TRANSITIONS_PRESETS[animation]
154
+ .transitionSpec,
155
+ } = options;
156
+
157
+ let spec = transitionSpec;
158
+
159
+ if (route.key !== previousRouteKey && route.key !== focusedRouteKey) {
160
+ // Don't animate if the screen is not previous one or new one
161
+ // This will avoid flicker for screens not involved in the transition
162
+ spec = NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
163
+ }
164
+
165
+ spec = spec ?? NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
166
+
167
+ const toValue =
168
+ index === state.index ? 0 : index >= state.index ? 1 : -1;
169
+
170
+ return Animated[spec.animation](tabAnims[route.key], {
171
+ ...spec.config,
172
+ toValue,
173
+ useNativeDriver,
174
+ });
175
+ })
176
+ .filter((anim) => anim != null);
177
+
178
+ if (animations.length) {
179
+ // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
180
+ setIsAnimating(true);
181
+ }
182
+
183
+ Animated.parallel(animations).start(({ finished }) => {
158
184
  if (finished && popToTopAction) {
159
- navigation.dispatch(popToTopAction);
185
+ console.log('dispatching pop to top action', popToTopAction);
186
+ // navigation.dispatch(popToTopAction);
160
187
  }
161
188
 
162
189
  if (previousRouteKey !== focusedRouteKey) {
@@ -165,12 +192,24 @@ export function BottomTabViewCustom({
165
192
  target: focusedRouteKey,
166
193
  });
167
194
  }
195
+
196
+ if (finished && animations.length) {
197
+ // Delay clearing `isAnimating`
198
+ // This will give time for `popToAction` to get handled before pause
199
+ timer = setTimeout(() => {
200
+ setIsAnimating(false);
201
+ }, 32);
202
+ }
168
203
  });
169
204
  };
170
205
 
171
206
  animateToIndex();
172
207
 
173
208
  previousRouteKeyRef.current = focusedRouteKey;
209
+
210
+ return () => {
211
+ clearTimeout(timer);
212
+ };
174
213
  }, [
175
214
  descriptors,
176
215
  focusedRouteKey,
@@ -203,11 +242,6 @@ export function BottomTabViewCustom({
203
242
  </BottomTabBarHeightCallbackContext.Provider>
204
243
  );
205
244
 
206
- // If there is no animation, we only have 2 states: visible and invisible
207
- const hasTwoStates = !routes.some((route) =>
208
- hasAnimation(descriptors[route.key].options)
209
- );
210
-
211
245
  const tabBarPosition = useTabBarPosition(
212
246
  descriptors[focusedRouteKey].options
213
247
  );
@@ -224,12 +258,7 @@ export function BottomTabViewCustom({
224
258
  {tabBarPosition === 'top' || tabBarPosition === 'left'
225
259
  ? tabBarElement
226
260
  : null}
227
- <ScreenContainer
228
- key="screens"
229
- enabled={detachInactiveScreens}
230
- hasTwoStates={hasTwoStates}
231
- style={styles.screens}
232
- >
261
+ <Container key="screens" style={styles.screens}>
233
262
  {routes.map((route, index) => {
234
263
  const descriptor = descriptors[route.key];
235
264
 
@@ -237,6 +266,7 @@ export function BottomTabViewCustom({
237
266
 
238
267
  const {
239
268
  lazy = true,
269
+ inactiveBehavior = 'pause',
240
270
  animation = 'none',
241
271
  sceneStyleInterpolator = NAMED_TRANSITIONS_PRESETS[animation]
242
272
  .sceneStyleInterpolator,
@@ -246,6 +276,16 @@ export function BottomTabViewCustom({
246
276
  const isFocused = state.index === index;
247
277
  const isPreloaded = state.preloadedRouteKeys.includes(route.key);
248
278
 
279
+ if (
280
+ lazy &&
281
+ !loaded.includes(route.key) &&
282
+ !isFocused &&
283
+ !isPreloaded
284
+ ) {
285
+ // Don't render a lazy screen if we've never navigated to it or it wasn't preloaded
286
+ return null;
287
+ }
288
+
249
289
  const animationEnabled = hasAnimation(descriptor.options);
250
290
 
251
291
  const content = (
@@ -254,75 +294,48 @@ export function BottomTabViewCustom({
254
294
  progress={tabAnims[route.key]}
255
295
  animationEnabled={animationEnabled}
256
296
  sceneStyleInterpolator={sceneStyleInterpolator}
257
- style={customSceneStyle}
297
+ style={[StyleSheet.absoluteFill, customSceneStyle]}
258
298
  >
259
- <Lazy enabled={lazy} visible={isFocused || isPreloaded}>
260
- <ScreenContent
261
- isFocused={isFocused}
262
- route={route}
263
- navigation={navigation}
264
- options={options}
265
- style={
266
- Platform.OS === 'web'
267
- ? {
268
- /**
269
- * Don't use react-native-screens on web:
270
- * - It applies display: none as fallback, which triggers `onLayout` events
271
- * - We still need to hide the view when screens is not enabled
272
- */
273
- ...StyleSheet.absoluteFillObject,
274
- visibility: isFocused ? 'visible' : 'hidden',
275
- }
276
- : undefined
277
- }
299
+ <ScreenContent
300
+ isFocused={isFocused}
301
+ route={route}
302
+ navigation={navigation}
303
+ options={options}
304
+ >
305
+ <BottomTabBarHeightContext.Provider
306
+ value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
278
307
  >
279
- <BottomTabBarHeightContext.Provider
280
- value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
281
- >
282
- {render()}
283
- </BottomTabBarHeightContext.Provider>
284
- </ScreenContent>
285
- </Lazy>
308
+ {render()}
309
+ </BottomTabBarHeightContext.Provider>
310
+ </ScreenContent>
286
311
  </AnimatedScreenContent>
287
312
  );
288
313
 
289
- if (Platform.OS === 'web') {
290
- return content;
291
- }
314
+ const isAnimatingRoute =
315
+ isAnimating &&
316
+ (lastUpdate.previous === route.key ||
317
+ lastUpdate.current === route.key);
292
318
 
293
- const activityState = isFocused
294
- ? STATE_ON_TOP // the screen is on top after the transition
295
- : animationEnabled // is animation is not enabled, immediately move to inactive state
296
- ? tabAnims[route.key].interpolate({
297
- inputRange: [0, 1 - EPSILON, 1],
298
- outputRange: [
299
- STATE_TRANSITIONING_OR_BELOW_TOP, // screen visible during transition
300
- STATE_TRANSITIONING_OR_BELOW_TOP,
301
- STATE_INACTIVE, // the screen is detached after transition
302
- ],
303
- extrapolate: 'extend',
304
- })
305
- : STATE_INACTIVE;
319
+ // For preloaded screens and if lazy is false,
320
+ // Keep them active so that the effects can run
321
+ const isActive =
322
+ inactiveBehavior === 'none' ||
323
+ isAnimatingRoute ||
324
+ isPreloaded ||
325
+ (lazy === false && !loaded.includes(route.key));
306
326
 
307
327
  return (
308
- <Screen
328
+ <ActivityView
309
329
  key={route.key}
310
- style={[
311
- StyleSheet.absoluteFill,
312
- {
313
- zIndex: isFocused ? 0 : -1,
314
- pointerEvents: isFocused ? 'auto' : 'none',
315
- },
316
- ]}
317
- activityState={activityState}
318
- enabled={detachInactiveScreens}
319
- shouldFreeze={activityState === STATE_INACTIVE && !isPreloaded}
330
+ mode={isFocused ? 'normal' : isActive ? 'inert' : 'paused'}
331
+ visible={isFocused || isAnimatingRoute}
332
+ style={{ ...StyleSheet.absoluteFill, zIndex: isFocused ? 0 : -1 }}
320
333
  >
321
334
  {content}
322
- </Screen>
335
+ </ActivityView>
323
336
  );
324
337
  })}
325
- </ScreenContainer>
338
+ </Container>
326
339
  {tabBarPosition === 'bottom' || tabBarPosition === 'right'
327
340
  ? tabBarElement
328
341
  : null}
@@ -339,7 +352,7 @@ function AnimatedScreenContent({
339
352
  }: {
340
353
  progress: Animated.Value;
341
354
  animationEnabled: boolean;
342
- sceneStyleInterpolator?: BottomTabSceneStyleInterpolator;
355
+ sceneStyleInterpolator?: BottomTabSceneStyleInterpolator | undefined;
343
356
  children: React.ReactNode;
344
357
  style: StyleProp<ViewStyle>;
345
358
  }) {
@@ -1,7 +1,7 @@
1
1
  import { getLabel } from '@react-navigation/elements';
2
2
  import {
3
+ ActivityView,
3
4
  Color,
4
- Lazy,
5
5
  SafeAreaProviderCompat,
6
6
  } from '@react-navigation/elements/internal';
7
7
  import {
@@ -20,6 +20,7 @@ import {
20
20
  type ColorValue,
21
21
  Platform,
22
22
  PlatformColor,
23
+ StyleSheet,
23
24
  } from 'react-native';
24
25
  import {
25
26
  type PlatformIcon,
@@ -62,6 +63,18 @@ export function BottomTabViewNative({
62
63
  const { dark, colors, fonts } = useTheme();
63
64
 
64
65
  const focusedRouteKey = state.routes[state.index].key;
66
+
67
+ const [loaded, setLoaded] = React.useState([focusedRouteKey]);
68
+
69
+ if (!loaded.includes(focusedRouteKey)) {
70
+ setLoaded([...loaded, focusedRouteKey]);
71
+ }
72
+
73
+ const [pendingNavigation, setPendingNavigation] = React.useState<{
74
+ from: string;
75
+ to: string;
76
+ } | null>(null);
77
+
65
78
  const previousRouteKeyRef = React.useRef(focusedRouteKey);
66
79
 
67
80
  React.useEffect(() => {
@@ -80,11 +93,26 @@ export function BottomTabViewNative({
80
93
  ...StackActions.popToTop(),
81
94
  target: prevRoute.state.key,
82
95
  };
96
+
83
97
  navigation.dispatch(popToTopAction);
84
98
  }
85
99
  }
86
100
 
87
101
  previousRouteKeyRef.current = focusedRouteKey;
102
+
103
+ // Delay clearing `isAnimating`
104
+ // This will give time for `popToAction` to get handled before pause
105
+ const timer = setTimeout(() => {
106
+ setPendingNavigation((pending) => {
107
+ if (pending?.to === focusedRouteKey) {
108
+ return null;
109
+ }
110
+
111
+ return pending;
112
+ });
113
+ }, 32);
114
+
115
+ return () => clearTimeout(timer);
88
116
  }, [descriptors, focusedRouteKey, navigation, state.index, state.routes]);
89
117
 
90
118
  const currentOptions = descriptors[state.routes[state.index].key]?.options;
@@ -265,6 +293,11 @@ export function BottomTabViewNative({
265
293
  state.routes.findIndex((r) => r.key === route.key);
266
294
 
267
295
  if (!isFocused) {
296
+ setPendingNavigation({
297
+ from: previousRouteKeyRef.current,
298
+ to: route.key,
299
+ });
300
+
268
301
  navigation.dispatch({
269
302
  ...CommonActions.navigate(route.name, route.params),
270
303
  target: state.key,
@@ -281,6 +314,7 @@ export function BottomTabViewNative({
281
314
  const {
282
315
  title,
283
316
  lazy = true,
317
+ inactiveBehavior = 'pause',
284
318
  tabBarLabel,
285
319
  tabBarBadgeStyle,
286
320
  tabBarIcon,
@@ -355,6 +389,15 @@ export function BottomTabViewNative({
355
389
  const icon = getIcon(false);
356
390
  const selectedIcon = getIcon(true);
357
391
 
392
+ // For preloaded screens and if lazy is false,
393
+ // Keep them active so that the effects can run
394
+ const isActive =
395
+ inactiveBehavior === 'none' ||
396
+ isPreloaded ||
397
+ pendingNavigation?.from === route.key ||
398
+ pendingNavigation?.to === route.key ||
399
+ (lazy === false && !loaded.includes(route.key));
400
+
358
401
  return (
359
402
  <Tabs.Screen
360
403
  onWillAppear={() => onTransitionStart({ route })}
@@ -390,6 +433,9 @@ export function BottomTabViewNative({
390
433
  : scrollEdgeEffects?.right,
391
434
  }}
392
435
  scrollEdgeAppearance={{
436
+ tabBarBackgroundColor,
437
+ tabBarShadowColor,
438
+ tabBarBlurEffect,
393
439
  stacked: {
394
440
  normal: tabItemAppearance,
395
441
  },
@@ -425,23 +471,38 @@ export function BottomTabViewNative({
425
471
  }
426
472
  experimental_userInterfaceStyle={dark ? 'dark' : 'light'}
427
473
  >
428
- <Lazy enabled={lazy} visible={isFocused || isPreloaded}>
429
- <ScreenContent
430
- isFocused={isFocused}
431
- route={route}
432
- navigation={navigation}
433
- options={options}
434
- style={sceneStyle}
474
+ {lazy &&
475
+ !loaded.includes(route.key) &&
476
+ !isFocused &&
477
+ !isPreloaded ? null : (
478
+ <ActivityView
479
+ key={route.key}
480
+ mode={isFocused ? 'normal' : isActive ? 'inert' : 'paused'}
481
+ visible={
482
+ // We don't need to hide the content since it's handled natively
483
+ // Hiding may also cause flash due to lag after native tab switch
484
+ // So we leave it always visible
485
+ true
486
+ }
487
+ style={StyleSheet.absoluteFill}
435
488
  >
436
- <AnimatedScreenContent isFocused={isFocused}>
437
- <BottomTabBarHeightContext.Provider value={0}>
438
- <NavigationMetaContext.Provider value={meta}>
439
- {render()}
440
- </NavigationMetaContext.Provider>
441
- </BottomTabBarHeightContext.Provider>
442
- </AnimatedScreenContent>
443
- </ScreenContent>
444
- </Lazy>
489
+ <ScreenContent
490
+ isFocused={isFocused}
491
+ route={route}
492
+ navigation={navigation}
493
+ options={options}
494
+ style={sceneStyle}
495
+ >
496
+ <AnimatedScreenContent isFocused={isFocused}>
497
+ <BottomTabBarHeightContext.Provider value={0}>
498
+ <NavigationMetaContext.Provider value={meta}>
499
+ {render()}
500
+ </NavigationMetaContext.Provider>
501
+ </BottomTabBarHeightContext.Provider>
502
+ </AnimatedScreenContent>
503
+ </ScreenContent>
504
+ </ActivityView>
505
+ )}
445
506
  </Tabs.Screen>
446
507
  );
447
508
  })}
@@ -18,8 +18,8 @@ export type TabBarIconProps = {
18
18
  route: Route<string>;
19
19
  variant: 'uikit' | 'material';
20
20
  size: 'compact' | 'regular';
21
- badge?: string | number;
22
- badgeStyle?: StyleProp<TextStyle>;
21
+ badge?: string | number | undefined;
22
+ badgeStyle?: StyleProp<TextStyle> | undefined;
23
23
  activeOpacity: number;
24
24
  inactiveOpacity: number;
25
25
  activeTintColor: ColorValue;
@@ -31,7 +31,7 @@ export type TabBarIconProps = {
31
31
  color: ColorValue;
32
32
  size: number;
33
33
  }) => BottomTabIcon | React.ReactNode);
34
- allowFontScaling?: boolean;
34
+ allowFontScaling?: boolean | undefined;
35
35
  style: StyleProp<ViewStyle>;
36
36
  };
37
37