@react-navigation/bottom-tabs 7.0.0-alpha.8 → 7.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/lib/commonjs/TransitionConfigs/SceneStyleInterpolators.js +11 -13
  2. package/lib/commonjs/TransitionConfigs/SceneStyleInterpolators.js.map +1 -1
  3. package/lib/commonjs/TransitionConfigs/TransitionPresets.js +6 -6
  4. package/lib/commonjs/TransitionConfigs/TransitionPresets.js.map +1 -1
  5. package/lib/commonjs/TransitionConfigs/TransitionSpecs.js +3 -3
  6. package/lib/commonjs/TransitionConfigs/TransitionSpecs.js.map +1 -1
  7. package/lib/commonjs/index.js +4 -4
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/navigators/createBottomTabNavigator.js +20 -17
  10. package/lib/commonjs/navigators/createBottomTabNavigator.js.map +1 -1
  11. package/lib/commonjs/types.js.map +1 -1
  12. package/lib/commonjs/utils/BottomTabBarHeightCallbackContext.js +1 -1
  13. package/lib/commonjs/utils/BottomTabBarHeightCallbackContext.js.map +1 -1
  14. package/lib/commonjs/utils/BottomTabBarHeightContext.js +1 -1
  15. package/lib/commonjs/utils/BottomTabBarHeightContext.js.map +1 -1
  16. package/lib/commonjs/utils/useAnimatedHashMap.js +8 -10
  17. package/lib/commonjs/utils/useAnimatedHashMap.js.map +1 -1
  18. package/lib/commonjs/utils/useBottomTabBarHeight.js +1 -1
  19. package/lib/commonjs/utils/useBottomTabBarHeight.js.map +1 -1
  20. package/lib/commonjs/utils/useIsKeyboardShown.js +1 -1
  21. package/lib/commonjs/utils/useIsKeyboardShown.js.map +1 -1
  22. package/lib/commonjs/views/Badge.js +13 -15
  23. package/lib/commonjs/views/Badge.js.map +1 -1
  24. package/lib/commonjs/views/BottomTabBar.js +81 -82
  25. package/lib/commonjs/views/BottomTabBar.js.map +1 -1
  26. package/lib/commonjs/views/BottomTabItem.js +69 -59
  27. package/lib/commonjs/views/BottomTabItem.js.map +1 -1
  28. package/lib/commonjs/views/BottomTabView.js +76 -50
  29. package/lib/commonjs/views/BottomTabView.js.map +1 -1
  30. package/lib/commonjs/views/ScreenFallback.js +11 -13
  31. package/lib/commonjs/views/ScreenFallback.js.map +1 -1
  32. package/lib/commonjs/views/TabBarIcon.js +14 -15
  33. package/lib/commonjs/views/TabBarIcon.js.map +1 -1
  34. package/lib/module/TransitionConfigs/SceneStyleInterpolators.js +9 -11
  35. package/lib/module/TransitionConfigs/SceneStyleInterpolators.js.map +1 -1
  36. package/lib/module/TransitionConfigs/TransitionPresets.js +7 -7
  37. package/lib/module/TransitionConfigs/TransitionPresets.js.map +1 -1
  38. package/lib/module/TransitionConfigs/TransitionSpecs.js +2 -2
  39. package/lib/module/TransitionConfigs/TransitionSpecs.js.map +1 -1
  40. package/lib/module/index.js +2 -2
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/navigators/createBottomTabNavigator.js +18 -15
  43. package/lib/module/navigators/createBottomTabNavigator.js.map +1 -1
  44. package/lib/module/types.js.map +1 -1
  45. package/lib/module/utils/BottomTabBarHeightCallbackContext.js.map +1 -1
  46. package/lib/module/utils/BottomTabBarHeightContext.js.map +1 -1
  47. package/lib/module/utils/useAnimatedHashMap.js +7 -9
  48. package/lib/module/utils/useAnimatedHashMap.js.map +1 -1
  49. package/lib/module/utils/useBottomTabBarHeight.js.map +1 -1
  50. package/lib/module/utils/useIsKeyboardShown.js.map +1 -1
  51. package/lib/module/views/Badge.js +11 -13
  52. package/lib/module/views/Badge.js.map +1 -1
  53. package/lib/module/views/BottomTabBar.js +82 -83
  54. package/lib/module/views/BottomTabBar.js.map +1 -1
  55. package/lib/module/views/BottomTabItem.js +68 -58
  56. package/lib/module/views/BottomTabItem.js.map +1 -1
  57. package/lib/module/views/BottomTabView.js +75 -49
  58. package/lib/module/views/BottomTabView.js.map +1 -1
  59. package/lib/module/views/ScreenFallback.js +10 -12
  60. package/lib/module/views/ScreenFallback.js.map +1 -1
  61. package/lib/module/views/TabBarIcon.js +13 -14
  62. package/lib/module/views/TabBarIcon.js.map +1 -1
  63. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts +2 -2
  64. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts.map +1 -1
  65. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +1 -1
  66. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -1
  67. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +2 -2
  68. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -1
  69. package/lib/typescript/src/index.d.ts +3 -3
  70. package/lib/typescript/src/index.d.ts.map +1 -1
  71. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts +14 -9
  72. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
  73. package/lib/typescript/src/types.d.ts +25 -12
  74. package/lib/typescript/src/types.d.ts.map +1 -1
  75. package/lib/typescript/src/views/BottomTabBar.d.ts +1 -5
  76. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  77. package/lib/typescript/src/views/BottomTabItem.d.ts +7 -1
  78. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  79. package/lib/typescript/src/views/BottomTabView.d.ts +1 -1
  80. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -1
  81. package/lib/typescript/src/views/ScreenFallback.d.ts.map +1 -1
  82. package/lib/typescript/src/views/TabBarIcon.d.ts +1 -1
  83. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  84. package/package.json +17 -17
  85. package/src/TransitionConfigs/SceneStyleInterpolators.tsx +5 -5
  86. package/src/TransitionConfigs/TransitionPresets.tsx +7 -7
  87. package/src/TransitionConfigs/TransitionSpecs.tsx +2 -2
  88. package/src/index.tsx +3 -2
  89. package/src/navigators/createBottomTabNavigator.tsx +33 -7
  90. package/src/types.tsx +31 -11
  91. package/src/views/BottomTabBar.tsx +74 -58
  92. package/src/views/BottomTabItem.tsx +27 -3
  93. package/src/views/BottomTabView.tsx +116 -55
  94. package/src/views/TabBarIcon.tsx +1 -1
@@ -10,6 +10,7 @@ import {
10
10
  type ParamListBase,
11
11
  type TabNavigationState,
12
12
  useLinkBuilder,
13
+ useLocale,
13
14
  useTheme,
14
15
  } from '@react-navigation/native';
15
16
  import Color from 'color';
@@ -20,11 +21,13 @@ import {
20
21
  Platform,
21
22
  type StyleProp,
22
23
  StyleSheet,
23
- useWindowDimensions,
24
24
  View,
25
25
  type ViewStyle,
26
26
  } from 'react-native';
27
- import type { EdgeInsets } from 'react-native-safe-area-context';
27
+ import {
28
+ type EdgeInsets,
29
+ useSafeAreaFrame,
30
+ } from 'react-native-safe-area-context';
28
31
 
29
32
  import type { BottomTabBarProps, BottomTabDescriptorMap } from '../types';
30
33
  import { BottomTabBarHeightCallbackContext } from '../utils/BottomTabBarHeightCallbackContext';
@@ -45,14 +48,12 @@ const useNativeDriver = Platform.OS !== 'web';
45
48
  type Options = {
46
49
  state: TabNavigationState<ParamListBase>;
47
50
  descriptors: BottomTabDescriptorMap;
48
- layout: { height: number; width: number };
49
51
  dimensions: { height: number; width: number };
50
52
  };
51
53
 
52
54
  const shouldUseHorizontalLabels = ({
53
55
  state,
54
56
  descriptors,
55
- layout,
56
57
  dimensions,
57
58
  }: Options) => {
58
59
  const { tabBarLabelPosition } =
@@ -67,7 +68,7 @@ const shouldUseHorizontalLabels = ({
67
68
  }
68
69
  }
69
70
 
70
- if (layout.width >= 768) {
71
+ if (dimensions.width >= 768) {
71
72
  // Screen size matches a tablet
72
73
  const maxTabWidth = state.routes.reduce((acc, route) => {
73
74
  const { tabBarItemStyle } = descriptors[route.key].options;
@@ -84,7 +85,7 @@ const shouldUseHorizontalLabels = ({
84
85
  return acc + DEFAULT_MAX_TAB_ITEM_WIDTH;
85
86
  }, 0);
86
87
 
87
- return maxTabWidth <= layout.width;
88
+ return maxTabWidth <= dimensions.width;
88
89
  } else {
89
90
  return dimensions.width > dimensions.height;
90
91
  }
@@ -143,6 +144,7 @@ export function BottomTabBar({
143
144
  style,
144
145
  }: Props) {
145
146
  const { colors } = useTheme();
147
+ const { direction } = useLocale();
146
148
  const { buildHref } = useLinkBuilder();
147
149
 
148
150
  const focusedRoute = state.routes[state.index];
@@ -158,7 +160,8 @@ export function BottomTabBar({
158
160
  tabBarBackground,
159
161
  tabBarActiveTintColor,
160
162
  tabBarInactiveTintColor,
161
- tabBarActiveBackgroundColor = tabBarPosition !== 'bottom'
163
+ tabBarActiveBackgroundColor = tabBarPosition !== 'bottom' &&
164
+ tabBarPosition !== 'top'
162
165
  ? Color(tabBarActiveTintColor ?? colors.primary)
163
166
  .alpha(0.12)
164
167
  .rgb()
@@ -167,8 +170,7 @@ export function BottomTabBar({
167
170
  tabBarInactiveBackgroundColor,
168
171
  } = focusedOptions;
169
172
 
170
- // FIXME: useSafeAreaFrame doesn't update values when window is resized on Web
171
- const dimensions = useWindowDimensions();
173
+ const dimensions = useSafeAreaFrame();
172
174
  const isKeyboardShown = useIsKeyboardShown();
173
175
 
174
176
  const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext);
@@ -229,22 +231,18 @@ export function BottomTabBar({
229
231
 
230
232
  const [layout, setLayout] = React.useState({
231
233
  height: 0,
232
- width: dimensions.width,
233
234
  });
234
235
 
235
236
  const handleLayout = (e: LayoutChangeEvent) => {
236
- const { height, width } = e.nativeEvent.layout;
237
+ const { height } = e.nativeEvent.layout;
237
238
 
238
239
  onHeightChange?.(height);
239
240
 
240
241
  setLayout((layout) => {
241
- if (height === layout.height && width === layout.width) {
242
+ if (height === layout.height) {
242
243
  return layout;
243
244
  } else {
244
- return {
245
- height,
246
- width,
247
- };
245
+ return { height };
248
246
  }
249
247
  });
250
248
  };
@@ -257,7 +255,6 @@ export function BottomTabBar({
257
255
  descriptors,
258
256
  insets,
259
257
  dimensions,
260
- layout,
261
258
  style: [tabBarStyle, style],
262
259
  });
263
260
 
@@ -265,26 +262,55 @@ export function BottomTabBar({
265
262
  state,
266
263
  descriptors,
267
264
  dimensions,
268
- layout,
269
265
  });
270
266
 
267
+ const isSidebar = tabBarPosition === 'left' || tabBarPosition === 'right';
268
+
271
269
  const tabBarBackgroundElement = tabBarBackground?.();
272
270
 
273
271
  return (
274
272
  <Animated.View
275
273
  style={[
276
274
  tabBarPosition === 'left'
277
- ? styles.left
275
+ ? styles.start
278
276
  : tabBarPosition === 'right'
279
- ? styles.right
280
- : styles.bottom,
277
+ ? styles.end
278
+ : styles.bottom,
279
+ (
280
+ Platform.OS === 'web'
281
+ ? tabBarPosition === 'right'
282
+ : (direction === 'rtl' && tabBarPosition === 'left') ||
283
+ (direction !== 'rtl' && tabBarPosition === 'right')
284
+ )
285
+ ? { borderLeftWidth: StyleSheet.hairlineWidth }
286
+ : (
287
+ Platform.OS === 'web'
288
+ ? tabBarPosition === 'left'
289
+ : (direction === 'rtl' && tabBarPosition === 'right') ||
290
+ (direction !== 'rtl' && tabBarPosition === 'left')
291
+ )
292
+ ? { borderRightWidth: StyleSheet.hairlineWidth }
293
+ : tabBarPosition === 'top'
294
+ ? { borderBottomWidth: StyleSheet.hairlineWidth }
295
+ : { borderTopWidth: StyleSheet.hairlineWidth },
281
296
  {
282
297
  backgroundColor:
283
298
  tabBarBackgroundElement != null ? 'transparent' : colors.card,
284
299
  borderColor: colors.border,
285
300
  },
286
- tabBarPosition === 'bottom'
287
- ? [
301
+ isSidebar
302
+ ? {
303
+ paddingTop: SPACING + insets.top,
304
+ paddingBottom: SPACING + insets.bottom,
305
+ paddingStart:
306
+ SPACING + (tabBarPosition === 'left' ? insets.left : 0),
307
+ paddingEnd:
308
+ SPACING + (tabBarPosition === 'right' ? insets.right : 0),
309
+ minWidth: hasHorizontalLabels
310
+ ? getDefaultSidebarWidth(dimensions)
311
+ : 0,
312
+ }
313
+ : [
288
314
  {
289
315
  transform: [
290
316
  {
@@ -308,31 +334,18 @@ export function BottomTabBar({
308
334
  paddingBottom,
309
335
  paddingHorizontal: Math.max(insets.left, insets.right),
310
336
  },
311
- ]
312
- : {
313
- paddingTop: insets.top,
314
- paddingBottom: insets.bottom,
315
- paddingLeft: tabBarPosition === 'left' ? insets.left : 0,
316
- paddingRight: tabBarPosition === 'right' ? insets.right : 0,
317
- minWidth: hasHorizontalLabels
318
- ? getDefaultSidebarWidth(dimensions)
319
- : 0,
320
- },
337
+ ],
321
338
  tabBarStyle,
322
339
  ]}
323
340
  pointerEvents={isTabBarHidden ? 'none' : 'auto'}
324
- onLayout={tabBarPosition === 'bottom' ? handleLayout : undefined}
341
+ onLayout={isSidebar ? undefined : handleLayout}
325
342
  >
326
343
  <View pointerEvents="none" style={StyleSheet.absoluteFill}>
327
344
  {tabBarBackgroundElement}
328
345
  </View>
329
346
  <View
330
347
  accessibilityRole="tablist"
331
- style={
332
- tabBarPosition === 'bottom'
333
- ? styles.bottomContent
334
- : styles.sideContent
335
- }
348
+ style={isSidebar ? styles.sideContent : styles.bottomContent}
336
349
  >
337
350
  {routes.map((route, index) => {
338
351
  const focused = index === state.index;
@@ -372,8 +385,8 @@ export function BottomTabBar({
372
385
  options.tabBarAccessibilityLabel !== undefined
373
386
  ? options.tabBarAccessibilityLabel
374
387
  : typeof label === 'string' && Platform.OS === 'ios'
375
- ? `${label}, tab, ${index + 1} of ${routes.length}`
376
- : undefined;
388
+ ? `${label}, tab, ${index + 1} of ${routes.length}`
389
+ : undefined;
377
390
 
378
391
  return (
379
392
  <NavigationContext.Provider
@@ -387,6 +400,7 @@ export function BottomTabBar({
387
400
  descriptor={descriptors[route.key]}
388
401
  focused={focused}
389
402
  horizontal={hasHorizontalLabels}
403
+ variant={isSidebar ? 'material' : 'uikit'}
390
404
  onPress={onPress}
391
405
  onLongPress={onLongPress}
392
406
  accessibilityLabel={accessibilityLabel}
@@ -410,14 +424,14 @@ export function BottomTabBar({
410
424
  labelStyle={options.tabBarLabelStyle}
411
425
  iconStyle={options.tabBarIconStyle}
412
426
  style={[
413
- tabBarPosition === 'bottom'
414
- ? styles.bottomItem
415
- : [
427
+ isSidebar
428
+ ? [
416
429
  styles.sideItem,
417
430
  hasHorizontalLabels
418
- ? { justifyContent: 'flex-start' }
419
- : null,
420
- ],
431
+ ? styles.sideItemHorizontal
432
+ : styles.sideItemVertical,
433
+ ]
434
+ : styles.bottomItem,
421
435
  options.tabBarItemStyle,
422
436
  ]}
423
437
  />
@@ -431,23 +445,20 @@ export function BottomTabBar({
431
445
  }
432
446
 
433
447
  const styles = StyleSheet.create({
434
- left: {
448
+ start: {
435
449
  top: 0,
436
450
  bottom: 0,
437
- left: 0,
438
- borderRightWidth: StyleSheet.hairlineWidth,
451
+ start: 0,
439
452
  },
440
- right: {
453
+ end: {
441
454
  top: 0,
442
455
  bottom: 0,
443
- right: 0,
444
- borderLeftWidth: StyleSheet.hairlineWidth,
456
+ end: 0,
445
457
  },
446
458
  bottom: {
447
- left: 0,
448
- right: 0,
459
+ start: 0,
460
+ end: 0,
449
461
  bottom: 0,
450
- borderTopWidth: StyleSheet.hairlineWidth,
451
462
  elevation: 8,
452
463
  },
453
464
  bottomContent: {
@@ -457,7 +468,6 @@ const styles = StyleSheet.create({
457
468
  sideContent: {
458
469
  flex: 1,
459
470
  flexDirection: 'column',
460
- padding: SPACING,
461
471
  },
462
472
  bottomItem: {
463
473
  flex: 1,
@@ -465,6 +475,12 @@ const styles = StyleSheet.create({
465
475
  sideItem: {
466
476
  margin: SPACING,
467
477
  padding: SPACING * 2,
468
- borderRadius: 4,
478
+ },
479
+ sideItemHorizontal: {
480
+ borderRadius: 56,
481
+ justifyContent: 'flex-start',
482
+ },
483
+ sideItemVertical: {
484
+ borderRadius: 16,
469
485
  },
470
486
  });
@@ -89,6 +89,12 @@ type Props = {
89
89
  * Whether the label should be aligned with the icon horizontally.
90
90
  */
91
91
  horizontal: boolean;
92
+ /**
93
+ * Variant of navigation bar styling
94
+ * - `uikit`: iOS UIKit style
95
+ * - `material`: Material Design style
96
+ */
97
+ variant: 'uikit' | 'material';
92
98
  /**
93
99
  * Color for the icon and label when the item is active.
94
100
  */
@@ -163,6 +169,7 @@ export function BottomTabItem({
163
169
  onPress,
164
170
  onLongPress,
165
171
  horizontal,
172
+ variant,
166
173
  activeTintColor: customActiveTintColor,
167
174
  inactiveTintColor: customInactiveTintColor,
168
175
  activeBackgroundColor = 'transparent',
@@ -173,7 +180,7 @@ export function BottomTabItem({
173
180
  iconStyle,
174
181
  style,
175
182
  }: Props) {
176
- const { colors } = useTheme();
183
+ const { colors, fonts } = useTheme();
177
184
 
178
185
  const activeTintColor =
179
186
  customActiveTintColor === undefined
@@ -216,7 +223,14 @@ export function BottomTabItem({
216
223
  return (
217
224
  <Label
218
225
  style={[
219
- horizontal ? styles.labelBeside : styles.labelBeneath,
226
+ horizontal
227
+ ? [
228
+ styles.labelBeside,
229
+ { marginStart: icon !== undefined ? 16 : 0 },
230
+ variant === 'uikit' && styles.labelBesideUikit,
231
+ ]
232
+ : styles.labelBeneath,
233
+ variant === 'material' && fonts.medium,
220
234
  labelStyle,
221
235
  ]}
222
236
  allowFontScaling={allowFontScaling}
@@ -286,6 +300,8 @@ export function BottomTabItem({
286
300
  const styles = StyleSheet.create({
287
301
  tab: {
288
302
  alignItems: 'center',
303
+ // Roundness for iPad hover effect
304
+ borderRadius: 10,
289
305
  },
290
306
  tabPortrait: {
291
307
  justifyContent: 'flex-end',
@@ -294,12 +310,20 @@ const styles = StyleSheet.create({
294
310
  tabLandscape: {
295
311
  justifyContent: 'center',
296
312
  flexDirection: 'row',
313
+ paddingVertical: 12,
314
+ paddingStart: 16,
315
+ paddingEnd: 24,
297
316
  },
298
317
  labelBeneath: {
299
318
  fontSize: 10,
300
319
  },
301
320
  labelBeside: {
321
+ marginEnd: 12,
322
+ marginVertical: 4,
323
+ lineHeight: 24,
324
+ marginStart: 20,
325
+ },
326
+ labelBesideUikit: {
302
327
  fontSize: 13,
303
- marginLeft: 20,
304
328
  },
305
329
  });
@@ -4,14 +4,21 @@ import {
4
4
  SafeAreaProviderCompat,
5
5
  Screen,
6
6
  } from '@react-navigation/elements';
7
- import type {
8
- ParamListBase,
9
- TabNavigationState,
7
+ import {
8
+ type NavigationAction,
9
+ type ParamListBase,
10
+ StackActions,
11
+ type TabNavigationState,
12
+ useLocale,
10
13
  } from '@react-navigation/native';
11
14
  import * as React from 'react';
12
15
  import { Animated, Platform, StyleSheet } from 'react-native';
13
16
  import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
14
17
 
18
+ import {
19
+ FadeTransition,
20
+ ShiftTransition,
21
+ } from '../TransitionConfigs/TransitionPresets';
15
22
  import type {
16
23
  BottomTabBarProps,
17
24
  BottomTabDescriptorMap,
@@ -38,14 +45,26 @@ const STATE_INACTIVE = 0;
38
45
  const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
39
46
  const STATE_ON_TOP = 2;
40
47
 
48
+ const NAMED_TRANSITIONS_PRESETS = {
49
+ fade: FadeTransition,
50
+ shift: ShiftTransition,
51
+ none: {
52
+ sceneStyleInterpolator: undefined,
53
+ transitionSpec: {
54
+ animation: 'timing',
55
+ config: { duration: 0 },
56
+ },
57
+ },
58
+ } as const;
59
+
41
60
  const hasAnimation = (options: BottomTabNavigationOptions) => {
42
- const { animationEnabled, transitionSpec } = options;
61
+ const { animation, transitionSpec } = options;
43
62
 
44
- if (animationEnabled === false || !transitionSpec) {
45
- return false;
63
+ if (animation) {
64
+ return animation !== 'none';
46
65
  }
47
66
 
48
- return true;
67
+ return !transitionSpec;
49
68
  };
50
69
 
51
70
  export function BottomTabView(props: Props) {
@@ -63,6 +82,8 @@ export function BottomTabView(props: Props) {
63
82
 
64
83
  const focusedRouteKey = state.routes[state.index].key;
65
84
 
85
+ const { direction } = useLocale();
86
+
66
87
  /**
67
88
  * List of loaded tabs, tabs will be loaded when navigated to.
68
89
  */
@@ -73,41 +94,82 @@ export function BottomTabView(props: Props) {
73
94
  setLoaded([...loaded, focusedRouteKey]);
74
95
  }
75
96
 
97
+ const previousRouteKeyRef = React.useRef(focusedRouteKey);
76
98
  const tabAnims = useAnimatedHashMap(state);
77
99
 
78
100
  React.useEffect(() => {
101
+ const previousRouteKey = previousRouteKeyRef.current;
102
+
103
+ let popToTopAction: NavigationAction | undefined;
104
+
105
+ if (
106
+ previousRouteKey !== focusedRouteKey &&
107
+ descriptors[previousRouteKey]?.options.popToTopOnBlur
108
+ ) {
109
+ const prevRoute = state.routes.find(
110
+ (route) => route.key === previousRouteKey
111
+ );
112
+
113
+ if (prevRoute?.state?.type === 'stack' && prevRoute.state.key) {
114
+ popToTopAction = {
115
+ ...StackActions.popToTop(),
116
+ target: prevRoute.state.key,
117
+ };
118
+ }
119
+ }
120
+
79
121
  const animateToIndex = () => {
80
122
  Animated.parallel(
81
123
  state.routes
82
124
  .map((route, index) => {
83
125
  const { options } = descriptors[route.key];
84
- const { transitionSpec } = options;
126
+ const {
127
+ animation = 'none',
128
+ transitionSpec = NAMED_TRANSITIONS_PRESETS[animation]
129
+ .transitionSpec,
130
+ } = options;
131
+
132
+ let spec = transitionSpec;
133
+
134
+ if (
135
+ route.key !== previousRouteKey &&
136
+ route.key !== focusedRouteKey
137
+ ) {
138
+ // Don't animate if the screen is not previous one or new one
139
+ // This will avoid flicker for screens not involved in the transition
140
+ spec = NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
141
+ }
85
142
 
86
- const animationEnabled = hasAnimation(options);
143
+ spec = spec ?? NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
87
144
 
88
145
  const toValue =
89
146
  index === state.index ? 0 : index >= state.index ? 1 : -1;
90
147
 
91
- if (!animationEnabled || !transitionSpec) {
92
- return Animated.timing(tabAnims[route.key], {
93
- toValue,
94
- duration: 0,
95
- useNativeDriver: true,
96
- });
97
- }
98
-
99
- return Animated[transitionSpec.animation](tabAnims[route.key], {
100
- ...transitionSpec.config,
148
+ return Animated[spec.animation](tabAnims[route.key], {
149
+ ...spec.config,
101
150
  toValue,
102
151
  useNativeDriver: true,
103
152
  });
104
153
  })
105
154
  .filter(Boolean) as Animated.CompositeAnimation[]
106
- ).start();
155
+ ).start(({ finished }) => {
156
+ if (finished && popToTopAction) {
157
+ navigation.dispatch(popToTopAction);
158
+ }
159
+ });
107
160
  };
108
161
 
109
162
  animateToIndex();
110
- }, [descriptors, state.index, state.routes, tabAnims]);
163
+
164
+ previousRouteKeyRef.current = focusedRouteKey;
165
+ }, [
166
+ descriptors,
167
+ focusedRouteKey,
168
+ navigation,
169
+ state.index,
170
+ state.routes,
171
+ tabAnims,
172
+ ]);
111
173
 
112
174
  const dimensions = SafeAreaProviderCompat.initialMetrics.frame;
113
175
  const [tabBarHeight, setTabBarHeight] = React.useState(() =>
@@ -115,7 +177,6 @@ export function BottomTabView(props: Props) {
115
177
  state,
116
178
  descriptors,
117
179
  dimensions,
118
- layout: { width: dimensions.width, height: 0 },
119
180
  insets: {
120
181
  ...SafeAreaProviderCompat.initialMetrics.insets,
121
182
  ...props.safeAreaInsets,
@@ -155,14 +216,19 @@ export function BottomTabView(props: Props) {
155
216
 
156
217
  return (
157
218
  <SafeAreaProviderCompat
158
- style={
159
- tabBarPosition === 'left'
160
- ? styles.left
161
- : tabBarPosition === 'right'
162
- ? styles.right
163
- : null
164
- }
219
+ style={{
220
+ flexDirection:
221
+ (tabBarPosition === 'left' && direction === 'ltr') ||
222
+ (tabBarPosition === 'right' && direction === 'rtl')
223
+ ? 'row-reverse'
224
+ : 'row',
225
+ }}
165
226
  >
227
+ {tabBarPosition === 'top' ? (
228
+ <BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
229
+ {renderTabBar()}
230
+ </BottomTabBarHeightCallbackContext.Provider>
231
+ ) : null}
166
232
  <MaybeScreenContainer
167
233
  enabled={detachInactiveScreens}
168
234
  hasTwoStates={hasTwoStates}
@@ -172,15 +238,12 @@ export function BottomTabView(props: Props) {
172
238
  const descriptor = descriptors[route.key];
173
239
  const {
174
240
  lazy = true,
175
- unmountOnBlur,
176
- sceneStyleInterpolator,
241
+ animation = 'none',
242
+ sceneStyleInterpolator = NAMED_TRANSITIONS_PRESETS[animation]
243
+ .sceneStyleInterpolator,
177
244
  } = descriptor.options;
178
245
  const isFocused = state.index === index;
179
246
 
180
- if (unmountOnBlur && !isFocused) {
181
- return null;
182
- }
183
-
184
247
  if (
185
248
  lazy &&
186
249
  !loaded.includes(route.key) &&
@@ -207,23 +270,25 @@ export function BottomTabView(props: Props) {
207
270
 
208
271
  const { sceneStyle } =
209
272
  sceneStyleInterpolator?.({
210
- current: tabAnims[route.key],
273
+ current: {
274
+ progress: tabAnims[route.key],
275
+ },
211
276
  }) ?? {};
212
277
 
213
278
  const animationEnabled = hasAnimation(descriptor.options);
214
279
  const activityState = isFocused
215
280
  ? STATE_ON_TOP // the screen is on top after the transition
216
281
  : animationEnabled // is animation is not enabled, immediately move to inactive state
217
- ? tabAnims[route.key].interpolate({
218
- inputRange: [0, 1 - EPSILON, 1],
219
- outputRange: [
220
- STATE_TRANSITIONING_OR_BELOW_TOP, // screen visible during transition
221
- STATE_TRANSITIONING_OR_BELOW_TOP,
222
- STATE_INACTIVE, // the screen is detached after transition
223
- ],
224
- extrapolate: 'extend',
225
- })
226
- : STATE_INACTIVE;
282
+ ? tabAnims[route.key].interpolate({
283
+ inputRange: [0, 1 - EPSILON, 1],
284
+ outputRange: [
285
+ STATE_TRANSITIONING_OR_BELOW_TOP, // screen visible during transition
286
+ STATE_TRANSITIONING_OR_BELOW_TOP,
287
+ STATE_INACTIVE, // the screen is detached after transition
288
+ ],
289
+ extrapolate: 'extend',
290
+ })
291
+ : STATE_INACTIVE;
227
292
 
228
293
  return (
229
294
  <MaybeScreen
@@ -259,20 +324,16 @@ export function BottomTabView(props: Props) {
259
324
  );
260
325
  })}
261
326
  </MaybeScreenContainer>
262
- <BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
263
- {renderTabBar()}
264
- </BottomTabBarHeightCallbackContext.Provider>
327
+ {tabBarPosition !== 'top' ? (
328
+ <BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
329
+ {renderTabBar()}
330
+ </BottomTabBarHeightCallbackContext.Provider>
331
+ ) : null}
265
332
  </SafeAreaProviderCompat>
266
333
  );
267
334
  }
268
335
 
269
336
  const styles = StyleSheet.create({
270
- left: {
271
- flexDirection: 'row-reverse',
272
- },
273
- right: {
274
- flexDirection: 'row',
275
- },
276
337
  screens: {
277
338
  flex: 1,
278
339
  overflow: 'hidden',
@@ -96,7 +96,7 @@ const styles = StyleSheet.create({
96
96
  },
97
97
  badge: {
98
98
  position: 'absolute',
99
- right: -5,
99
+ end: -5,
100
100
  top: -5,
101
101
  },
102
102
  });