@react-navigation/bottom-tabs 7.0.0-alpha.2 → 7.0.0-alpha.4

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 (71) 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 +19 -0
  4. package/lib/commonjs/TransitionConfigs/TransitionPresets.js.map +1 -0
  5. package/lib/commonjs/TransitionConfigs/TransitionSpecs.js +24 -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/utils/useAnimatedHashMap.js +31 -0
  10. package/lib/commonjs/utils/useAnimatedHashMap.js.map +1 -0
  11. package/lib/commonjs/views/BottomTabBar.js +53 -13
  12. package/lib/commonjs/views/BottomTabBar.js.map +1 -1
  13. package/lib/commonjs/views/BottomTabItem.js +21 -27
  14. package/lib/commonjs/views/BottomTabItem.js.map +1 -1
  15. package/lib/commonjs/views/BottomTabView.js +89 -8
  16. package/lib/commonjs/views/BottomTabView.js.map +1 -1
  17. package/lib/commonjs/views/ScreenFallback.js +6 -8
  18. package/lib/commonjs/views/ScreenFallback.js.map +1 -1
  19. package/lib/commonjs/views/TabBarIcon.js +12 -17
  20. package/lib/commonjs/views/TabBarIcon.js.map +1 -1
  21. package/lib/module/TransitionConfigs/SceneStyleInterpolators.js +40 -0
  22. package/lib/module/TransitionConfigs/SceneStyleInterpolators.js.map +1 -0
  23. package/lib/module/TransitionConfigs/TransitionPresets.js +11 -0
  24. package/lib/module/TransitionConfigs/TransitionPresets.js.map +1 -0
  25. package/lib/module/TransitionConfigs/TransitionSpecs.js +16 -0
  26. package/lib/module/TransitionConfigs/TransitionSpecs.js.map +1 -0
  27. package/lib/module/index.js +9 -0
  28. package/lib/module/index.js.map +1 -1
  29. package/lib/module/utils/useAnimatedHashMap.js +23 -0
  30. package/lib/module/utils/useAnimatedHashMap.js.map +1 -0
  31. package/lib/module/views/BottomTabBar.js +55 -15
  32. package/lib/module/views/BottomTabBar.js.map +1 -1
  33. package/lib/module/views/BottomTabItem.js +22 -28
  34. package/lib/module/views/BottomTabItem.js.map +1 -1
  35. package/lib/module/views/BottomTabView.js +90 -9
  36. package/lib/module/views/BottomTabView.js.map +1 -1
  37. package/lib/module/views/ScreenFallback.js +6 -8
  38. package/lib/module/views/ScreenFallback.js.map +1 -1
  39. package/lib/module/views/TabBarIcon.js +12 -17
  40. package/lib/module/views/TabBarIcon.js.map +1 -1
  41. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts +10 -0
  42. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts.map +1 -0
  43. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +4 -0
  44. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -0
  45. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +4 -0
  46. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -0
  47. package/lib/typescript/src/index.d.ts +7 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -1
  49. package/lib/typescript/src/types.d.ts +54 -0
  50. package/lib/typescript/src/types.d.ts.map +1 -1
  51. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts +4 -0
  52. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts.map +1 -0
  53. package/lib/typescript/src/views/BottomTabBar.d.ts +1 -1
  54. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  55. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  56. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -1
  57. package/lib/typescript/src/views/ScreenFallback.d.ts +4 -4
  58. package/lib/typescript/src/views/ScreenFallback.d.ts.map +1 -1
  59. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  60. package/package.json +5 -5
  61. package/src/TransitionConfigs/SceneStyleInterpolators.tsx +44 -0
  62. package/src/TransitionConfigs/TransitionPresets.tsx +13 -0
  63. package/src/TransitionConfigs/TransitionSpecs.tsx +19 -0
  64. package/src/index.tsx +9 -0
  65. package/src/types.tsx +75 -0
  66. package/src/utils/useAnimatedHashMap.tsx +25 -0
  67. package/src/views/BottomTabBar.tsx +109 -34
  68. package/src/views/BottomTabItem.tsx +31 -37
  69. package/src/views/BottomTabView.tsx +115 -10
  70. package/src/views/ScreenFallback.tsx +6 -13
  71. package/src/views/TabBarIcon.tsx +13 -21
@@ -1,3 +1,4 @@
1
+ import { getLabel, Label } from '@react-navigation/elements';
1
2
  import { CommonActions, Link, Route, useTheme } from '@react-navigation/native';
2
3
  import Color from 'color';
3
4
  import React from 'react';
@@ -7,7 +8,6 @@ import {
7
8
  Pressable,
8
9
  StyleProp,
9
10
  StyleSheet,
10
- Text,
11
11
  TextStyle,
12
12
  ViewStyle,
13
13
  } from 'react-native';
@@ -195,7 +195,7 @@ export function BottomTabItem({
195
195
  iconStyle,
196
196
  style,
197
197
  }: Props) {
198
- const { colors, fonts } = useTheme();
198
+ const { colors } = useTheme();
199
199
 
200
200
  const activeTintColor =
201
201
  customActiveTintColor === undefined
@@ -214,38 +214,38 @@ export function BottomTabItem({
214
214
 
215
215
  const color = focused ? activeTintColor : inactiveTintColor;
216
216
 
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>
217
+ if (typeof label !== 'string') {
218
+ const { options } = descriptor;
219
+ const children = getLabel(
220
+ {
221
+ label:
222
+ typeof options.tabBarLabel === 'string'
223
+ ? options.tabBarLabel
224
+ : undefined,
225
+ title: options.title,
226
+ },
227
+ route.name
232
228
  );
233
- }
234
229
 
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;
230
+ return label({
231
+ focused,
232
+ color,
233
+ position: horizontal ? 'beside-icon' : 'below-icon',
234
+ children,
235
+ });
236
+ }
242
237
 
243
- return label({
244
- focused,
245
- color,
246
- position: horizontal ? 'beside-icon' : 'below-icon',
247
- children,
248
- });
238
+ return (
239
+ <Label
240
+ style={[
241
+ horizontal ? styles.labelBeside : styles.labelBeneath,
242
+ labelStyle,
243
+ ]}
244
+ allowFontScaling={allowFontScaling}
245
+ >
246
+ {label}
247
+ </Label>
248
+ );
249
249
  };
250
250
 
251
251
  const renderIcon = ({ focused }: { focused: boolean }) => {
@@ -306,7 +306,6 @@ export function BottomTabItem({
306
306
 
307
307
  const styles = StyleSheet.create({
308
308
  tab: {
309
- flex: 1,
310
309
  alignItems: 'center',
311
310
  },
312
311
  tabPortrait: {
@@ -317,17 +316,12 @@ const styles = StyleSheet.create({
317
316
  justifyContent: 'center',
318
317
  flexDirection: 'row',
319
318
  },
320
- label: {
321
- textAlign: 'center',
322
- backgroundColor: 'transparent',
323
- },
324
319
  labelBeneath: {
325
320
  fontSize: 10,
326
321
  },
327
322
  labelBeside: {
328
323
  fontSize: 13,
329
324
  marginLeft: 20,
330
- marginTop: 3,
331
325
  },
332
326
  button: {
333
327
  display: 'flex',
@@ -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,12 +18,15 @@ 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';
29
+ import CompositeAnimation = Animated.CompositeAnimation;
27
30
 
28
31
  type Props = BottomTabNavigationConfig & {
29
32
  state: TabNavigationState<ParamListBase>;
@@ -31,6 +34,21 @@ type Props = BottomTabNavigationConfig & {
31
34
  descriptors: BottomTabDescriptorMap;
32
35
  };
33
36
 
37
+ const EPSILON = 1e-5;
38
+ const STATE_INACTIVE = 0;
39
+ const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
40
+ const STATE_ON_TOP = 2;
41
+
42
+ const hasAnimation = (options: BottomTabNavigationOptions) => {
43
+ const { animationEnabled, transitionSpec } = options;
44
+
45
+ if (animationEnabled === false || !transitionSpec) {
46
+ return false;
47
+ }
48
+
49
+ return true;
50
+ };
51
+
34
52
  export function BottomTabView(props: Props) {
35
53
  const {
36
54
  tabBar = (props: BottomTabBarProps) => <BottomTabBar {...props} />,
@@ -43,14 +61,54 @@ export function BottomTabView(props: Props) {
43
61
  Platform.OS === 'ios',
44
62
  sceneContainerStyle,
45
63
  } = props;
46
-
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 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,16 +146,35 @@ 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
+ >
93
166
  <MaybeScreenContainer
94
167
  enabled={detachInactiveScreens}
95
- hasTwoStates
96
- style={styles.container}
168
+ hasTwoStates={hasTwoStates}
169
+ style={styles.screens}
97
170
  >
98
171
  {routes.map((route, index) => {
99
172
  const descriptor = descriptors[route.key];
100
- const { lazy = true, unmountOnBlur } = descriptor.options;
173
+ const {
174
+ lazy = true,
175
+ unmountOnBlur,
176
+ sceneStyleInterpolator,
177
+ } = descriptor.options;
101
178
  const isFocused = state.index === index;
102
179
 
103
180
  if (unmountOnBlur && !isFocused) {
@@ -123,15 +200,37 @@ export function BottomTabView(props: Props) {
123
200
  headerTransparent,
124
201
  } = descriptor.options;
125
202
 
203
+ const { sceneStyle } =
204
+ sceneStyleInterpolator?.({
205
+ current: tabAnims[route.key],
206
+ }) ?? {};
207
+
208
+ const animationEnabled = hasAnimation(descriptor.options);
209
+ const activityState = isFocused
210
+ ? STATE_ON_TOP // the screen is on top after the transition
211
+ : animationEnabled // is animation is not enabled, immediately move to inactive state
212
+ ? tabAnims[route.key].interpolate({
213
+ inputRange: [0, 1 - EPSILON, 1],
214
+ outputRange: [
215
+ STATE_TRANSITIONING_OR_BELOW_TOP, // screen visible during transition
216
+ STATE_TRANSITIONING_OR_BELOW_TOP,
217
+ STATE_INACTIVE, // the screen is detached after transition
218
+ ],
219
+ extrapolate: 'extend',
220
+ })
221
+ : STATE_INACTIVE;
222
+
126
223
  return (
127
224
  <MaybeScreen
128
225
  key={route.key}
129
226
  style={[StyleSheet.absoluteFill, { zIndex: isFocused ? 0 : -1 }]}
130
- visible={isFocused}
227
+ active={activityState}
131
228
  enabled={detachInactiveScreens}
132
229
  freezeOnBlur={freezeOnBlur}
133
230
  >
134
- <BottomTabBarHeightContext.Provider value={tabBarHeight}>
231
+ <BottomTabBarHeightContext.Provider
232
+ value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
233
+ >
135
234
  <Screen
136
235
  focused={isFocused}
137
236
  route={descriptor.route}
@@ -146,7 +245,7 @@ export function BottomTabView(props: Props) {
146
245
  descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
147
246
  options: descriptor.options,
148
247
  })}
149
- style={sceneContainerStyle}
248
+ style={[sceneContainerStyle, animationEnabled && sceneStyle]}
150
249
  >
151
250
  {descriptor.render()}
152
251
  </Screen>
@@ -163,7 +262,13 @@ export function BottomTabView(props: Props) {
163
262
  }
164
263
 
165
264
  const styles = StyleSheet.create({
166
- container: {
265
+ left: {
266
+ flexDirection: 'row-reverse',
267
+ },
268
+ right: {
269
+ flexDirection: 'row',
270
+ },
271
+ screens: {
167
272
  flex: 1,
168
273
  overflow: 'hidden',
169
274
  },
@@ -1,11 +1,10 @@
1
- import { ResourceSavingView } from '@react-navigation/elements';
2
1
  import * as React from 'react';
3
- import { StyleProp, View, ViewProps, ViewStyle } from 'react-native';
2
+ import { Animated, StyleProp, View, ViewProps, ViewStyle } from 'react-native';
4
3
 
5
4
  type Props = {
6
- visible: boolean;
7
- children: React.ReactNode;
8
5
  enabled: boolean;
6
+ active: 0 | 1 | 2 | Animated.AnimatedInterpolation<0 | 1>;
7
+ children: React.ReactNode;
9
8
  freezeOnBlur?: boolean;
10
9
  style?: StyleProp<ViewStyle>;
11
10
  };
@@ -33,18 +32,12 @@ export const MaybeScreenContainer = ({
33
32
  return <View {...rest} />;
34
33
  };
35
34
 
36
- export function MaybeScreen({ visible, children, ...rest }: Props) {
35
+ export function MaybeScreen({ enabled, active, ...rest }: ViewProps & Props) {
37
36
  if (Screens?.screensEnabled?.()) {
38
37
  return (
39
- <Screens.Screen activityState={visible ? 2 : 0} {...rest}>
40
- {children}
41
- </Screens.Screen>
38
+ <Screens.Screen enabled={enabled} activityState={active} {...rest} />
42
39
  );
43
40
  }
44
41
 
45
- return (
46
- <ResourceSavingView visible={visible} {...rest}>
47
- {children}
48
- </ResourceSavingView>
49
- );
42
+ return <View {...rest} />;
50
43
  }
@@ -27,6 +27,8 @@ type Props = {
27
27
  style: StyleProp<ViewStyle>;
28
28
  };
29
29
 
30
+ const ICON_SIZE = 25;
31
+
30
32
  export function TabBarIcon({
31
33
  route: _,
32
34
  horizontal,
@@ -39,8 +41,6 @@ export function TabBarIcon({
39
41
  renderIcon,
40
42
  style,
41
43
  }: Props) {
42
- const size = 25;
43
-
44
44
  // We render the icon twice at the same position on top of each other:
45
45
  // active and inactive one, so we can fade between them.
46
46
  return (
@@ -50,25 +50,21 @@ export function TabBarIcon({
50
50
  <View style={[styles.icon, { opacity: activeOpacity }]}>
51
51
  {renderIcon({
52
52
  focused: true,
53
- size,
53
+ size: ICON_SIZE,
54
54
  color: activeTintColor,
55
55
  })}
56
56
  </View>
57
57
  <View style={[styles.icon, { opacity: inactiveOpacity }]}>
58
58
  {renderIcon({
59
59
  focused: false,
60
- size,
60
+ size: ICON_SIZE,
61
61
  color: inactiveTintColor,
62
62
  })}
63
63
  </View>
64
64
  <Badge
65
65
  visible={badge != null}
66
- style={[
67
- styles.badge,
68
- horizontal ? styles.badgeHorizontal : styles.badgeVertical,
69
- badgeStyle,
70
- ]}
71
- size={(size * 3) / 4}
66
+ style={[styles.badge, badgeStyle]}
67
+ size={ICON_SIZE * 0.75}
72
68
  >
73
69
  {badge}
74
70
  </Badge>
@@ -88,23 +84,19 @@ const styles = StyleSheet.create({
88
84
  height: '100%',
89
85
  width: '100%',
90
86
  // Workaround for react-native >= 0.54 layout bug
91
- minWidth: 25,
87
+ minWidth: ICON_SIZE,
92
88
  },
93
89
  iconVertical: {
94
- flex: 1,
90
+ width: ICON_SIZE,
91
+ height: ICON_SIZE,
95
92
  },
96
93
  iconHorizontal: {
97
- height: '100%',
98
- marginTop: 3,
94
+ width: ICON_SIZE,
95
+ height: ICON_SIZE,
99
96
  },
100
97
  badge: {
101
98
  position: 'absolute',
102
- left: 3,
103
- },
104
- badgeVertical: {
105
- top: 3,
106
- },
107
- badgeHorizontal: {
108
- top: 7,
99
+ right: -5,
100
+ top: -5,
109
101
  },
110
102
  });