@react-navigation/bottom-tabs 7.19.2 → 7.21.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 (33) hide show
  1. package/lib/module/unstable/Deferred.js +3 -10
  2. package/lib/module/unstable/Deferred.js.map +1 -1
  3. package/lib/module/unstable/NativeBottomTabView.native.js +309 -198
  4. package/lib/module/unstable/NativeBottomTabView.native.js.map +1 -1
  5. package/lib/module/views/BottomTabBar.js +111 -58
  6. package/lib/module/views/BottomTabBar.js.map +1 -1
  7. package/lib/module/views/BottomTabItem.js +2 -0
  8. package/lib/module/views/BottomTabItem.js.map +1 -1
  9. package/lib/module/views/BottomTabView.js +81 -60
  10. package/lib/module/views/BottomTabView.js.map +1 -1
  11. package/lib/module/views/TabBarIcon.js +6 -2
  12. package/lib/module/views/TabBarIcon.js.map +1 -1
  13. package/lib/typescript/src/types.d.ts +26 -1
  14. package/lib/typescript/src/types.d.ts.map +1 -1
  15. package/lib/typescript/src/unstable/Deferred.d.ts.map +1 -1
  16. package/lib/typescript/src/unstable/NativeBottomTabView.native.d.ts.map +1 -1
  17. package/lib/typescript/src/unstable/types.d.ts +148 -5
  18. package/lib/typescript/src/unstable/types.d.ts.map +1 -1
  19. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  20. package/lib/typescript/src/views/BottomTabItem.d.ts +5 -1
  21. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  22. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -1
  23. package/lib/typescript/src/views/TabBarIcon.d.ts +2 -1
  24. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  25. package/package.json +6 -6
  26. package/src/types.tsx +36 -2
  27. package/src/unstable/Deferred.tsx +3 -11
  28. package/src/unstable/NativeBottomTabView.native.tsx +376 -217
  29. package/src/unstable/types.tsx +164 -6
  30. package/src/views/BottomTabBar.tsx +153 -72
  31. package/src/views/BottomTabItem.tsx +6 -0
  32. package/src/views/BottomTabView.tsx +101 -69
  33. package/src/views/TabBarIcon.tsx +9 -2
@@ -6,6 +6,7 @@ import {
6
6
  import {
7
7
  CommonActions,
8
8
  NavigationMetaContext,
9
+ type NavigationRoute,
9
10
  type ParamListBase,
10
11
  type Route,
11
12
  StackActions,
@@ -19,15 +20,16 @@ import {
19
20
  type NativeSyntheticEvent,
20
21
  Platform,
21
22
  PlatformColor,
23
+ type TextStyle,
22
24
  } from 'react-native';
23
25
  import {
24
- type PlatformIconAndroid,
25
- type PlatformIconIOS,
26
26
  type PlatformIconShared,
27
27
  Tabs,
28
28
  type TabsBottomAccessoryEnvironment,
29
29
  type TabSelectedEvent,
30
30
  type TabSelectionRejectedEvent,
31
+ type TabsScreenIconAndroid,
32
+ type TabsScreenIconIOS,
31
33
  type TabsScreenItemStateAppearanceAndroid,
32
34
  type TabsScreenItemStateAppearanceIOS,
33
35
  } from 'react-native-screens';
@@ -35,6 +37,7 @@ import {
35
37
  import { Deferred } from './Deferred';
36
38
  import { NativeScreen } from './NativeScreen/NativeScreen';
37
39
  import type {
40
+ NativeBottomTabDescriptor,
38
41
  NativeBottomTabDescriptorMap,
39
42
  NativeBottomTabIcon,
40
43
  NativeBottomTabNavigationConfig,
@@ -50,8 +53,8 @@ type Props = NativeBottomTabNavigationConfig & {
50
53
  };
51
54
 
52
55
  type PlatformIcon = {
53
- ios?: PlatformIconIOS;
54
- android?: PlatformIconAndroid;
56
+ ios?: TabsScreenIconIOS;
57
+ android?: TabsScreenIconAndroid;
55
58
  shared?: PlatformIconShared;
56
59
  };
57
60
 
@@ -76,6 +79,9 @@ const meta = {
76
79
  type: 'native-tabs',
77
80
  };
78
81
 
82
+ const SUPPORTS_PROMINENT_TAB =
83
+ Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 27;
84
+
79
85
  function reducer(state: NativeState, action: NativeAction): NativeState {
80
86
  switch (action.type) {
81
87
  case 'CONFIRM_STATE':
@@ -149,7 +155,8 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
149
155
  // JS sends a requested tab with the native provenance it was based on.
150
156
  // Native replies with the selected tab and its new provenance.
151
157
  const onTabSelected = (event: NativeSyntheticEvent<TabSelectedEvent>) => {
152
- const { selectedScreenKey, provenance, actionOrigin } = event.nativeEvent;
158
+ const { selectedScreenKey, provenance, actionOrigin, isRepeated } =
159
+ event.nativeEvent;
153
160
 
154
161
  const confirmed = {
155
162
  routeKey: selectedScreenKey,
@@ -167,9 +174,20 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
167
174
  }
168
175
 
169
176
  if (actionOrigin === 'user') {
177
+ const { tabBarRepeatedPressBehavior } = descriptors[route.key].options;
178
+
170
179
  navigation.emit({
171
180
  type: 'tabPress',
172
181
  target: route.key,
182
+ data: {
183
+ origin: 'native',
184
+ behavior: {
185
+ scrollToTop:
186
+ isRepeated && tabBarRepeatedPressBehavior?.scrollToTop !== false,
187
+ popToTop:
188
+ isRepeated && tabBarRepeatedPressBehavior?.popToTop !== false,
189
+ },
190
+ },
173
191
  });
174
192
  }
175
193
 
@@ -236,6 +254,13 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
236
254
  navigation.emit({
237
255
  type: 'tabPress',
238
256
  target: preventedScreenKey,
257
+ data: {
258
+ origin: 'native',
259
+ behavior: {
260
+ scrollToTop: false,
261
+ popToTop: false,
262
+ },
263
+ },
239
264
  });
240
265
  };
241
266
 
@@ -255,49 +280,32 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
255
280
  color: fontColor,
256
281
  } = currentOptions.tabBarLabelStyle || {};
257
282
 
258
- const activeTintColor =
259
- currentOptions.tabBarActiveTintColor ?? colors.primary;
260
-
261
- const inactiveTintColor =
262
- currentOptions.tabBarInactiveTintColor ??
263
- Platform.select<ColorValue | string>({
264
- ios: PlatformColor('label'),
265
- default: colors.text,
266
- });
283
+ const { activeIndicatorColor, activeTintColor, inactiveTintColor } =
284
+ React.useMemo(() => {
285
+ const activeTintColor =
286
+ currentOptions.tabBarActiveTintColor ?? colors.primary;
267
287
 
268
- const activeIndicatorColor =
269
- (currentOptions?.tabBarActiveIndicatorColor ??
270
- typeof activeTintColor === 'string')
271
- ? Color(activeTintColor)?.alpha(0.1).string()
272
- : undefined;
288
+ const inactiveTintColor =
289
+ currentOptions.tabBarInactiveTintColor ??
290
+ Platform.select<ColorValue | string>({
291
+ ios: PlatformColor('label'),
292
+ default: colors.text,
293
+ });
273
294
 
274
- const onTransitionStart = ({
275
- closing,
276
- route,
277
- }: {
278
- closing: boolean;
279
- route: Route<string>;
280
- }) => {
281
- navigation.emit({
282
- type: 'transitionStart',
283
- data: { closing },
284
- target: route.key,
285
- });
286
- };
287
-
288
- const onTransitionEnd = ({
289
- closing,
290
- route,
291
- }: {
292
- closing: boolean;
293
- route: Route<string>;
294
- }) => {
295
- navigation.emit({
296
- type: 'transitionEnd',
297
- data: { closing },
298
- target: route.key,
299
- });
300
- };
295
+ const activeIndicatorColor =
296
+ currentOptions?.tabBarActiveIndicatorColor ??
297
+ (typeof activeTintColor === 'string'
298
+ ? Color(activeTintColor)?.alpha(0.1).string()
299
+ : undefined);
300
+
301
+ return { activeIndicatorColor, activeTintColor, inactiveTintColor };
302
+ }, [
303
+ currentOptions?.tabBarActiveIndicatorColor,
304
+ currentOptions.tabBarActiveTintColor,
305
+ currentOptions.tabBarInactiveTintColor,
306
+ colors.primary,
307
+ colors.text,
308
+ ]);
301
309
 
302
310
  const tabBarControllerMode =
303
311
  currentOptions.tabBarControllerMode === 'auto'
@@ -311,6 +319,17 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
311
319
  ? 'never'
312
320
  : currentOptions.tabBarMinimizeBehavior;
313
321
 
322
+ const tabBarSidebarPreferredPlacement =
323
+ currentOptions.tabBarSidebarPreferredPlacement === 'auto'
324
+ ? 'automatic'
325
+ : currentOptions.tabBarSidebarPreferredPlacement;
326
+
327
+ const prominentScreenKey = SUPPORTS_PROMINENT_TAB
328
+ ? state.routes.find(
329
+ (route) => descriptors[route.key]?.options.tabBarRole === 'prominent'
330
+ )?.key
331
+ : undefined;
332
+
314
333
  const shouldHideTabBar = currentOptions.tabBarStyle?.display === 'none';
315
334
 
316
335
  const bottomAccessory = currentOptions.bottomAccessory;
@@ -330,180 +349,49 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
330
349
  colorScheme={dark ? 'dark' : 'light'}
331
350
  nativeContainerStyle={{ backgroundColor: colors.background }}
332
351
  ios={{
352
+ prominentScreenKey,
333
353
  bottomAccessory: bottomAccessory
334
354
  ? (environment: TabsBottomAccessoryEnvironment) =>
335
355
  bottomAccessory({ placement: environment })
336
356
  : undefined,
357
+ tabBarHiddenAnimationEnabled:
358
+ currentOptions.tabBarVisibilityAnimationEnabled,
337
359
  tabBarControllerMode,
360
+ tabBarSidebarPreferredPlacement,
338
361
  tabBarMinimizeBehavior,
339
362
  tabBarTintColor: activeTintColor,
340
363
  }}
341
364
  >
342
365
  {state.routes.map((route, index) => {
343
- const { options, render, navigation } = descriptors[route.key];
366
+ const descriptor = descriptors[route.key];
344
367
  const isFocused = state.index === index;
345
368
  const isPreloaded = state.preloadedRouteKeys.includes(route.key);
346
369
 
347
- const {
348
- title,
349
- lazy = false,
350
- tabBarLabel,
351
- tabBarSelectionEnabled,
352
- tabBarBadgeStyle,
353
- tabBarIcon,
354
- tabBarBadge,
355
- tabBarSystemItem,
356
- tabBarBlurEffect = dark ? 'systemMaterialDark' : 'systemMaterial',
357
- tabBarStyle,
358
- overrideScrollViewContentInsetAdjustmentBehavior,
359
- } = options;
360
-
361
- const {
362
- backgroundColor: tabBarBackgroundColor,
363
- shadowColor: tabBarShadowColor,
364
- } = tabBarStyle || {};
365
-
366
- const tabTitle =
367
- // On iOS, `systemItem` already provides a localized label
368
- // So we should only use `tabBarLabel` if explicitly provided
369
- Platform.OS === 'ios' && tabBarSystemItem != null
370
- ? tabBarLabel
371
- : getLabel({ label: tabBarLabel, title }, route.name);
372
-
373
- const badgeBackgroundColor =
374
- tabBarBadgeStyle?.backgroundColor ?? colors.notification;
375
- const badgeTextColor =
376
- tabBarBadgeStyle?.color ??
377
- (typeof badgeBackgroundColor === 'string'
378
- ? Color(badgeBackgroundColor).isLight()
379
- ? 'black'
380
- : 'white'
381
- : undefined);
382
-
383
- const tabItemAppearance: TabsScreenItemStateAppearanceIOS = {
384
- tabBarItemTitleFontFamily: fontFamily,
385
- tabBarItemTitleFontSize: fontSize,
386
- tabBarItemTitleFontWeight: fontWeight,
387
- tabBarItemTitleFontStyle: fontStyle,
388
- tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
389
- tabBarItemIconColor: inactiveTintColor,
390
- tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
391
- };
392
-
393
- const normalTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
394
- {
395
- tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
396
- tabBarItemIconColor: inactiveTintColor,
397
- };
398
-
399
- const selectedTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
400
- {
401
- tabBarItemTitleFontColor: activeTintColor,
402
- tabBarItemIconColor: activeTintColor,
403
- };
404
-
405
- const icon =
406
- typeof tabBarIcon === 'function'
407
- ? getPlatformIcon(tabBarIcon({ focused: false }))
408
- : tabBarIcon != null
409
- ? getPlatformIcon(tabBarIcon)
410
- : undefined;
411
-
412
- const selectedIcon =
413
- typeof tabBarIcon === 'function'
414
- ? getPlatformIcon(tabBarIcon({ focused: true }))
415
- : undefined;
370
+ const { lazy = false } = descriptor.options;
416
371
 
417
372
  return (
418
- <Tabs.Screen
419
- onWillDisappear={() =>
420
- onTransitionStart({ closing: true, route })
421
- }
422
- onWillAppear={() => onTransitionStart({ closing: false, route })}
423
- onDidAppear={() => onTransitionEnd({ closing: false, route })}
424
- onDidDisappear={() => onTransitionEnd({ closing: true, route })}
373
+ <TabScene
425
374
  key={route.key}
426
- screenKey={route.key}
427
- preventNativeSelection={tabBarSelectionEnabled === false}
428
- badgeValue={tabBarBadge?.toString()}
429
- title={tabTitle}
430
- nativeContainerStyle={{ backgroundColor: colors.background }}
431
- specialEffects={{
432
- repeatedTabSelection: {
433
- popToRoot: true,
434
- scrollToTop: true,
435
- },
436
- }}
437
- android={{
438
- icon: icon?.android ?? icon?.shared,
439
- selectedIcon: selectedIcon?.android ?? selectedIcon?.shared,
440
- standardAppearance: {
441
- tabBarBackgroundColor: tabBarBackgroundColor ?? colors.card,
442
- tabBarItemRippleColor: currentOptions?.tabBarRippleColor,
443
- tabBarItemLabelVisibilityMode:
444
- currentOptions?.tabBarLabelVisibilityMode,
445
- normal: normalTabItemAppearance,
446
- selected: selectedTabItemAppearance,
447
- tabBarItemActiveIndicatorColor: activeIndicatorColor,
448
- tabBarItemActiveIndicatorEnabled:
449
- currentOptions?.tabBarActiveIndicatorEnabled,
450
- tabBarItemTitleFontFamily: fontFamily,
451
- tabBarItemTitleFontWeight: fontWeight,
452
- tabBarItemTitleSmallLabelFontSize: fontSize,
453
- tabBarItemTitleLargeLabelFontSize: fontSize,
454
- tabBarItemTitleFontStyle: fontStyle,
455
- tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
456
- tabBarItemBadgeTextColor: badgeTextColor,
457
- },
458
- }}
459
- ios={{
460
- icon: icon?.ios ?? icon?.shared,
461
- selectedIcon: selectedIcon?.ios ?? selectedIcon?.shared,
462
- systemItem: tabBarSystemItem,
463
- scrollEdgeAppearance: {
464
- tabBarBackgroundColor,
465
- tabBarShadowColor,
466
- tabBarBlurEffect,
467
- stacked: {
468
- normal: tabItemAppearance,
469
- },
470
- inline: {
471
- normal: tabItemAppearance,
472
- },
473
- compactInline: {
474
- normal: tabItemAppearance,
475
- },
476
- },
477
- standardAppearance: {
478
- tabBarBackgroundColor,
479
- tabBarShadowColor,
480
- tabBarBlurEffect,
481
- stacked: {
482
- normal: tabItemAppearance,
483
- },
484
- inline: {
485
- normal: tabItemAppearance,
486
- },
487
- compactInline: {
488
- normal: tabItemAppearance,
489
- },
490
- },
491
- overrideScrollViewContentInsetAdjustmentBehavior,
492
- }}
493
- >
494
- <Deferred lazy={lazy} visible={isFocused || isPreloaded}>
495
- <ScreenWithHeader
496
- isFocused={isFocused}
497
- route={route}
498
- navigation={navigation}
499
- options={options}
500
- >
501
- <NavigationMetaContext.Provider value={meta}>
502
- {render()}
503
- </NavigationMetaContext.Provider>
504
- </ScreenWithHeader>
505
- </Deferred>
506
- </Tabs.Screen>
375
+ route={route}
376
+ descriptor={descriptor}
377
+ navigation={navigation}
378
+ isFocused={isFocused}
379
+ lazy={lazy}
380
+ visible={isFocused || isPreloaded}
381
+ fontFamily={fontFamily}
382
+ fontWeight={fontWeight}
383
+ fontSize={fontSize}
384
+ fontStyle={fontStyle}
385
+ fontColor={fontColor}
386
+ activeTintColor={activeTintColor}
387
+ inactiveTintColor={inactiveTintColor}
388
+ activeIndicatorColor={activeIndicatorColor}
389
+ rippleColor={currentOptions.tabBarRippleColor}
390
+ labelVisibilityMode={currentOptions.tabBarLabelVisibilityMode}
391
+ activeIndicatorEnabled={
392
+ currentOptions.tabBarActiveIndicatorEnabled
393
+ }
394
+ />
507
395
  );
508
396
  })}
509
397
  </Tabs.Host>
@@ -511,6 +399,258 @@ export function NativeBottomTabView({ state, navigation, descriptors }: Props) {
511
399
  );
512
400
  }
513
401
 
402
+ type TabSceneProps = {
403
+ route: NavigationRoute<ParamListBase, string>;
404
+ descriptor: NativeBottomTabDescriptor;
405
+ navigation: NativeBottomTabNavigationHelpers;
406
+ isFocused: boolean;
407
+ lazy: boolean;
408
+ visible: boolean;
409
+ fontFamily: TextStyle['fontFamily'];
410
+ fontWeight: TextStyle['fontWeight'];
411
+ fontSize: TextStyle['fontSize'];
412
+ fontStyle: TextStyle['fontStyle'];
413
+ fontColor: TextStyle['color'];
414
+ activeTintColor: ColorValue;
415
+ inactiveTintColor: ColorValue;
416
+ activeIndicatorColor: ColorValue | undefined;
417
+ rippleColor: ColorValue | undefined;
418
+ labelVisibilityMode: NativeBottomTabNavigationOptions['tabBarLabelVisibilityMode'];
419
+ activeIndicatorEnabled: boolean | undefined;
420
+ };
421
+
422
+ const TabScene = React.memo(function TabScene({
423
+ route,
424
+ descriptor,
425
+ navigation,
426
+ isFocused,
427
+ lazy,
428
+ visible,
429
+ fontFamily,
430
+ fontWeight,
431
+ fontSize,
432
+ fontStyle,
433
+ fontColor,
434
+ activeTintColor,
435
+ inactiveTintColor,
436
+ activeIndicatorColor,
437
+ rippleColor,
438
+ labelVisibilityMode,
439
+ activeIndicatorEnabled,
440
+ }: TabSceneProps) {
441
+ const { dark, colors } = useTheme();
442
+
443
+ const onTransitionStart = ({
444
+ closing,
445
+ route,
446
+ }: {
447
+ closing: boolean;
448
+ route: Route<string>;
449
+ }) => {
450
+ navigation.emit({
451
+ type: 'transitionStart',
452
+ data: { closing },
453
+ target: route.key,
454
+ });
455
+ };
456
+
457
+ const onTransitionEnd = ({
458
+ closing,
459
+ route,
460
+ }: {
461
+ closing: boolean;
462
+ route: Route<string>;
463
+ }) => {
464
+ navigation.emit({
465
+ type: 'transitionEnd',
466
+ data: { closing },
467
+ target: route.key,
468
+ });
469
+ };
470
+
471
+ const { options, render } = descriptor;
472
+
473
+ const {
474
+ title,
475
+ tabBarLabel,
476
+ tabBarLabelPositionAdjustment,
477
+ tabBarSelectionEnabled,
478
+ tabBarRepeatedPressBehavior,
479
+ tabBarBadgeStyle,
480
+ tabBarIcon,
481
+ tabBarIconSize,
482
+ tabBarActiveIndicatorWidth,
483
+ tabBarActiveIndicatorHeight,
484
+ tabBarBadge,
485
+ tabBarSystemItem,
486
+ tabBarRole,
487
+ tabBarSearchAutoFocus,
488
+ tabBarBlurEffect = dark ? 'systemMaterialDark' : 'systemMaterial',
489
+ tabBarStyle,
490
+ overrideScrollViewContentInsetAdjustmentBehavior,
491
+ } = options;
492
+
493
+ const systemItem =
494
+ tabBarSystemItem ??
495
+ // Setting the `role` to 'search' doesn't add title and icon automatically
496
+ // So we also set `tabBarSystemItem` to 'search' to match SwiftUI's behavior
497
+ (tabBarRole === 'search' ? 'search' : undefined);
498
+
499
+ const {
500
+ backgroundColor: tabBarBackgroundColor,
501
+ shadowColor: tabBarShadowColor,
502
+ } = tabBarStyle || {};
503
+
504
+ const tabTitle =
505
+ // On iOS, `systemItem` already provides a localized label
506
+ // So we should only use `tabBarLabel` if explicitly provided
507
+ Platform.OS === 'ios' && systemItem != null
508
+ ? tabBarLabel
509
+ : getLabel({ label: tabBarLabel, title }, route.name);
510
+
511
+ const badgeBackgroundColor =
512
+ tabBarBadgeStyle?.backgroundColor ?? colors.notification;
513
+ const badgeTextColor =
514
+ tabBarBadgeStyle?.color ??
515
+ (typeof badgeBackgroundColor === 'string'
516
+ ? Color(badgeBackgroundColor).isLight()
517
+ ? 'black'
518
+ : 'white'
519
+ : undefined);
520
+
521
+ const tabItemAppearance: TabsScreenItemStateAppearanceIOS = {
522
+ tabBarItemTitleFontFamily: fontFamily,
523
+ tabBarItemTitleFontSize: fontSize,
524
+ tabBarItemTitleFontWeight: fontWeight,
525
+ tabBarItemTitleFontStyle: fontStyle,
526
+ tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
527
+ tabBarItemIconColor: inactiveTintColor,
528
+ tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
529
+ };
530
+
531
+ const tabItemLayoutAppearance = {
532
+ stacked: {
533
+ normal: {
534
+ ...tabItemAppearance,
535
+ tabBarItemTitlePositionAdjustment:
536
+ tabBarLabelPositionAdjustment?.stacked,
537
+ },
538
+ },
539
+ inline: {
540
+ normal: {
541
+ ...tabItemAppearance,
542
+ tabBarItemTitlePositionAdjustment:
543
+ tabBarLabelPositionAdjustment?.inline,
544
+ },
545
+ },
546
+ compactInline: {
547
+ normal: {
548
+ ...tabItemAppearance,
549
+ tabBarItemTitlePositionAdjustment:
550
+ tabBarLabelPositionAdjustment?.compactInline,
551
+ },
552
+ },
553
+ };
554
+
555
+ const normalTabItemAppearance: TabsScreenItemStateAppearanceAndroid = {
556
+ tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
557
+ tabBarItemIconColor: inactiveTintColor,
558
+ };
559
+
560
+ const selectedTabItemAppearance: TabsScreenItemStateAppearanceAndroid = {
561
+ tabBarItemTitleFontColor: activeTintColor,
562
+ tabBarItemIconColor: activeTintColor,
563
+ };
564
+
565
+ const icon =
566
+ typeof tabBarIcon === 'function'
567
+ ? getPlatformIcon(tabBarIcon({ focused: false }))
568
+ : tabBarIcon != null
569
+ ? getPlatformIcon(tabBarIcon)
570
+ : undefined;
571
+
572
+ const selectedIcon =
573
+ typeof tabBarIcon === 'function'
574
+ ? getPlatformIcon(tabBarIcon({ focused: true }))
575
+ : undefined;
576
+
577
+ return (
578
+ <Tabs.Screen
579
+ onWillDisappear={() => onTransitionStart({ closing: true, route })}
580
+ onWillAppear={() => onTransitionStart({ closing: false, route })}
581
+ onDidAppear={() => onTransitionEnd({ closing: false, route })}
582
+ onDidDisappear={() => onTransitionEnd({ closing: true, route })}
583
+ screenKey={route.key}
584
+ preventNativeSelection={tabBarSelectionEnabled === false}
585
+ badgeValue={tabBarBadge?.toString()}
586
+ title={tabTitle}
587
+ style={{ backgroundColor: colors.background }}
588
+ specialEffects={{
589
+ repeatedTabSelection: {
590
+ popToRoot: tabBarRepeatedPressBehavior?.popToTop !== false,
591
+ scrollToTop: tabBarRepeatedPressBehavior?.scrollToTop !== false,
592
+ },
593
+ }}
594
+ android={{
595
+ icon: icon?.android ?? icon?.shared,
596
+ selectedIcon: selectedIcon?.android ?? selectedIcon?.shared,
597
+ standardAppearance: {
598
+ tabBarBackgroundColor: tabBarBackgroundColor ?? colors.card,
599
+ tabBarItemRippleColor: rippleColor,
600
+ tabBarItemLabelVisibilityMode: labelVisibilityMode,
601
+ normal: normalTabItemAppearance,
602
+ selected: selectedTabItemAppearance,
603
+ tabBarItemIconSize: tabBarIconSize,
604
+ tabBarItemActiveIndicatorColor: activeIndicatorColor,
605
+ tabBarItemActiveIndicatorEnabled: activeIndicatorEnabled,
606
+ tabBarItemActiveIndicatorWidth: tabBarActiveIndicatorWidth,
607
+ tabBarItemActiveIndicatorHeight: tabBarActiveIndicatorHeight,
608
+ tabBarItemTitleFontFamily: fontFamily,
609
+ tabBarItemTitleFontWeight: fontWeight,
610
+ tabBarItemTitleSmallLabelFontSize: fontSize,
611
+ tabBarItemTitleLargeLabelFontSize: fontSize,
612
+ tabBarItemTitleFontStyle: fontStyle,
613
+ tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
614
+ tabBarItemBadgeTextColor: badgeTextColor,
615
+ },
616
+ }}
617
+ ios={{
618
+ icon: icon?.ios ?? icon?.shared,
619
+ selectedIcon: selectedIcon?.ios ?? selectedIcon?.shared,
620
+ systemItem,
621
+ role: tabBarRole === 'search' ? tabBarRole : undefined,
622
+ automaticallyActivatesSearch: tabBarSearchAutoFocus,
623
+ scrollEdgeAppearance: {
624
+ tabBarBackgroundColor,
625
+ tabBarShadowColor,
626
+ tabBarBlurEffect,
627
+ ...tabItemLayoutAppearance,
628
+ },
629
+ standardAppearance: {
630
+ tabBarBackgroundColor,
631
+ tabBarShadowColor,
632
+ tabBarBlurEffect,
633
+ ...tabItemLayoutAppearance,
634
+ },
635
+ overrideScrollViewContentInsetAdjustmentBehavior,
636
+ }}
637
+ >
638
+ <Deferred lazy={lazy} visible={visible}>
639
+ <ScreenWithHeader
640
+ isFocused={isFocused}
641
+ route={route}
642
+ navigation={descriptor.navigation}
643
+ options={options}
644
+ >
645
+ <NavigationMetaContext.Provider value={meta}>
646
+ {render()}
647
+ </NavigationMetaContext.Provider>
648
+ </ScreenWithHeader>
649
+ </Deferred>
650
+ </Tabs.Screen>
651
+ );
652
+ });
653
+
514
654
  function ScreenWithHeader({
515
655
  isFocused,
516
656
  route,
@@ -572,28 +712,47 @@ function getPlatformIcon(icon: NativeBottomTabIcon): PlatformIcon {
572
712
  switch (icon.type) {
573
713
  case 'sfSymbol':
574
714
  return {
575
- ios: icon,
715
+ ios: {
716
+ type: 'sfSymbol',
717
+ name: icon.name,
718
+ renderingMode:
719
+ icon.renderingMode === 'multicolor'
720
+ ? 'original'
721
+ : icon.renderingMode,
722
+ },
576
723
  android: undefined,
577
724
  shared: undefined,
578
725
  };
579
- case 'image':
726
+ case 'image': {
727
+ // A `{ uri: 'name' }` source maps to an Android drawable by name.
728
+ const drawableName =
729
+ typeof icon.source === 'object' &&
730
+ 'uri' in icon.source &&
731
+ icon.source.uri?.includes(':') === false
732
+ ? icon.source.uri
733
+ : undefined;
734
+
580
735
  return {
581
- ios:
582
- icon.tinted === false
736
+ ios: {
737
+ type: 'imageSource',
738
+ imageSource: icon.source,
739
+ renderingMode: icon.tinted === false ? 'original' : 'template',
740
+ },
741
+ android:
742
+ drawableName != null
583
743
  ? {
584
- type: 'imageSource',
585
- imageSource: icon.source,
744
+ type: 'drawableResource',
745
+ name: drawableName,
746
+ tinting: icon.tinted === false ? 'original' : 'tinted',
586
747
  }
587
748
  : {
588
- type: 'templateSource',
589
- templateSource: icon.source,
749
+ type: 'imageSource',
750
+ imageSource: icon.source,
751
+ tinting: icon.tinted === false ? 'original' : 'tinted',
590
752
  },
591
- android: undefined,
592
- shared: {
593
- type: 'imageSource',
594
- imageSource: icon.source,
595
- },
753
+ shared: undefined,
596
754
  };
755
+ }
597
756
  default: {
598
757
  const _exhaustiveCheck: never = icon;
599
758