@react-navigation/bottom-tabs 7.0.0-alpha.3 → 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.
package/src/types.tsx CHANGED
@@ -223,6 +223,11 @@ export type BottomTabNavigationOptions = HeaderOptions & {
223
223
  */
224
224
  tabBarBackground?: () => React.ReactNode;
225
225
 
226
+ /**
227
+ * Position of the tab bar on the screen. Defaults to `bottom`.
228
+ */
229
+ tabBarPosition?: 'bottom' | 'left' | 'right';
230
+
226
231
  /**
227
232
  * Whether this screens should render the first time it's accessed. Defaults to `true`.
228
233
  * Set it to `false` if you want to render the screen on initial render.
@@ -1,4 +1,8 @@
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,
@@ -8,6 +12,7 @@ import {
8
12
  useLinkTools,
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,
@@ -15,10 +20,11 @@ import {
15
20
  Platform,
16
21
  StyleProp,
17
22
  StyleSheet,
23
+ useWindowDimensions,
18
24
  View,
19
25
  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
 
@@ -140,6 +147,7 @@ export function BottomTabBar({
140
147
  const focusedOptions = focusedDescriptor.options;
141
148
 
142
149
  const {
150
+ tabBarPosition = 'bottom',
143
151
  tabBarShowLabel,
144
152
  tabBarHideOnKeyboard = false,
145
153
  tabBarVisibilityAnimationConfig,
@@ -147,11 +155,17 @@ export function BottomTabBar({
147
155
  tabBarBackground,
148
156
  tabBarActiveTintColor,
149
157
  tabBarInactiveTintColor,
150
- tabBarActiveBackgroundColor,
158
+ tabBarActiveBackgroundColor = tabBarPosition !== 'bottom'
159
+ ? Color(tabBarActiveTintColor ?? colors.primary)
160
+ .alpha(0.12)
161
+ .rgb()
162
+ .string()
163
+ : undefined,
151
164
  tabBarInactiveBackgroundColor,
152
165
  } = focusedOptions;
153
166
 
154
- const dimensions = useSafeAreaFrame();
167
+ // FIXME: useSafeAreaFrame doesn't update values when window is resized on Web
168
+ const dimensions = useWindowDimensions();
155
169
  const isKeyboardShown = useIsKeyboardShown();
156
170
 
157
171
  const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext);
@@ -256,42 +270,67 @@ export function BottomTabBar({
256
270
  return (
257
271
  <Animated.View
258
272
  style={[
259
- styles.tabBar,
273
+ tabBarPosition === 'left'
274
+ ? styles.left
275
+ : tabBarPosition === 'right'
276
+ ? styles.right
277
+ : styles.bottom,
260
278
  {
261
279
  backgroundColor:
262
280
  tabBarBackgroundElement != null ? 'transparent' : colors.card,
263
- borderTopColor: colors.border,
281
+ borderColor: colors.border,
264
282
  },
265
- {
266
- transform: [
267
- {
268
- translateY: visible.interpolate({
269
- inputRange: [0, 1],
270
- outputRange: [
271
- layout.height + paddingBottom + StyleSheet.hairlineWidth,
272
- 0,
283
+ tabBarPosition === 'bottom'
284
+ ? [
285
+ {
286
+ transform: [
287
+ {
288
+ translateY: visible.interpolate({
289
+ inputRange: [0, 1],
290
+ outputRange: [
291
+ layout.height +
292
+ paddingBottom +
293
+ StyleSheet.hairlineWidth,
294
+ 0,
295
+ ],
296
+ }),
297
+ },
273
298
  ],
274
- }),
299
+ // Absolutely position the tab bar so that the content is below it
300
+ // This is needed to avoid gap at bottom when the tab bar is hidden
301
+ position: isTabBarHidden ? 'absolute' : undefined,
302
+ },
303
+ {
304
+ height: tabBarHeight,
305
+ paddingBottom,
306
+ paddingHorizontal: Math.max(insets.left, insets.right),
307
+ },
308
+ ]
309
+ : {
310
+ paddingTop: insets.top,
311
+ paddingBottom: insets.bottom,
312
+ paddingLeft: tabBarPosition === 'left' ? insets.left : 0,
313
+ paddingRight: tabBarPosition === 'right' ? insets.right : 0,
314
+ minWidth: hasHorizontalLabels
315
+ ? getDefaultSidebarWidth(dimensions)
316
+ : 0,
275
317
  },
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
318
  tabBarStyle,
287
319
  ]}
288
320
  pointerEvents={isTabBarHidden ? 'none' : 'auto'}
289
- onLayout={handleLayout}
321
+ onLayout={tabBarPosition === 'bottom' ? handleLayout : undefined}
290
322
  >
291
323
  <View pointerEvents="none" style={StyleSheet.absoluteFill}>
292
324
  {tabBarBackgroundElement}
293
325
  </View>
294
- <View accessibilityRole="tablist" style={styles.content}>
326
+ <View
327
+ accessibilityRole="tablist"
328
+ style={
329
+ tabBarPosition === 'bottom'
330
+ ? styles.bottomContent
331
+ : styles.sideContent
332
+ }
333
+ >
295
334
  {routes.map((route, index) => {
296
335
  const focused = index === state.index;
297
336
  const { options } = descriptors[route.key];
@@ -319,11 +358,12 @@ export function BottomTabBar({
319
358
  };
320
359
 
321
360
  const label =
322
- options.tabBarLabel !== undefined
361
+ typeof options.tabBarLabel === 'function'
323
362
  ? options.tabBarLabel
324
- : options.title !== undefined
325
- ? options.title
326
- : route.name;
363
+ : getLabel(
364
+ { label: options.tabBarLabel, title: options.title },
365
+ route.name
366
+ );
327
367
 
328
368
  const accessibilityLabel =
329
369
  options.tabBarAccessibilityLabel !== undefined
@@ -366,7 +406,17 @@ export function BottomTabBar({
366
406
  showLabel={tabBarShowLabel}
367
407
  labelStyle={options.tabBarLabelStyle}
368
408
  iconStyle={options.tabBarIconStyle}
369
- style={options.tabBarItemStyle}
409
+ style={[
410
+ tabBarPosition === 'bottom'
411
+ ? styles.bottomItem
412
+ : [
413
+ styles.sideItem,
414
+ hasHorizontalLabels
415
+ ? { justifyContent: 'flex-start' }
416
+ : null,
417
+ ],
418
+ options.tabBarItemStyle,
419
+ ]}
370
420
  />
371
421
  </NavigationRouteContext.Provider>
372
422
  </NavigationContext.Provider>
@@ -378,15 +428,40 @@ export function BottomTabBar({
378
428
  }
379
429
 
380
430
  const styles = StyleSheet.create({
381
- tabBar: {
431
+ left: {
432
+ top: 0,
433
+ bottom: 0,
434
+ left: 0,
435
+ borderRightWidth: StyleSheet.hairlineWidth,
436
+ },
437
+ right: {
438
+ top: 0,
439
+ bottom: 0,
440
+ right: 0,
441
+ borderLeftWidth: StyleSheet.hairlineWidth,
442
+ },
443
+ bottom: {
382
444
  left: 0,
383
445
  right: 0,
384
446
  bottom: 0,
385
447
  borderTopWidth: StyleSheet.hairlineWidth,
386
448
  elevation: 8,
387
449
  },
388
- content: {
450
+ bottomContent: {
389
451
  flex: 1,
390
452
  flexDirection: 'row',
391
453
  },
454
+ sideContent: {
455
+ flex: 1,
456
+ flexDirection: 'column',
457
+ padding: SPACING,
458
+ },
459
+ bottomItem: {
460
+ flex: 1,
461
+ },
462
+ sideItem: {
463
+ margin: SPACING,
464
+ padding: SPACING * 2,
465
+ borderRadius: 4,
466
+ },
392
467
  });
@@ -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',
@@ -151,12 +151,22 @@ export function BottomTabView(props: Props) {
151
151
  hasAnimation(descriptors[route.key].options)
152
152
  );
153
153
 
154
+ const { tabBarPosition = 'bottom' } = descriptors[focusedRouteKey].options;
155
+
154
156
  return (
155
- <SafeAreaProviderCompat>
157
+ <SafeAreaProviderCompat
158
+ style={
159
+ tabBarPosition === 'left'
160
+ ? styles.left
161
+ : tabBarPosition === 'right'
162
+ ? styles.right
163
+ : null
164
+ }
165
+ >
156
166
  <MaybeScreenContainer
157
167
  enabled={detachInactiveScreens}
158
168
  hasTwoStates={hasTwoStates}
159
- style={styles.container}
169
+ style={styles.screens}
160
170
  >
161
171
  {routes.map((route, index) => {
162
172
  const descriptor = descriptors[route.key];
@@ -218,7 +228,9 @@ export function BottomTabView(props: Props) {
218
228
  enabled={detachInactiveScreens}
219
229
  freezeOnBlur={freezeOnBlur}
220
230
  >
221
- <BottomTabBarHeightContext.Provider value={tabBarHeight}>
231
+ <BottomTabBarHeightContext.Provider
232
+ value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
233
+ >
222
234
  <Screen
223
235
  focused={isFocused}
224
236
  route={descriptor.route}
@@ -250,7 +262,13 @@ export function BottomTabView(props: Props) {
250
262
  }
251
263
 
252
264
  const styles = StyleSheet.create({
253
- container: {
265
+ left: {
266
+ flexDirection: 'row-reverse',
267
+ },
268
+ right: {
269
+ flexDirection: 'row',
270
+ },
271
+ screens: {
254
272
  flex: 1,
255
273
  overflow: 'hidden',
256
274
  },
@@ -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
  });