@react-navigation/bottom-tabs 7.20.0 → 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 +288 -196
  4. package/lib/module/unstable/NativeBottomTabView.native.js.map +1 -1
  5. package/lib/module/views/BottomTabBar.js +111 -64
  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 +78 -57
  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 +4 -0
  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 +124 -4
  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 +5 -0
  27. package/src/unstable/Deferred.tsx +3 -11
  28. package/src/unstable/NativeBottomTabView.native.tsx +356 -218
  29. package/src/unstable/types.tsx +133 -4
  30. package/src/views/BottomTabBar.tsx +153 -82
  31. package/src/views/BottomTabItem.tsx +6 -0
  32. package/src/views/BottomTabView.tsx +100 -66
  33. package/src/views/TabBarIcon.tsx +9 -2
@@ -101,8 +101,6 @@ type IconImage = {
101
101
  * Disabling will keep the image's original colors.
102
102
  *
103
103
  * Defaults to `true`.
104
- *
105
- * @platform ios
106
104
  */
107
105
  tinted?: boolean;
108
106
  };
@@ -118,10 +116,25 @@ type IconIOSSfSymbol = {
118
116
  * @platform ios
119
117
  */
120
118
  name: SFSymbol;
119
+ /**
120
+ * How to color the symbol.
121
+ * - `monochrome`: Single color, using the tint color.
122
+ * - `multicolor`: Uses the symbol's built-in multicolor scheme.
123
+ *
124
+ * Defaults to a single color for system symbols.
125
+ *
126
+ * @platform ios
127
+ */
128
+ renderingMode?: 'monochrome' | 'multicolor';
121
129
  };
122
130
 
123
131
  export type NativeBottomTabIcon = IconIOSSfSymbol | IconImage;
124
132
 
133
+ type TabBarLabelOffset = {
134
+ horizontal?: number;
135
+ vertical?: number;
136
+ };
137
+
125
138
  export type NativeBottomTabNavigationOptions = NativeHeaderOptions & {
126
139
  /**
127
140
  * Title text for the screen.
@@ -130,16 +143,90 @@ export type NativeBottomTabNavigationOptions = NativeHeaderOptions & {
130
143
 
131
144
  /**
132
145
  * Uses iOS built-in tab bar items with standard iOS styling and localized titles.
133
- * If set to `search`, it's positioned next to the tab bar on iOS 26 and above.
134
146
  *
135
- * The `tabBarIcon` and `tabBarLabel` options will override the icon and label from the system item.
147
+ * The `tabBarIcon` and `tabBarLabel` options override the icon and label from the system item.
136
148
  * If you want to keep the system behavior on iOS, but need to provide icon and label for other platforms,
137
149
  * Use `Platform.OS` or `Platform.select` to conditionally set `undefined` for `tabBarIcon` and `tabBarLabel` on iOS.
138
150
  *
151
+ * If set to `search`, it may receive the prominent visual treatment on iOS 26.
152
+ * See `tabBarRole` for more details on iOS 27 and above.
153
+ *
139
154
  * @platform ios
140
155
  */
141
156
  tabBarSystemItem?: TabsScreenSystemItem;
142
157
 
158
+ /**
159
+ * The role of the tab within the system tab bar.
160
+ *
161
+ * Supported values:
162
+ * - `prominent` - the tab gets the prominent visual treatment when supported
163
+ * - `search` - the tab is used as the system search tab
164
+ *
165
+ * If set to `prominent`, the tab may receive special styling to make it stand out,
166
+ * e.g., being positioned as a separate button next to the tab bar on iOS 27 and above.
167
+ * Only one tab can have the `prominent` role at a time.
168
+ * Supported on iOS 27 and above.
169
+ *
170
+ * If set to `search`, the tab gets the prominent visual treatment on iOS 26.
171
+ * On iOS 27 and above, it gets the prominent styling only if `tabBarSearchAutoFocus` is enabled,
172
+ * and no other tab has the `prominent` role.
173
+ * The tab uses system search icon and label by default.
174
+ * They can be overridden by `tabBarSystemItem`, `tabBarIcon`, and `tabBarLabel` options.
175
+ * Adding or removing the `search` role after the tab is rendered is not supported.
176
+ * Supported on iOS 26.1 and above.
177
+ *
178
+ * @platform ios
179
+ */
180
+ tabBarRole?: 'prominent' | 'search';
181
+
182
+ /**
183
+ * Whether the search field is automatically focused when the tab is selected.
184
+ * Cancelling the search selects the previously selected tab.
185
+ *
186
+ * Only applies to tabs with `tabBarRole: 'search'`,
187
+ * and requires `headerSearchBarOptions` to be set in a native stack nested in the tab.
188
+ *
189
+ * Defaults to `false`.
190
+ *
191
+ * Supported on iOS 26.1 and above.
192
+ *
193
+ * @platform ios
194
+ */
195
+ tabBarSearchAutoFocus?: boolean;
196
+
197
+ /**
198
+ * Whether the sidebar or the tab bar is preferred when only one of them can be displayed.
199
+ *
200
+ * Supported values:
201
+ * - `auto` - uses the system default (tab bar)
202
+ * - `sidebar` - the sidebar is displayed when it's available
203
+ * - `tabBar` - the tab bar is displayed
204
+ *
205
+ * Requires `tabBarControllerMode` to be set to `tabSidebar`.
206
+ *
207
+ * The sidebar is only available when the horizontal size class is regular,
208
+ * e.g. on iPad, or on larger iPhones in landscape.
209
+ *
210
+ * Defaults to `auto`.
211
+ *
212
+ * Supported on iOS 27 and above.
213
+ * Not supported on tvOS.
214
+ *
215
+ * @platform ios
216
+ */
217
+ tabBarSidebarPreferredPlacement?: 'auto' | 'sidebar' | 'tabBar';
218
+
219
+ /**
220
+ * Whether showing and hiding the tab bar is animated.
221
+ *
222
+ * Defaults to `true`.
223
+ *
224
+ * Supported on iOS 18 and above.
225
+ *
226
+ * @platform ios
227
+ */
228
+ tabBarVisibilityAnimationEnabled?: boolean;
229
+
143
230
  /**
144
231
  * Title string of the tab displayed in the tab bar
145
232
  *
@@ -186,6 +273,16 @@ export type NativeBottomTabNavigationOptions = NativeHeaderOptions & {
186
273
  | NativeBottomTabIcon
187
274
  | ((props: { focused: boolean }) => NativeBottomTabIcon);
188
275
 
276
+ /**
277
+ * Size of the tab's icon.
278
+ *
279
+ * The largest size is used for the icon area in all tabs.
280
+ * Icons smaller than the largest size are centered in this area.
281
+ *
282
+ * @platform android
283
+ */
284
+ tabBarIconSize?: number;
285
+
189
286
  /**
190
287
  * Text to show in a badge on the tab icon.
191
288
  */
@@ -228,6 +325,20 @@ export type NativeBottomTabNavigationOptions = NativeHeaderOptions & {
228
325
  */
229
326
  tabBarActiveIndicatorEnabled?: boolean;
230
327
 
328
+ /**
329
+ * Width of the tab's active indicator.
330
+ *
331
+ * @platform android
332
+ */
333
+ tabBarActiveIndicatorWidth?: number;
334
+
335
+ /**
336
+ * Height of the tab's active indicator.
337
+ *
338
+ * @platform android
339
+ */
340
+ tabBarActiveIndicatorHeight?: number;
341
+
231
342
  /**
232
343
  * Color of tab bar item's ripple effect.
233
344
  *
@@ -347,6 +458,24 @@ export type NativeBottomTabNavigationOptions = NativeHeaderOptions & {
347
458
  */
348
459
  tabBarMinimizeBehavior?: 'auto' | 'none' | 'onScrollDown' | 'onScrollUp';
349
460
 
461
+ /**
462
+ * Offset applied to the tab label relative to its default position.
463
+ * Positive `vertical` moves the label down, positive `horizontal` moves it right.
464
+ *
465
+ * The offset is specified separately for each tab bar item layout:
466
+ *
467
+ * - `stacked` - icon above the label, e.g. on iPhone in portrait
468
+ * - `inline` - icon beside the label in regular width, e.g. on iPad
469
+ * - `compactInline` - icon beside the label in compact width, e.g. on iPhone in landscape
470
+ *
471
+ * @platform ios
472
+ */
473
+ tabBarLabelPositionAdjustment?: {
474
+ stacked?: TabBarLabelOffset;
475
+ inline?: TabBarLabelOffset;
476
+ compactInline?: TabBarLabelOffset;
477
+ };
478
+
350
479
  /**
351
480
  * Function which returns a React element to display as an accessory view.
352
481
  *
@@ -25,7 +25,13 @@ import {
25
25
  } from 'react-native';
26
26
  import { type EdgeInsets } from 'react-native-safe-area-context';
27
27
 
28
- import type { BottomTabBarProps, BottomTabDescriptorMap } from '../types';
28
+ import type {
29
+ BottomTabBarProps,
30
+ BottomTabDescriptor,
31
+ BottomTabDescriptorMap,
32
+ LabelVisibilityMode,
33
+ Variant,
34
+ } from '../types';
29
35
  import { BottomTabBarHeightCallbackContext } from '../utils/BottomTabBarHeightCallbackContext';
30
36
  import { useIsKeyboardShown } from '../utils/useIsKeyboardShown';
31
37
  import { BottomTabItem } from './BottomTabItem';
@@ -396,38 +402,6 @@ export function BottomTabBar({
396
402
  const focused = index === state.index;
397
403
  const { options } = descriptors[route.key];
398
404
 
399
- const onPress = () => {
400
- const event = navigation.emit({
401
- type: 'tabPress',
402
- target: route.key,
403
- canPreventDefault: true,
404
- data: {
405
- behavior: {
406
- scrollToTop:
407
- focused &&
408
- options.tabBarRepeatedPressBehavior?.scrollToTop !== false,
409
- popToTop:
410
- focused &&
411
- options.tabBarRepeatedPressBehavior?.popToTop !== false,
412
- },
413
- },
414
- });
415
-
416
- if (!focused && !event.defaultPrevented) {
417
- navigation.dispatch({
418
- ...CommonActions.navigate(route.name, route.params),
419
- target: state.key,
420
- });
421
- }
422
- };
423
-
424
- const onLongPress = () => {
425
- navigation.emit({
426
- type: 'tabLongPress',
427
- target: route.key,
428
- });
429
- };
430
-
431
405
  const label =
432
406
  typeof options.tabBarLabel === 'function'
433
407
  ? options.tabBarLabel
@@ -444,57 +418,28 @@ export function BottomTabBar({
444
418
  : undefined;
445
419
 
446
420
  return (
447
- <NavigationProvider
421
+ <BottomTabBarItem
448
422
  key={route.key}
449
423
  route={route}
450
- navigation={descriptors[route.key].navigation}
451
- >
452
- <BottomTabItem
453
- href={buildHref(route.name, route.params)}
454
- route={route}
455
- descriptor={descriptors[route.key]}
456
- focused={focused}
457
- horizontal={hasHorizontalLabels}
458
- compact={compact}
459
- sidebar={sidebar}
460
- variant={tabBarVariant}
461
- onPress={onPress}
462
- onLongPress={onLongPress}
463
- accessibilityLabel={accessibilityLabel}
464
- testID={options.tabBarButtonTestID}
465
- allowFontScaling={options.tabBarAllowFontScaling}
466
- activeTintColor={tabBarActiveTintColor}
467
- inactiveTintColor={tabBarInactiveTintColor}
468
- activeBackgroundColor={tabBarActiveBackgroundColor}
469
- inactiveBackgroundColor={tabBarInactiveBackgroundColor}
470
- button={options.tabBarButton}
471
- icon={
472
- options.tabBarIcon ??
473
- (({ color, size }) => (
474
- <MissingIcon color={color} size={size} />
475
- ))
476
- }
477
- badge={options.tabBarBadge}
478
- badgeStyle={options.tabBarBadgeStyle}
479
- label={label}
480
- showLabel={tabBarShowLabel}
481
- labelVisibilityMode={tabBarLabelVisibilityMode}
482
- labelStyle={options.tabBarLabelStyle}
483
- iconStyle={options.tabBarIconStyle}
484
- style={[
485
- sidebar
486
- ? {
487
- marginVertical: hasHorizontalLabels
488
- ? tabBarVariant === 'material'
489
- ? 0
490
- : 1
491
- : spacing / 2,
492
- }
493
- : styles.bottomItem,
494
- options.tabBarItemStyle,
495
- ]}
496
- />
497
- </NavigationProvider>
424
+ descriptor={descriptors[route.key]}
425
+ navigation={navigation}
426
+ navigatorKey={state.key}
427
+ href={buildHref(route.name, route.params)}
428
+ focused={focused}
429
+ label={label}
430
+ accessibilityLabel={accessibilityLabel}
431
+ hasHorizontalLabels={hasHorizontalLabels}
432
+ compact={compact}
433
+ sidebar={sidebar}
434
+ tabBarVariant={tabBarVariant}
435
+ spacing={spacing}
436
+ tabBarActiveTintColor={tabBarActiveTintColor}
437
+ tabBarInactiveTintColor={tabBarInactiveTintColor}
438
+ tabBarActiveBackgroundColor={tabBarActiveBackgroundColor}
439
+ tabBarInactiveBackgroundColor={tabBarInactiveBackgroundColor}
440
+ tabBarShowLabel={tabBarShowLabel}
441
+ tabBarLabelVisibilityMode={tabBarLabelVisibilityMode}
442
+ />
498
443
  );
499
444
  })}
500
445
  </View>
@@ -502,6 +447,132 @@ export function BottomTabBar({
502
447
  );
503
448
  }
504
449
 
450
+ type BottomTabBarItemProps = {
451
+ route: TabNavigationState<ParamListBase>['routes'][number];
452
+ descriptor: BottomTabDescriptor;
453
+ navigation: BottomTabBarProps['navigation'];
454
+ navigatorKey: string;
455
+ href: string | undefined;
456
+ focused: boolean;
457
+ label: React.ComponentProps<typeof BottomTabItem>['label'];
458
+ accessibilityLabel: string | undefined;
459
+ hasHorizontalLabels: boolean;
460
+ compact: boolean;
461
+ sidebar: boolean;
462
+ tabBarVariant: Variant;
463
+ spacing: number;
464
+ tabBarActiveTintColor: string | undefined;
465
+ tabBarInactiveTintColor: string | undefined;
466
+ tabBarActiveBackgroundColor: string | undefined;
467
+ tabBarInactiveBackgroundColor: string | undefined;
468
+ tabBarShowLabel: boolean | undefined;
469
+ tabBarLabelVisibilityMode: LabelVisibilityMode | undefined;
470
+ };
471
+
472
+ const BottomTabBarItem = React.memo(function BottomTabBarItem({
473
+ route,
474
+ descriptor,
475
+ navigation,
476
+ navigatorKey,
477
+ href,
478
+ focused,
479
+ label,
480
+ accessibilityLabel,
481
+ hasHorizontalLabels,
482
+ compact,
483
+ sidebar,
484
+ tabBarVariant,
485
+ spacing,
486
+ tabBarActiveTintColor,
487
+ tabBarInactiveTintColor,
488
+ tabBarActiveBackgroundColor,
489
+ tabBarInactiveBackgroundColor,
490
+ tabBarShowLabel,
491
+ tabBarLabelVisibilityMode,
492
+ }: BottomTabBarItemProps) {
493
+ const { options } = descriptor;
494
+
495
+ const onPress = () => {
496
+ const event = navigation.emit({
497
+ type: 'tabPress',
498
+ target: route.key,
499
+ canPreventDefault: true,
500
+ data: {
501
+ behavior: {
502
+ scrollToTop:
503
+ focused &&
504
+ options.tabBarRepeatedPressBehavior?.scrollToTop !== false,
505
+ popToTop:
506
+ focused && options.tabBarRepeatedPressBehavior?.popToTop !== false,
507
+ },
508
+ },
509
+ });
510
+
511
+ if (!focused && !event.defaultPrevented) {
512
+ navigation.dispatch({
513
+ ...CommonActions.navigate(route.name, route.params),
514
+ target: navigatorKey,
515
+ });
516
+ }
517
+ };
518
+
519
+ const onLongPress = () => {
520
+ navigation.emit({
521
+ type: 'tabLongPress',
522
+ target: route.key,
523
+ });
524
+ };
525
+
526
+ return (
527
+ <NavigationProvider route={route} navigation={descriptor.navigation}>
528
+ <BottomTabItem
529
+ href={href}
530
+ route={route}
531
+ descriptor={descriptor}
532
+ focused={focused}
533
+ horizontal={hasHorizontalLabels}
534
+ compact={compact}
535
+ sidebar={sidebar}
536
+ variant={tabBarVariant}
537
+ onPress={onPress}
538
+ onLongPress={onLongPress}
539
+ accessibilityLabel={accessibilityLabel}
540
+ testID={options.tabBarButtonTestID}
541
+ allowFontScaling={options.tabBarAllowFontScaling}
542
+ activeTintColor={tabBarActiveTintColor}
543
+ inactiveTintColor={tabBarInactiveTintColor}
544
+ activeBackgroundColor={tabBarActiveBackgroundColor}
545
+ inactiveBackgroundColor={tabBarInactiveBackgroundColor}
546
+ button={options.tabBarButton}
547
+ icon={
548
+ options.tabBarIcon ??
549
+ (({ color, size }) => <MissingIcon color={color} size={size} />)
550
+ }
551
+ badge={options.tabBarBadge}
552
+ badgeStyle={options.tabBarBadgeStyle}
553
+ label={label}
554
+ showLabel={tabBarShowLabel}
555
+ labelVisibilityMode={tabBarLabelVisibilityMode}
556
+ labelStyle={options.tabBarLabelStyle}
557
+ iconSize={options.tabBarIconSize}
558
+ iconStyle={options.tabBarIconStyle}
559
+ style={[
560
+ sidebar
561
+ ? {
562
+ marginVertical: hasHorizontalLabels
563
+ ? tabBarVariant === 'material'
564
+ ? 0
565
+ : 1
566
+ : spacing / 2,
567
+ }
568
+ : styles.bottomItem,
569
+ options.tabBarItemStyle,
570
+ ]}
571
+ />
572
+ </NavigationProvider>
573
+ );
574
+ });
575
+
505
576
  const styles = StyleSheet.create({
506
577
  start: {
507
578
  top: 0,
@@ -140,6 +140,10 @@ type Props = {
140
140
  * Style object for the label element.
141
141
  */
142
142
  labelStyle?: StyleProp<TextStyle>;
143
+ /**
144
+ * Size of the icon.
145
+ */
146
+ iconSize?: number;
143
147
  /**
144
148
  * Style object for the icon element.
145
149
  */
@@ -186,6 +190,7 @@ export function BottomTabItem({
186
190
  // https://developer.apple.com/documentation/uikit/uiview/3183939-largecontenttitle
187
191
  allowFontScaling = SUPPORTS_LARGE_CONTENT_VIEWER ? false : undefined,
188
192
  labelStyle,
193
+ iconSize,
189
194
  iconStyle,
190
195
  style,
191
196
  }: Props) {
@@ -300,6 +305,7 @@ export function BottomTabItem({
300
305
  route={route}
301
306
  variant={variant}
302
307
  size={compact ? 'compact' : 'regular'}
308
+ iconSize={iconSize}
303
309
  badge={badge}
304
310
  badgeStyle={badgeStyle}
305
311
  activeOpacity={activeOpacity}
@@ -5,6 +5,7 @@ import {
5
5
  Screen,
6
6
  } from '@react-navigation/elements';
7
7
  import {
8
+ type NavigationRoute,
8
9
  type ParamListBase,
9
10
  StackActions,
10
11
  type TabNavigationState,
@@ -19,6 +20,7 @@ import {
19
20
  } from '../TransitionConfigs/TransitionPresets';
20
21
  import type {
21
22
  BottomTabBarProps,
23
+ BottomTabDescriptor,
22
24
  BottomTabDescriptorMap,
23
25
  BottomTabHeaderProps,
24
26
  BottomTabNavigationConfig,
@@ -283,12 +285,7 @@ export function BottomTabView(props: Props) {
283
285
  >
284
286
  {routes.map((route, index) => {
285
287
  const descriptor = descriptors[route.key];
286
- const {
287
- lazy = true,
288
- animation = 'none',
289
- sceneStyleInterpolator = NAMED_TRANSITIONS_PRESETS[animation]
290
- .sceneStyleInterpolator,
291
- } = descriptor.options;
288
+ const { lazy = true } = descriptor.options;
292
289
  const isFocused = state.index === index;
293
290
  const isPreloaded = state.preloadedRouteKeys.includes(route.key);
294
291
 
@@ -302,73 +299,23 @@ export function BottomTabView(props: Props) {
302
299
  return null;
303
300
  }
304
301
 
305
- const {
306
- freezeOnBlur,
307
- header = ({ layout, options }: BottomTabHeaderProps) => (
308
- <Header
309
- {...options}
310
- layout={layout}
311
- title={getHeaderTitle(options, route.name)}
312
- />
313
- ),
314
- headerShown,
315
- headerStatusBarHeight,
316
- headerTransparent,
317
- sceneStyle: customSceneStyle,
318
- } = descriptor.options;
319
-
320
- const { sceneStyle } =
321
- sceneStyleInterpolator?.({
322
- current: {
323
- progress: tabAnims[route.key],
324
- },
325
- }) ?? {};
326
-
327
- const animationEnabled = hasAnimation(descriptor.options);
328
302
  const isAnimatingRoute =
329
303
  lastUpdate.animating &&
330
304
  (lastUpdate.previous === route.key ||
331
305
  lastUpdate.current === route.key);
332
306
 
333
- const activityState = isFocused
334
- ? STATE_ON_TOP // the screen is on top after the transition
335
- : animationEnabled && isAnimatingRoute
336
- ? STATE_TRANSITIONING_OR_BELOW_TOP // screen visible during transition
337
- : STATE_INACTIVE;
338
-
339
307
  return (
340
- <MaybeScreen
308
+ <TabScene
341
309
  key={route.key}
342
- style={[StyleSheet.absoluteFill, { zIndex: isFocused ? 0 : -1 }]}
343
- active={activityState}
344
- enabled={detachInactiveScreens}
345
- freezeOnBlur={freezeOnBlur}
346
- shouldFreeze={activityState === STATE_INACTIVE && !isPreloaded}
347
- pointerEvents={isFocused ? 'box-none' : 'none'}
348
- >
349
- <BottomTabBarHeightContext.Provider
350
- value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
351
- >
352
- <Screen
353
- focused={isFocused}
354
- route={descriptor.route}
355
- navigation={descriptor.navigation}
356
- headerShown={headerShown}
357
- headerStatusBarHeight={headerStatusBarHeight}
358
- headerTransparent={headerTransparent}
359
- header={header({
360
- layout: dimensions,
361
- route: descriptor.route,
362
- navigation:
363
- descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
364
- options: descriptor.options,
365
- })}
366
- style={[customSceneStyle, animationEnabled && sceneStyle]}
367
- >
368
- {descriptor.render()}
369
- </Screen>
370
- </BottomTabBarHeightContext.Provider>
371
- </MaybeScreen>
310
+ route={route}
311
+ descriptor={descriptor}
312
+ isFocused={isFocused}
313
+ isPreloaded={isPreloaded}
314
+ isAnimatingRoute={isAnimatingRoute}
315
+ tabAnimation={tabAnims[route.key]}
316
+ tabBarHeight={tabBarPosition === 'bottom' ? tabBarHeight : 0}
317
+ detachInactiveScreens={detachInactiveScreens}
318
+ />
372
319
  );
373
320
  })}
374
321
  </MaybeScreenContainer>
@@ -379,6 +326,93 @@ export function BottomTabView(props: Props) {
379
326
  );
380
327
  }
381
328
 
329
+ type TabSceneProps = {
330
+ route: NavigationRoute<ParamListBase, string>;
331
+ descriptor: BottomTabDescriptor;
332
+ isFocused: boolean;
333
+ isPreloaded: boolean;
334
+ isAnimatingRoute: boolean;
335
+ tabAnimation: Animated.Value;
336
+ tabBarHeight: number;
337
+ detachInactiveScreens: boolean;
338
+ };
339
+
340
+ const TabScene = React.memo(function TabScene({
341
+ route,
342
+ descriptor,
343
+ isFocused,
344
+ isPreloaded,
345
+ isAnimatingRoute,
346
+ tabAnimation,
347
+ tabBarHeight,
348
+ detachInactiveScreens,
349
+ }: TabSceneProps) {
350
+ const {
351
+ animation = 'none',
352
+ sceneStyleInterpolator = NAMED_TRANSITIONS_PRESETS[animation]
353
+ .sceneStyleInterpolator,
354
+ freezeOnBlur,
355
+ header = ({ layout, options }: BottomTabHeaderProps) => (
356
+ <Header
357
+ {...options}
358
+ layout={layout}
359
+ title={getHeaderTitle(options, route.name)}
360
+ />
361
+ ),
362
+ headerShown,
363
+ headerStatusBarHeight,
364
+ headerTransparent,
365
+ sceneStyle: customSceneStyle,
366
+ } = descriptor.options;
367
+
368
+ const { sceneStyle } =
369
+ sceneStyleInterpolator?.({
370
+ current: {
371
+ progress: tabAnimation,
372
+ },
373
+ }) ?? {};
374
+
375
+ const animationEnabled = hasAnimation(descriptor.options);
376
+
377
+ const activityState = isFocused
378
+ ? STATE_ON_TOP // the screen is on top after the transition
379
+ : animationEnabled && isAnimatingRoute
380
+ ? STATE_TRANSITIONING_OR_BELOW_TOP // screen visible during transition
381
+ : STATE_INACTIVE;
382
+
383
+ return (
384
+ <MaybeScreen
385
+ style={[StyleSheet.absoluteFill, { zIndex: isFocused ? 0 : -1 }]}
386
+ active={activityState}
387
+ enabled={detachInactiveScreens}
388
+ freezeOnBlur={freezeOnBlur}
389
+ shouldFreeze={activityState === STATE_INACTIVE && !isPreloaded}
390
+ pointerEvents={isFocused ? 'box-none' : 'none'}
391
+ >
392
+ <BottomTabBarHeightContext.Provider value={tabBarHeight}>
393
+ <Screen
394
+ focused={isFocused}
395
+ route={descriptor.route}
396
+ navigation={descriptor.navigation}
397
+ headerShown={headerShown}
398
+ headerStatusBarHeight={headerStatusBarHeight}
399
+ headerTransparent={headerTransparent}
400
+ header={header({
401
+ layout: SafeAreaProviderCompat.initialMetrics.frame,
402
+ route: descriptor.route,
403
+ navigation:
404
+ descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
405
+ options: descriptor.options,
406
+ })}
407
+ style={[customSceneStyle, animationEnabled && sceneStyle]}
408
+ >
409
+ {descriptor.render()}
410
+ </Screen>
411
+ </BottomTabBarHeightContext.Provider>
412
+ </MaybeScreen>
413
+ );
414
+ });
415
+
382
416
  const styles = StyleSheet.create({
383
417
  screens: {
384
418
  flex: 1,