@react-navigation/bottom-tabs 7.0.0-alpha.1 → 7.0.0-alpha.10

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 (102) hide show
  1. package/lib/commonjs/TransitionConfigs/SceneStyleInterpolators.js +47 -0
  2. package/lib/commonjs/TransitionConfigs/SceneStyleInterpolators.js.map +1 -0
  3. package/lib/commonjs/TransitionConfigs/TransitionPresets.js +17 -0
  4. package/lib/commonjs/TransitionConfigs/TransitionPresets.js.map +1 -0
  5. package/lib/commonjs/TransitionConfigs/TransitionSpecs.js +22 -0
  6. package/lib/commonjs/TransitionConfigs/TransitionSpecs.js.map +1 -0
  7. package/lib/commonjs/index.js +9 -0
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/navigators/createBottomTabNavigator.js +5 -4
  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 +3 -4
  13. package/lib/commonjs/utils/BottomTabBarHeightCallbackContext.js.map +1 -1
  14. package/lib/commonjs/utils/BottomTabBarHeightContext.js +3 -4
  15. package/lib/commonjs/utils/BottomTabBarHeightContext.js.map +1 -1
  16. package/lib/commonjs/utils/useAnimatedHashMap.js +31 -0
  17. package/lib/commonjs/utils/useAnimatedHashMap.js.map +1 -0
  18. package/lib/commonjs/utils/useBottomTabBarHeight.js +2 -2
  19. package/lib/commonjs/utils/useBottomTabBarHeight.js.map +1 -1
  20. package/lib/commonjs/utils/useIsKeyboardShown.js +2 -2
  21. package/lib/commonjs/utils/useIsKeyboardShown.js.map +1 -1
  22. package/lib/commonjs/views/Badge.js +2 -2
  23. package/lib/commonjs/views/Badge.js.map +1 -1
  24. package/lib/commonjs/views/BottomTabBar.js +63 -25
  25. package/lib/commonjs/views/BottomTabBar.js.map +1 -1
  26. package/lib/commonjs/views/BottomTabItem.js +32 -54
  27. package/lib/commonjs/views/BottomTabItem.js.map +1 -1
  28. package/lib/commonjs/views/BottomTabView.js +100 -18
  29. package/lib/commonjs/views/BottomTabView.js.map +1 -1
  30. package/lib/commonjs/views/ScreenFallback.js +10 -14
  31. package/lib/commonjs/views/ScreenFallback.js.map +1 -1
  32. package/lib/commonjs/views/TabBarIcon.js +12 -17
  33. package/lib/commonjs/views/TabBarIcon.js.map +1 -1
  34. package/lib/module/TransitionConfigs/SceneStyleInterpolators.js +40 -0
  35. package/lib/module/TransitionConfigs/SceneStyleInterpolators.js.map +1 -0
  36. package/lib/module/TransitionConfigs/TransitionPresets.js +11 -0
  37. package/lib/module/TransitionConfigs/TransitionPresets.js.map +1 -0
  38. package/lib/module/TransitionConfigs/TransitionSpecs.js +16 -0
  39. package/lib/module/TransitionConfigs/TransitionSpecs.js.map +1 -0
  40. package/lib/module/index.js +9 -0
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/navigators/createBottomTabNavigator.js +2 -0
  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 +23 -0
  48. package/lib/module/utils/useAnimatedHashMap.js.map +1 -0
  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.map +1 -1
  52. package/lib/module/views/BottomTabBar.js +66 -28
  53. package/lib/module/views/BottomTabBar.js.map +1 -1
  54. package/lib/module/views/BottomTabItem.js +34 -56
  55. package/lib/module/views/BottomTabItem.js.map +1 -1
  56. package/lib/module/views/BottomTabView.js +99 -17
  57. package/lib/module/views/BottomTabView.js.map +1 -1
  58. package/lib/module/views/ScreenFallback.js +8 -12
  59. package/lib/module/views/ScreenFallback.js.map +1 -1
  60. package/lib/module/views/TabBarIcon.js +12 -17
  61. package/lib/module/views/TabBarIcon.js.map +1 -1
  62. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts +10 -0
  63. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts.map +1 -0
  64. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +4 -0
  65. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -0
  66. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +4 -0
  67. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -0
  68. package/lib/typescript/src/index.d.ts +7 -0
  69. package/lib/typescript/src/index.d.ts.map +1 -1
  70. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts +4 -4
  71. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
  72. package/lib/typescript/src/types.d.ts +57 -3
  73. package/lib/typescript/src/types.d.ts.map +1 -1
  74. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts +4 -0
  75. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts.map +1 -0
  76. package/lib/typescript/src/views/Badge.d.ts +3 -3
  77. package/lib/typescript/src/views/Badge.d.ts.map +1 -1
  78. package/lib/typescript/src/views/BottomTabBar.d.ts +5 -5
  79. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  80. package/lib/typescript/src/views/BottomTabItem.d.ts +3 -3
  81. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  82. package/lib/typescript/src/views/BottomTabView.d.ts +2 -2
  83. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -1
  84. package/lib/typescript/src/views/ScreenFallback.d.ts +5 -5
  85. package/lib/typescript/src/views/ScreenFallback.d.ts.map +1 -1
  86. package/lib/typescript/src/views/TabBarIcon.d.ts +2 -2
  87. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  88. package/package.json +18 -19
  89. package/src/TransitionConfigs/SceneStyleInterpolators.tsx +44 -0
  90. package/src/TransitionConfigs/TransitionPresets.tsx +13 -0
  91. package/src/TransitionConfigs/TransitionSpecs.tsx +19 -0
  92. package/src/index.tsx +9 -0
  93. package/src/navigators/createBottomTabNavigator.tsx +7 -5
  94. package/src/types.tsx +82 -5
  95. package/src/utils/useAnimatedHashMap.tsx +25 -0
  96. package/src/utils/useIsKeyboardShown.tsx +1 -1
  97. package/src/views/Badge.tsx +6 -1
  98. package/src/views/BottomTabBar.tsx +123 -45
  99. package/src/views/BottomTabItem.tsx +51 -81
  100. package/src/views/BottomTabView.tsx +131 -14
  101. package/src/views/ScreenFallback.tsx +12 -13
  102. package/src/views/TabBarIcon.tsx +16 -24
@@ -1,24 +1,30 @@
1
- import { MissingIcon } from '@react-navigation/elements';
1
+ import {
2
+ getDefaultSidebarWidth,
3
+ getLabel,
4
+ MissingIcon,
5
+ } from '@react-navigation/elements';
2
6
  import {
3
7
  CommonActions,
4
8
  NavigationContext,
5
9
  NavigationRouteContext,
6
- ParamListBase,
7
- TabNavigationState,
8
- useLinkTools,
10
+ type ParamListBase,
11
+ type TabNavigationState,
12
+ useLinkBuilder,
9
13
  useTheme,
10
14
  } from '@react-navigation/native';
15
+ import Color from 'color';
11
16
  import React from 'react';
12
17
  import {
13
18
  Animated,
14
- LayoutChangeEvent,
19
+ type LayoutChangeEvent,
15
20
  Platform,
16
- StyleProp,
21
+ type StyleProp,
17
22
  StyleSheet,
23
+ useWindowDimensions,
18
24
  View,
19
- ViewStyle,
25
+ type ViewStyle,
20
26
  } from 'react-native';
21
- import { EdgeInsets, useSafeAreaFrame } from 'react-native-safe-area-context';
27
+ import type { EdgeInsets } from 'react-native-safe-area-context';
22
28
 
23
29
  import type { BottomTabBarProps, BottomTabDescriptorMap } from '../types';
24
30
  import { BottomTabBarHeightCallbackContext } from '../utils/BottomTabBarHeightCallbackContext';
@@ -32,6 +38,7 @@ type Props = BottomTabBarProps & {
32
38
  const DEFAULT_TABBAR_HEIGHT = 49;
33
39
  const COMPACT_TABBAR_HEIGHT = 32;
34
40
  const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
41
+ const SPACING = 5;
35
42
 
36
43
  const useNativeDriver = Platform.OS !== 'web';
37
44
 
@@ -97,8 +104,11 @@ export const getTabBarHeight = ({
97
104
  insets: EdgeInsets;
98
105
  style: Animated.WithAnimatedValue<StyleProp<ViewStyle>> | undefined;
99
106
  }) => {
100
- // @ts-ignore
101
- const customHeight = StyleSheet.flatten(style)?.height;
107
+ const flattenedStyle = StyleSheet.flatten(style);
108
+ const customHeight =
109
+ flattenedStyle && 'height' in flattenedStyle
110
+ ? flattenedStyle.height
111
+ : undefined;
102
112
 
103
113
  if (typeof customHeight === 'number') {
104
114
  return customHeight;
@@ -133,13 +143,14 @@ export function BottomTabBar({
133
143
  style,
134
144
  }: Props) {
135
145
  const { colors } = useTheme();
136
- const { buildHref } = useLinkTools();
146
+ const { buildHref } = useLinkBuilder();
137
147
 
138
148
  const focusedRoute = state.routes[state.index];
139
149
  const focusedDescriptor = descriptors[focusedRoute.key];
140
150
  const focusedOptions = focusedDescriptor.options;
141
151
 
142
152
  const {
153
+ tabBarPosition = 'bottom',
143
154
  tabBarShowLabel,
144
155
  tabBarHideOnKeyboard = false,
145
156
  tabBarVisibilityAnimationConfig,
@@ -147,11 +158,18 @@ export function BottomTabBar({
147
158
  tabBarBackground,
148
159
  tabBarActiveTintColor,
149
160
  tabBarInactiveTintColor,
150
- tabBarActiveBackgroundColor,
161
+ tabBarActiveBackgroundColor = tabBarPosition !== 'bottom' &&
162
+ tabBarPosition !== 'top'
163
+ ? Color(tabBarActiveTintColor ?? colors.primary)
164
+ .alpha(0.12)
165
+ .rgb()
166
+ .string()
167
+ : undefined,
151
168
  tabBarInactiveBackgroundColor,
152
169
  } = focusedOptions;
153
170
 
154
- const dimensions = useSafeAreaFrame();
171
+ // FIXME: useSafeAreaFrame doesn't update values when window is resized on Web
172
+ const dimensions = useWindowDimensions();
155
173
  const isKeyboardShown = useIsKeyboardShown();
156
174
 
157
175
  const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext);
@@ -253,45 +271,69 @@ export function BottomTabBar({
253
271
 
254
272
  const tabBarBackgroundElement = tabBarBackground?.();
255
273
 
274
+ const tabBarIsHorizontal =
275
+ tabBarPosition === 'bottom' || tabBarPosition === 'top';
276
+
256
277
  return (
257
278
  <Animated.View
258
279
  style={[
259
- styles.tabBar,
280
+ tabBarPosition === 'left'
281
+ ? styles.left
282
+ : tabBarPosition === 'right'
283
+ ? styles.right
284
+ : styles.bottom,
260
285
  {
261
286
  backgroundColor:
262
287
  tabBarBackgroundElement != null ? 'transparent' : colors.card,
263
- borderTopColor: colors.border,
288
+ borderColor: colors.border,
264
289
  },
265
- {
266
- transform: [
267
- {
268
- translateY: visible.interpolate({
269
- inputRange: [0, 1],
270
- outputRange: [
271
- layout.height + paddingBottom + StyleSheet.hairlineWidth,
272
- 0,
290
+ tabBarIsHorizontal
291
+ ? [
292
+ {
293
+ transform: [
294
+ {
295
+ translateY: visible.interpolate({
296
+ inputRange: [0, 1],
297
+ outputRange: [
298
+ layout.height +
299
+ paddingBottom +
300
+ StyleSheet.hairlineWidth,
301
+ 0,
302
+ ],
303
+ }),
304
+ },
273
305
  ],
274
- }),
306
+ // Absolutely position the tab bar so that the content is below it
307
+ // This is needed to avoid gap at bottom when the tab bar is hidden
308
+ position: isTabBarHidden ? 'absolute' : undefined,
309
+ },
310
+ {
311
+ height: tabBarHeight,
312
+ paddingBottom,
313
+ paddingHorizontal: Math.max(insets.left, insets.right),
314
+ },
315
+ ]
316
+ : {
317
+ paddingTop: insets.top,
318
+ paddingBottom: insets.bottom,
319
+ paddingLeft: tabBarPosition === 'left' ? insets.left : 0,
320
+ paddingRight: tabBarPosition === 'right' ? insets.right : 0,
321
+ minWidth: hasHorizontalLabels
322
+ ? getDefaultSidebarWidth(dimensions)
323
+ : 0,
275
324
  },
276
- ],
277
- // Absolutely position the tab bar so that the content is below it
278
- // This is needed to avoid gap at bottom when the tab bar is hidden
279
- position: isTabBarHidden ? 'absolute' : (null as any),
280
- },
281
- {
282
- height: tabBarHeight,
283
- paddingBottom,
284
- paddingHorizontal: Math.max(insets.left, insets.right),
285
- },
286
325
  tabBarStyle,
287
326
  ]}
288
327
  pointerEvents={isTabBarHidden ? 'none' : 'auto'}
289
- onLayout={handleLayout}
328
+ onLayout={tabBarIsHorizontal ? handleLayout : undefined}
290
329
  >
291
330
  <View pointerEvents="none" style={StyleSheet.absoluteFill}>
292
331
  {tabBarBackgroundElement}
293
332
  </View>
294
- <View accessibilityRole="tablist" style={styles.content}>
333
+ <View
334
+ accessibilityRole="tablist"
335
+ style={tabBarIsHorizontal ? styles.bottomContent : styles.sideContent}
336
+ >
295
337
  {routes.map((route, index) => {
296
338
  const focused = index === state.index;
297
339
  const { options } = descriptors[route.key];
@@ -319,18 +361,19 @@ export function BottomTabBar({
319
361
  };
320
362
 
321
363
  const label =
322
- options.tabBarLabel !== undefined
364
+ typeof options.tabBarLabel === 'function'
323
365
  ? options.tabBarLabel
324
- : options.title !== undefined
325
- ? options.title
326
- : route.name;
366
+ : getLabel(
367
+ { label: options.tabBarLabel, title: options.title },
368
+ route.name
369
+ );
327
370
 
328
371
  const accessibilityLabel =
329
372
  options.tabBarAccessibilityLabel !== undefined
330
373
  ? options.tabBarAccessibilityLabel
331
374
  : typeof label === 'string' && Platform.OS === 'ios'
332
- ? `${label}, tab, ${index + 1} of ${routes.length}`
333
- : undefined;
375
+ ? `${label}, tab, ${index + 1} of ${routes.length}`
376
+ : undefined;
334
377
 
335
378
  return (
336
379
  <NavigationContext.Provider
@@ -366,7 +409,17 @@ export function BottomTabBar({
366
409
  showLabel={tabBarShowLabel}
367
410
  labelStyle={options.tabBarLabelStyle}
368
411
  iconStyle={options.tabBarIconStyle}
369
- style={options.tabBarItemStyle}
412
+ style={[
413
+ tabBarIsHorizontal
414
+ ? styles.bottomItem
415
+ : [
416
+ styles.sideItem,
417
+ hasHorizontalLabels
418
+ ? { justifyContent: 'flex-start' }
419
+ : null,
420
+ ],
421
+ options.tabBarItemStyle,
422
+ ]}
370
423
  />
371
424
  </NavigationRouteContext.Provider>
372
425
  </NavigationContext.Provider>
@@ -378,15 +431,40 @@ export function BottomTabBar({
378
431
  }
379
432
 
380
433
  const styles = StyleSheet.create({
381
- tabBar: {
434
+ left: {
435
+ top: 0,
436
+ bottom: 0,
437
+ left: 0,
438
+ borderRightWidth: StyleSheet.hairlineWidth,
439
+ },
440
+ right: {
441
+ top: 0,
442
+ bottom: 0,
443
+ right: 0,
444
+ borderLeftWidth: StyleSheet.hairlineWidth,
445
+ },
446
+ bottom: {
382
447
  left: 0,
383
448
  right: 0,
384
449
  bottom: 0,
385
450
  borderTopWidth: StyleSheet.hairlineWidth,
386
451
  elevation: 8,
387
452
  },
388
- content: {
453
+ bottomContent: {
389
454
  flex: 1,
390
455
  flexDirection: 'row',
391
456
  },
457
+ sideContent: {
458
+ flex: 1,
459
+ flexDirection: 'column',
460
+ padding: SPACING,
461
+ },
462
+ bottomItem: {
463
+ flex: 1,
464
+ },
465
+ sideItem: {
466
+ margin: SPACING,
467
+ padding: SPACING * 2,
468
+ borderRadius: 4,
469
+ },
392
470
  });
@@ -1,15 +1,14 @@
1
- import { CommonActions, Link, Route, useTheme } from '@react-navigation/native';
1
+ import { getLabel, Label, PlatformPressable } from '@react-navigation/elements';
2
+ import { type Route, useTheme } from '@react-navigation/native';
2
3
  import Color from 'color';
3
4
  import React from 'react';
4
5
  import {
5
- GestureResponderEvent,
6
+ type GestureResponderEvent,
6
7
  Platform,
7
- Pressable,
8
- StyleProp,
8
+ type StyleProp,
9
9
  StyleSheet,
10
- Text,
11
- TextStyle,
12
- ViewStyle,
10
+ type TextStyle,
11
+ type ViewStyle,
13
12
  } from 'react-native';
14
13
 
15
14
  import type {
@@ -64,7 +63,7 @@ type Props = {
64
63
  */
65
64
  badgeStyle?: StyleProp<TextStyle>;
66
65
  /**
67
- * The button for the tab. Uses a `TouchableWithoutFeedback` by default.
66
+ * The button for the tab. Uses a `Pressable` by default.
68
67
  */
69
68
  button?: (props: BottomTabBarButtonProps) => React.ReactNode;
70
69
  /**
@@ -145,40 +144,19 @@ export function BottomTabItem({
145
144
  accessibilityRole,
146
145
  ...rest
147
146
  }: BottomTabBarButtonProps) => {
148
- if (Platform.OS === 'web') {
149
- // React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`.
150
- // We need to use `onClick` to be able to prevent default browser handling of links.
151
- return (
152
- <Link
153
- {...rest}
154
- href={href}
155
- action={CommonActions.navigate(route.name, route.params)}
156
- style={[styles.button, style]}
157
- onPress={(e: any) => {
158
- if (
159
- !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
160
- (e.button == null || e.button === 0) // ignore everything but left clicks
161
- ) {
162
- e.preventDefault();
163
- onPress?.(e);
164
- }
165
- }}
166
- >
167
- {children}
168
- </Link>
169
- );
170
- } else {
171
- return (
172
- <Pressable
173
- {...rest}
174
- accessibilityRole={accessibilityRole}
175
- onPress={onPress}
176
- style={style}
177
- >
178
- {children}
179
- </Pressable>
180
- );
181
- }
147
+ return (
148
+ <PlatformPressable
149
+ {...rest}
150
+ android_ripple={{ borderless: true }}
151
+ pressOpacity={1}
152
+ href={href}
153
+ accessibilityRole={accessibilityRole}
154
+ onPress={onPress}
155
+ style={style}
156
+ >
157
+ {children}
158
+ </PlatformPressable>
159
+ );
182
160
  },
183
161
  accessibilityLabel,
184
162
  testID,
@@ -195,7 +173,7 @@ export function BottomTabItem({
195
173
  iconStyle,
196
174
  style,
197
175
  }: Props) {
198
- const { colors, fonts } = useTheme();
176
+ const { colors } = useTheme();
199
177
 
200
178
  const activeTintColor =
201
179
  customActiveTintColor === undefined
@@ -214,38 +192,39 @@ export function BottomTabItem({
214
192
 
215
193
  const color = focused ? activeTintColor : inactiveTintColor;
216
194
 
217
- if (typeof label === 'string') {
218
- return (
219
- <Text
220
- numberOfLines={1}
221
- style={[
222
- { color },
223
- fonts.regular,
224
- styles.label,
225
- horizontal ? styles.labelBeside : styles.labelBeneath,
226
- labelStyle,
227
- ]}
228
- allowFontScaling={allowFontScaling}
229
- >
230
- {label}
231
- </Text>
195
+ if (typeof label !== 'string') {
196
+ const { options } = descriptor;
197
+ const children = getLabel(
198
+ {
199
+ label:
200
+ typeof options.tabBarLabel === 'string'
201
+ ? options.tabBarLabel
202
+ : undefined,
203
+ title: options.title,
204
+ },
205
+ route.name
232
206
  );
233
- }
234
207
 
235
- const { options } = descriptor;
236
- const children =
237
- typeof options.tabBarLabel === 'string'
238
- ? options.tabBarLabel
239
- : options.title !== undefined
240
- ? options.title
241
- : route.name;
208
+ return label({
209
+ focused,
210
+ color,
211
+ position: horizontal ? 'beside-icon' : 'below-icon',
212
+ children,
213
+ });
214
+ }
242
215
 
243
- return label({
244
- focused,
245
- color,
246
- position: horizontal ? 'beside-icon' : 'below-icon',
247
- children,
248
- });
216
+ return (
217
+ <Label
218
+ style={[
219
+ horizontal ? styles.labelBeside : styles.labelBeneath,
220
+ labelStyle,
221
+ ]}
222
+ allowFontScaling={allowFontScaling}
223
+ tintColor={color}
224
+ >
225
+ {label}
226
+ </Label>
227
+ );
249
228
  };
250
229
 
251
230
  const renderIcon = ({ focused }: { focused: boolean }) => {
@@ -306,7 +285,6 @@ export function BottomTabItem({
306
285
 
307
286
  const styles = StyleSheet.create({
308
287
  tab: {
309
- flex: 1,
310
288
  alignItems: 'center',
311
289
  },
312
290
  tabPortrait: {
@@ -317,19 +295,11 @@ const styles = StyleSheet.create({
317
295
  justifyContent: 'center',
318
296
  flexDirection: 'row',
319
297
  },
320
- label: {
321
- textAlign: 'center',
322
- backgroundColor: 'transparent',
323
- },
324
298
  labelBeneath: {
325
299
  fontSize: 10,
326
300
  },
327
301
  labelBeside: {
328
302
  fontSize: 13,
329
303
  marginLeft: 20,
330
- marginTop: 3,
331
- },
332
- button: {
333
- display: 'flex',
334
304
  },
335
305
  });
@@ -9,7 +9,7 @@ import type {
9
9
  TabNavigationState,
10
10
  } from '@react-navigation/native';
11
11
  import * as React from 'react';
12
- import { Platform, StyleSheet } from 'react-native';
12
+ import { Animated, Platform, StyleSheet } from 'react-native';
13
13
  import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
14
14
 
15
15
  import type {
@@ -18,10 +18,12 @@ import type {
18
18
  BottomTabHeaderProps,
19
19
  BottomTabNavigationConfig,
20
20
  BottomTabNavigationHelpers,
21
+ BottomTabNavigationOptions,
21
22
  BottomTabNavigationProp,
22
23
  } from '../types';
23
24
  import { BottomTabBarHeightCallbackContext } from '../utils/BottomTabBarHeightCallbackContext';
24
25
  import { BottomTabBarHeightContext } from '../utils/BottomTabBarHeightContext';
26
+ import { useAnimatedHashMap } from '../utils/useAnimatedHashMap';
25
27
  import { BottomTabBar, getTabBarHeight } from './BottomTabBar';
26
28
  import { MaybeScreen, MaybeScreenContainer } from './ScreenFallback';
27
29
 
@@ -31,6 +33,21 @@ type Props = BottomTabNavigationConfig & {
31
33
  descriptors: BottomTabDescriptorMap;
32
34
  };
33
35
 
36
+ const EPSILON = 1e-5;
37
+ const STATE_INACTIVE = 0;
38
+ const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
39
+ const STATE_ON_TOP = 2;
40
+
41
+ const hasAnimation = (options: BottomTabNavigationOptions) => {
42
+ const { animationEnabled, transitionSpec } = options;
43
+
44
+ if (animationEnabled === false || !transitionSpec) {
45
+ return false;
46
+ }
47
+
48
+ return true;
49
+ };
50
+
34
51
  export function BottomTabView(props: Props) {
35
52
  const {
36
53
  tabBar = (props: BottomTabBarProps) => <BottomTabBar {...props} />,
@@ -45,12 +62,53 @@ export function BottomTabView(props: Props) {
45
62
  } = props;
46
63
 
47
64
  const focusedRouteKey = state.routes[state.index].key;
65
+
66
+ /**
67
+ * List of loaded tabs, tabs will be loaded when navigated to.
68
+ */
48
69
  const [loaded, setLoaded] = React.useState([focusedRouteKey]);
49
70
 
50
71
  if (!loaded.includes(focusedRouteKey)) {
72
+ // Set the current tab to be loaded if it was not loaded before
51
73
  setLoaded([...loaded, focusedRouteKey]);
52
74
  }
53
75
 
76
+ const tabAnims = useAnimatedHashMap(state);
77
+
78
+ React.useEffect(() => {
79
+ const animateToIndex = () => {
80
+ Animated.parallel(
81
+ state.routes
82
+ .map((route, index) => {
83
+ const { options } = descriptors[route.key];
84
+ const { transitionSpec } = options;
85
+
86
+ const animationEnabled = hasAnimation(options);
87
+
88
+ const toValue =
89
+ index === state.index ? 0 : index >= state.index ? 1 : -1;
90
+
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,
101
+ toValue,
102
+ useNativeDriver: true,
103
+ });
104
+ })
105
+ .filter(Boolean) as Animated.CompositeAnimation[]
106
+ ).start();
107
+ };
108
+
109
+ animateToIndex();
110
+ }, [descriptors, state.index, state.routes, tabAnims]);
111
+
54
112
  const dimensions = SafeAreaProviderCompat.initialMetrics.frame;
55
113
  const [tabBarHeight, setTabBarHeight] = React.useState(() =>
56
114
  getTabBarHeight({
@@ -88,24 +146,53 @@ export function BottomTabView(props: Props) {
88
146
 
89
147
  const { routes } = state;
90
148
 
149
+ // If there is no animation, we only have 2 states: visible and invisible
150
+ const hasTwoStates = !routes.some((route) =>
151
+ hasAnimation(descriptors[route.key].options)
152
+ );
153
+
154
+ const { tabBarPosition = 'bottom' } = descriptors[focusedRouteKey].options;
155
+
91
156
  return (
92
- <SafeAreaProviderCompat>
157
+ <SafeAreaProviderCompat
158
+ style={
159
+ tabBarPosition === 'left'
160
+ ? styles.left
161
+ : tabBarPosition === 'right'
162
+ ? styles.right
163
+ : null
164
+ }
165
+ >
166
+ {tabBarPosition === 'top' ? (
167
+ <BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
168
+ {renderTabBar()}
169
+ </BottomTabBarHeightCallbackContext.Provider>
170
+ ) : null}
93
171
  <MaybeScreenContainer
94
172
  enabled={detachInactiveScreens}
95
- hasTwoStates
96
- style={styles.container}
173
+ hasTwoStates={hasTwoStates}
174
+ style={styles.screens}
97
175
  >
98
176
  {routes.map((route, index) => {
99
177
  const descriptor = descriptors[route.key];
100
- const { lazy = true, unmountOnBlur } = descriptor.options;
178
+ const {
179
+ lazy = true,
180
+ unmountOnBlur,
181
+ sceneStyleInterpolator,
182
+ } = descriptor.options;
101
183
  const isFocused = state.index === index;
102
184
 
103
185
  if (unmountOnBlur && !isFocused) {
104
186
  return null;
105
187
  }
106
188
 
107
- if (lazy && !loaded.includes(route.key) && !isFocused) {
108
- // Don't render a lazy screen if we've never navigated to it
189
+ if (
190
+ lazy &&
191
+ !loaded.includes(route.key) &&
192
+ !isFocused &&
193
+ !state.preloadedRouteKeys.includes(route.key)
194
+ ) {
195
+ // Don't render a lazy screen if we've never navigated to it or it wasn't preloaded
109
196
  return null;
110
197
  }
111
198
 
@@ -123,15 +210,37 @@ export function BottomTabView(props: Props) {
123
210
  headerTransparent,
124
211
  } = descriptor.options;
125
212
 
213
+ const { sceneStyle } =
214
+ sceneStyleInterpolator?.({
215
+ current: tabAnims[route.key],
216
+ }) ?? {};
217
+
218
+ const animationEnabled = hasAnimation(descriptor.options);
219
+ const activityState = isFocused
220
+ ? STATE_ON_TOP // the screen is on top after the transition
221
+ : animationEnabled // is animation is not enabled, immediately move to inactive state
222
+ ? tabAnims[route.key].interpolate({
223
+ inputRange: [0, 1 - EPSILON, 1],
224
+ outputRange: [
225
+ STATE_TRANSITIONING_OR_BELOW_TOP, // screen visible during transition
226
+ STATE_TRANSITIONING_OR_BELOW_TOP,
227
+ STATE_INACTIVE, // the screen is detached after transition
228
+ ],
229
+ extrapolate: 'extend',
230
+ })
231
+ : STATE_INACTIVE;
232
+
126
233
  return (
127
234
  <MaybeScreen
128
235
  key={route.key}
129
236
  style={[StyleSheet.absoluteFill, { zIndex: isFocused ? 0 : -1 }]}
130
- visible={isFocused}
237
+ active={activityState}
131
238
  enabled={detachInactiveScreens}
132
239
  freezeOnBlur={freezeOnBlur}
133
240
  >
134
- <BottomTabBarHeightContext.Provider value={tabBarHeight}>
241
+ <BottomTabBarHeightContext.Provider
242
+ value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
243
+ >
135
244
  <Screen
136
245
  focused={isFocused}
137
246
  route={descriptor.route}
@@ -146,7 +255,7 @@ export function BottomTabView(props: Props) {
146
255
  descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
147
256
  options: descriptor.options,
148
257
  })}
149
- style={sceneContainerStyle}
258
+ style={[sceneContainerStyle, animationEnabled && sceneStyle]}
150
259
  >
151
260
  {descriptor.render()}
152
261
  </Screen>
@@ -155,15 +264,23 @@ export function BottomTabView(props: Props) {
155
264
  );
156
265
  })}
157
266
  </MaybeScreenContainer>
158
- <BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
159
- {renderTabBar()}
160
- </BottomTabBarHeightCallbackContext.Provider>
267
+ {tabBarPosition !== 'top' ? (
268
+ <BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
269
+ {renderTabBar()}
270
+ </BottomTabBarHeightCallbackContext.Provider>
271
+ ) : null}
161
272
  </SafeAreaProviderCompat>
162
273
  );
163
274
  }
164
275
 
165
276
  const styles = StyleSheet.create({
166
- container: {
277
+ left: {
278
+ flexDirection: 'row-reverse',
279
+ },
280
+ right: {
281
+ flexDirection: 'row',
282
+ },
283
+ screens: {
167
284
  flex: 1,
168
285
  overflow: 'hidden',
169
286
  },