@soundtrackio/react-native-bottom-tabs 1.4.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 (125) hide show
  1. package/README.md +32 -0
  2. package/android/build.gradle +132 -0
  3. package/android/gradle.properties +5 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/AndroidManifestNew.xml +2 -0
  6. package/android/src/main/java/com/rcttabview/ImageSource.kt +86 -0
  7. package/android/src/main/java/com/rcttabview/LatestRequestTracker.kt +19 -0
  8. package/android/src/main/java/com/rcttabview/RCTTabView.kt +689 -0
  9. package/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +222 -0
  10. package/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt +19 -0
  11. package/android/src/main/java/com/rcttabview/RCTTabViewPrewarm.kt +40 -0
  12. package/android/src/main/java/com/rcttabview/RebuildBatch.kt +31 -0
  13. package/android/src/main/java/com/rcttabview/TabBarVisibility.kt +14 -0
  14. package/android/src/main/java/com/rcttabview/Utils.kt +31 -0
  15. package/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt +25 -0
  16. package/android/src/main/java/com/rcttabview/events/OnTabBarMeasuredEvent.kt +24 -0
  17. package/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt +32 -0
  18. package/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt +24 -0
  19. package/android/src/test/java/com/rcttabview/LatestRequestTrackerTest.kt +37 -0
  20. package/android/src/test/java/com/rcttabview/RebuildBatchTest.kt +44 -0
  21. package/android/src/test/java/com/rcttabview/TabBarVisibilityTest.kt +58 -0
  22. package/app.plugin.js +1 -0
  23. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h +33 -0
  24. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp +19 -0
  25. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h +35 -0
  26. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp +15 -0
  27. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h +25 -0
  28. package/ios/BottomAccessoryProvider.swift +27 -0
  29. package/ios/Extensions.swift +101 -0
  30. package/ios/PlatformAliases.swift +43 -0
  31. package/ios/RCTBottomAccessoryComponentView.h +17 -0
  32. package/ios/RCTBottomAccessoryComponentView.mm +76 -0
  33. package/ios/RCTTabViewComponentView.h +16 -0
  34. package/ios/RCTTabViewComponentView.mm +285 -0
  35. package/ios/RepresentableView.swift +55 -0
  36. package/ios/SVG/CoreSVG.h +19 -0
  37. package/ios/SVG/CoreSVG.mm +177 -0
  38. package/ios/SVG/SvgDecoder.h +10 -0
  39. package/ios/SVG/SvgDecoder.mm +38 -0
  40. package/ios/TabAppearModifier.swift +38 -0
  41. package/ios/TabBarFontSize.swift +88 -0
  42. package/ios/TabItem.swift +35 -0
  43. package/ios/TabItemEventModifier.swift +163 -0
  44. package/ios/TabView/AnyTabView.swift +7 -0
  45. package/ios/TabView/LegacyTabView.swift +70 -0
  46. package/ios/TabView/NewTabView.swift +138 -0
  47. package/ios/TabViewImpl.swift +566 -0
  48. package/ios/TabViewProps.swift +109 -0
  49. package/ios/TabViewProvider.swift +368 -0
  50. package/ios/react-native-bottom-tabs-Bridging-Header.h +4 -0
  51. package/lib/module/BottomAccessoryView.js +47 -0
  52. package/lib/module/BottomAccessoryView.js.map +1 -0
  53. package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
  54. package/lib/module/DelayedFreeze.js +31 -0
  55. package/lib/module/DelayedFreeze.js.map +1 -0
  56. package/lib/module/NativeSVGDecoder.js +5 -0
  57. package/lib/module/NativeSVGDecoder.js.map +1 -0
  58. package/lib/module/SceneMap.js +28 -0
  59. package/lib/module/SceneMap.js.map +1 -0
  60. package/lib/module/TabView.js +294 -0
  61. package/lib/module/TabView.js.map +1 -0
  62. package/lib/module/TabViewNativeComponent.ts +73 -0
  63. package/lib/module/expo.js +35 -0
  64. package/lib/module/expo.js.map +1 -0
  65. package/lib/module/index.js +20 -0
  66. package/lib/module/index.js.map +1 -0
  67. package/lib/module/package.json +1 -0
  68. package/lib/module/types.js +4 -0
  69. package/lib/module/types.js.map +1 -0
  70. package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
  71. package/lib/module/utils/BottomTabBarHeightContext.js.map +1 -0
  72. package/lib/module/utils/useBottomTabBarHeight.js +12 -0
  73. package/lib/module/utils/useBottomTabBarHeight.js.map +1 -0
  74. package/lib/typescript/package.json +1 -0
  75. package/lib/typescript/src/BottomAccessoryView.d.ts +7 -0
  76. package/lib/typescript/src/BottomAccessoryView.d.ts.map +1 -0
  77. package/lib/typescript/src/BottomAccessoryView.js +24 -0
  78. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +15 -0
  79. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts.map +1 -0
  80. package/lib/typescript/src/BottomAccessoryViewNativeComponent.js +4 -0
  81. package/lib/typescript/src/DelayedFreeze.d.ts +7 -0
  82. package/lib/typescript/src/DelayedFreeze.d.ts.map +1 -0
  83. package/lib/typescript/src/DelayedFreeze.js +21 -0
  84. package/lib/typescript/src/NativeSVGDecoder.d.ts +5 -0
  85. package/lib/typescript/src/NativeSVGDecoder.d.ts.map +1 -0
  86. package/lib/typescript/src/NativeSVGDecoder.js +2 -0
  87. package/lib/typescript/src/SceneMap.d.ts +9 -0
  88. package/lib/typescript/src/SceneMap.d.ts.map +1 -0
  89. package/lib/typescript/src/SceneMap.js +15 -0
  90. package/lib/typescript/src/TabView.d.ts +219 -0
  91. package/lib/typescript/src/TabView.d.ts.map +1 -0
  92. package/lib/typescript/src/TabView.js +157 -0
  93. package/lib/typescript/src/TabViewNativeComponent.d.ts +59 -0
  94. package/lib/typescript/src/TabViewNativeComponent.d.ts.map +1 -0
  95. package/lib/typescript/src/TabViewNativeComponent.js +4 -0
  96. package/lib/typescript/src/expo.d.ts +6 -0
  97. package/lib/typescript/src/expo.d.ts.map +1 -0
  98. package/lib/typescript/src/expo.js +34 -0
  99. package/lib/typescript/src/index.d.ts +15 -0
  100. package/lib/typescript/src/index.d.ts.map +1 -0
  101. package/lib/typescript/src/index.js +11 -0
  102. package/lib/typescript/src/types.d.ts +31 -0
  103. package/lib/typescript/src/types.d.ts.map +1 -0
  104. package/lib/typescript/src/types.js +1 -0
  105. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +2 -0
  106. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts.map +1 -0
  107. package/lib/typescript/src/utils/BottomTabBarHeightContext.js +2 -0
  108. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +1 -0
  109. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts.map +1 -0
  110. package/lib/typescript/src/utils/useBottomTabBarHeight.js +9 -0
  111. package/lib/typescript/tsconfig.tsbuildinfo +1 -0
  112. package/package.json +135 -0
  113. package/react-native-bottom-tabs.podspec +43 -0
  114. package/src/BottomAccessoryView.tsx +59 -0
  115. package/src/BottomAccessoryViewNativeComponent.ts +27 -0
  116. package/src/DelayedFreeze.tsx +31 -0
  117. package/src/NativeSVGDecoder.ts +5 -0
  118. package/src/SceneMap.tsx +34 -0
  119. package/src/TabView.tsx +558 -0
  120. package/src/TabViewNativeComponent.ts +73 -0
  121. package/src/expo.ts +58 -0
  122. package/src/index.tsx +23 -0
  123. package/src/types.ts +36 -0
  124. package/src/utils/BottomTabBarHeightContext.ts +5 -0
  125. package/src/utils/useBottomTabBarHeight.ts +15 -0
@@ -0,0 +1,27 @@
1
+ import { codegenNativeComponent } from 'react-native';
2
+ import type { ViewProps } from 'react-native';
3
+ import type {
4
+ DirectEventHandler,
5
+ Double,
6
+ } from 'react-native/Libraries/Types/CodegenTypes';
7
+
8
+ export type OnNativeLayout = Readonly<{
9
+ width: Double;
10
+ height: Double;
11
+ }>;
12
+
13
+ export type OnPlacementChanged = Readonly<{
14
+ placement: string;
15
+ }>;
16
+
17
+ export interface BottomAccessoryViewNativeProps extends ViewProps {
18
+ onNativeLayout?: DirectEventHandler<OnNativeLayout>;
19
+ onPlacementChanged?: DirectEventHandler<OnPlacementChanged>;
20
+ }
21
+
22
+ export default codegenNativeComponent<BottomAccessoryViewNativeProps>(
23
+ 'BottomAccessoryView',
24
+ {
25
+ excludedPlatforms: ['android'],
26
+ }
27
+ );
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { Freeze } from 'react-freeze';
3
+
4
+ interface FreezeWrapperProps {
5
+ freeze: boolean;
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ // Animation delay for freeze effect.
10
+ const ANIMATION_DELAY = 200;
11
+
12
+ // This component delays the freeze effect by animation delay.
13
+ // So that the screen is not frozen immediately causing background flash.
14
+ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
15
+ // flag used for determining whether freeze should be enabled
16
+ const [freezeState, setFreezeState] = React.useState(false);
17
+
18
+ React.useEffect(() => {
19
+ const id = setTimeout(() => {
20
+ setFreezeState(freeze);
21
+ }, ANIMATION_DELAY);
22
+
23
+ return () => {
24
+ clearTimeout(id);
25
+ };
26
+ }, [freeze]);
27
+
28
+ return <Freeze freeze={freeze ? freezeState : false}>{children}</Freeze>;
29
+ }
30
+
31
+ export default DelayedFreeze;
@@ -0,0 +1,5 @@
1
+ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
+
3
+ export interface Spec extends TurboModule {}
4
+
5
+ export default TurboModuleRegistry.getEnforcing<Spec>('SvgDecoder');
@@ -0,0 +1,34 @@
1
+ import * as React from 'react';
2
+
3
+ type SceneProps = {
4
+ route: any;
5
+ jumpTo: (key: string) => void;
6
+ };
7
+
8
+ const SceneComponent = React.memo(
9
+ <T extends { component: React.ComponentType<any> } & SceneProps>({
10
+ component,
11
+ ...rest
12
+ }: T) => {
13
+ return React.createElement(component, rest);
14
+ }
15
+ );
16
+
17
+ SceneComponent.displayName = 'SceneComponent';
18
+
19
+ export function SceneMap<T>(scenes: { [key: string]: React.ComponentType<T> }) {
20
+ return ({ route, jumpTo }: SceneProps) => {
21
+ const component = scenes[route.key];
22
+ if (!component) {
23
+ return null;
24
+ }
25
+ return (
26
+ <SceneComponent
27
+ key={route.key}
28
+ jumpTo={jumpTo}
29
+ component={component}
30
+ route={route}
31
+ />
32
+ );
33
+ };
34
+ }
@@ -0,0 +1,558 @@
1
+ import React, { useLayoutEffect, useRef } from 'react';
2
+ import type {
3
+ OnNativeLayout,
4
+ OnPageSelectedEventData,
5
+ OnTabBarMeasured,
6
+ TabViewItems,
7
+ } from './TabViewNativeComponent';
8
+ import {
9
+ type ColorValue,
10
+ type DimensionValue,
11
+ Image,
12
+ Platform,
13
+ type StyleProp,
14
+ StyleSheet,
15
+ View,
16
+ type ViewStyle,
17
+ processColor,
18
+ } from 'react-native';
19
+ import { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
20
+
21
+ // eslint-disable-next-line @react-native/no-deep-imports
22
+ import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
23
+ import NativeTabView from './TabViewNativeComponent';
24
+ import useLatestCallback from 'use-latest-callback';
25
+ import type {
26
+ AppleIcon,
27
+ BaseRoute,
28
+ IconRenderingMode,
29
+ LayoutDirection,
30
+ NavigationState,
31
+ TabRole,
32
+ } from './types';
33
+ import DelayedFreeze from './DelayedFreeze';
34
+ import {
35
+ BottomAccessoryView,
36
+ type BottomAccessoryViewProps,
37
+ } from './BottomAccessoryView';
38
+
39
+ const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
40
+ icon?.sfSymbol;
41
+
42
+ interface Props<Route extends BaseRoute> {
43
+ /*
44
+ * Whether to show labels in tabs. When false, only icons will be displayed.
45
+ */
46
+ labeled?: boolean;
47
+ /**
48
+ * A tab bar style that adapts to each platform.
49
+ *
50
+ * that varies depending on the platform:
51
+ * Tab views using the sidebar adaptable style have an appearance
52
+ * - iPadOS displays a top tab bar that can adapt into a sidebar.
53
+ * - iOS displays a bottom tab bar.
54
+ * - macOS and tvOS always show a sidebar.
55
+ * - visionOS shows an ornament and also shows a sidebar for secondary tabs within a `TabSection`.
56
+ */
57
+ sidebarAdaptable?: boolean;
58
+ /**
59
+ * Whether to disable page animations between tabs. (iOS only) Defaults to `false`.
60
+ */
61
+ disablePageAnimations?: boolean;
62
+ /**
63
+ * Whether to enable haptic feedback. Defaults to `false`.
64
+ */
65
+ hapticFeedbackEnabled?: boolean;
66
+ /**
67
+ * Describes the appearance attributes for the tabBar to use when an observable scroll view is scrolled to the bottom. (iOS only)
68
+ */
69
+ scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent';
70
+
71
+ /**
72
+ * Behavior for minimizing the tab bar. (iOS 26+)
73
+ */
74
+ minimizeBehavior?: 'automatic' | 'onScrollDown' | 'onScrollUp' | 'never';
75
+ /**
76
+ * Active tab color.
77
+ */
78
+ tabBarActiveTintColor?: ColorValue;
79
+ /**
80
+ * Inactive tab color.
81
+ * Has no effect on iOS 26 and above (Liquid Glass).
82
+ */
83
+ tabBarInactiveTintColor?: ColorValue;
84
+ /**
85
+ * Enables the experimental iOS 26 Liquid Glass tint color workaround that bakes tab labels into images.
86
+ * This has many drawbacks, such as affecting icon sizing when labels have different widths, bad positioning of badges, and possibly breaking accessibility features.
87
+ *
88
+ * @platform ios
89
+ */
90
+ experimental_bakedTintColors?: boolean;
91
+ /**
92
+ * State for the tab view.
93
+ *
94
+ * The state should contain a `routes` prop which is an array of objects containing `key` and `title` props, such as `{ key: 'music', title: 'Music' }`.
95
+ *
96
+ */
97
+ navigationState: NavigationState<Route>;
98
+ /**
99
+ * Function which takes an object with the route and returns a React element.
100
+ */
101
+ renderScene: (props: {
102
+ route: Route;
103
+ jumpTo: (key: string) => void;
104
+ }) => React.ReactNode | null;
105
+ /**
106
+ * Callback which is called on tab change, receives the index of the new tab as argument.
107
+ */
108
+ onIndexChange: (index: number) => void;
109
+ /**
110
+ * Callback which is called on long press on tab, receives the index of the tab as argument.
111
+ */
112
+ onTabLongPress?: (index: number) => void;
113
+ /**
114
+ * Get lazy for the current screen. Uses true by default.
115
+ */
116
+ getLazy?: (props: { route: Route }) => boolean | undefined;
117
+ /**
118
+ * Get label text for the tab, uses `route.title` by default.
119
+ */
120
+ getLabelText?: (props: { route: Route }) => string | undefined;
121
+ /**
122
+ * Get badge for the tab, uses `route.badge` by default.
123
+ */
124
+ getBadge?: (props: { route: Route }) => string | undefined;
125
+ /**
126
+ * Get badge background color for the tab, uses `route.badgeBackgroundColor` by default. (Android only)
127
+ */
128
+ getBadgeBackgroundColor?: (props: { route: Route }) => ColorValue | undefined;
129
+ /**
130
+ * Get badge text color for the tab, uses `route.badgeTextColor` by default. (Android only)
131
+ */
132
+ getBadgeTextColor?: (props: { route: Route }) => ColorValue | undefined;
133
+ /**
134
+ * Get active tint color for the tab, uses `route.activeTintColor` by default.
135
+ */
136
+ getActiveTintColor?: (props: { route: Route }) => ColorValue | undefined;
137
+ /**
138
+ * Determines whether the tab prevents default action (switching tabs) on press, uses `route.preventsDefault` by default.
139
+ */
140
+ getPreventsDefault?: (props: { route: Route }) => boolean | undefined;
141
+ /**
142
+ * Get icon for the tab, uses `route.focusedIcon` by default.
143
+ */
144
+ getIcon?: (props: {
145
+ route: Route;
146
+ focused: boolean;
147
+ }) => ImageSource | AppleIcon | undefined | null;
148
+
149
+ /**
150
+ * Get the rendering mode for the tab icon, uses `route.iconRenderingMode` by default.
151
+ *
152
+ * Use `original` to preserve multicolor image icons instead of applying the native tab tint.
153
+ */
154
+ getIconRenderingMode?: (props: {
155
+ route: Route;
156
+ }) => IconRenderingMode | undefined;
157
+
158
+ /**
159
+ * Get hidden for the tab, uses `route.hidden` by default.
160
+ * If `true`, the tab will be hidden.
161
+ */
162
+ getHidden?: (props: { route: Route }) => boolean | undefined;
163
+
164
+ /**
165
+ * Get testID for the tab, uses `route.testID` by default.
166
+ */
167
+ getTestID?: (props: { route: Route }) => string | undefined;
168
+
169
+ /**
170
+ * Get role for the tab, uses `route.role` by default. (iOS only)
171
+ */
172
+ getRole?: (props: { route: Route }) => TabRole | undefined;
173
+
174
+ /**
175
+ * Custom tab bar to render. Set to `null` to hide the tab bar completely.
176
+ */
177
+ tabBar?: () => React.ReactNode;
178
+
179
+ /**
180
+ * Get freezeOnBlur for the current screen. Uses false by default.
181
+ */
182
+ getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined;
183
+
184
+ /**
185
+ * Get style for the scene, uses `route.style` by default.
186
+ */
187
+ getSceneStyle?: (props: { route: Route }) => StyleProp<ViewStyle>;
188
+
189
+ tabBarStyle?: {
190
+ /**
191
+ * Background color of the tab bar.
192
+ */
193
+ backgroundColor?: ColorValue;
194
+ };
195
+
196
+ /**
197
+ * A Boolean value that indicates whether the tab bar is translucent. (iOS only)
198
+ */
199
+ translucent?: boolean;
200
+ rippleColor?: ColorValue;
201
+ /**
202
+ * Color of tab indicator. (Android only)
203
+ */
204
+ activeIndicatorColor?: ColorValue;
205
+ tabLabelStyle?: {
206
+ /**
207
+ * Font family for the tab labels.
208
+ */
209
+ fontFamily?: string;
210
+
211
+ /**
212
+ * Font weight for the tab labels.
213
+ */
214
+ fontWeight?: string;
215
+
216
+ /**
217
+ * Font size for the tab labels.
218
+ */
219
+ fontSize?: number;
220
+ };
221
+ /**
222
+ * A function that returns a React element to display as bottom accessory view.
223
+ * iOS 26+ only.
224
+ *
225
+ * @platform ios
226
+ */
227
+ renderBottomAccessoryView?: BottomAccessoryViewProps['renderBottomAccessoryView'];
228
+ /**
229
+ * The direction of the layout.
230
+ * @default 'locale'
231
+ */
232
+ layoutDirection?: LayoutDirection;
233
+ /**
234
+ * Whether to hide the native tab bar.
235
+ */
236
+ tabBarHidden?: boolean;
237
+ /**
238
+ * Whether to hide the tab bar while the keyboard is shown. Android only:
239
+ * with `adjustResize` the bar is otherwise lifted above the keyboard.
240
+ *
241
+ * @platform android
242
+ */
243
+ tabBarHideOnKeyboard?: boolean;
244
+ }
245
+
246
+ const ANDROID_MAX_TABS = 100;
247
+
248
+ const TabView = <Route extends BaseRoute>({
249
+ navigationState,
250
+ renderScene,
251
+ onIndexChange,
252
+ onTabLongPress,
253
+ rippleColor,
254
+ tabBarActiveTintColor: activeTintColor,
255
+ tabBarInactiveTintColor: inactiveTintColor,
256
+ getBadge = ({ route }: { route: Route }) => route.badge,
257
+ getBadgeBackgroundColor = ({ route }: { route: Route }) =>
258
+ route.badgeBackgroundColor,
259
+ getBadgeTextColor = ({ route }: { route: Route }) => route.badgeTextColor,
260
+ getLazy = ({ route }: { route: Route }) => route.lazy,
261
+ getLabelText = ({ route }: { route: Route }) => route.title,
262
+ getIcon = ({ route, focused }: { route: Route; focused: boolean }) =>
263
+ route.unfocusedIcon
264
+ ? focused
265
+ ? route.focusedIcon
266
+ : route.unfocusedIcon
267
+ : route.focusedIcon,
268
+ getHidden = ({ route }: { route: Route }) => route.hidden,
269
+ getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor,
270
+ getTestID = ({ route }: { route: Route }) => route.testID,
271
+ getRole = ({ route }: { route: Route }) => route.role,
272
+ getIconRenderingMode = ({ route }: { route: Route }) =>
273
+ route.iconRenderingMode,
274
+ getSceneStyle = ({ route }: { route: Route }) => route.style,
275
+ getPreventsDefault = ({ route }: { route: Route }) => route.preventsDefault,
276
+ hapticFeedbackEnabled = false,
277
+ // Android's native behavior is to show labels when there are less than 4 tabs. We leave it as undefined to use the platform default behavior.
278
+ labeled = Platform.OS !== 'android' ? true : undefined,
279
+ getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
280
+ tabBar: renderCustomTabBar,
281
+ tabBarHidden,
282
+ tabBarHideOnKeyboard,
283
+ tabBarStyle,
284
+ tabLabelStyle,
285
+ renderBottomAccessoryView,
286
+ layoutDirection = 'locale',
287
+ experimental_bakedTintColors: experimentalBakedTintColors = false,
288
+ ...props
289
+ }: Props<Route>) => {
290
+ // @ts-ignore
291
+ const focusedKey = navigationState.routes[navigationState.index].key;
292
+ const customTabBarWrapperRef = useRef<View>(null);
293
+ const [tabBarHeight, setTabBarHeight] = React.useState<number | undefined>(0);
294
+ const [measuredDimensions, setMeasuredDimensions] = React.useState<
295
+ { width: DimensionValue; height: DimensionValue } | undefined
296
+ >({ width: '100%', height: '100%' });
297
+
298
+ const trimmedRoutes = React.useMemo(() => {
299
+ if (
300
+ Platform.OS === 'android' &&
301
+ navigationState.routes.length > ANDROID_MAX_TABS
302
+ ) {
303
+ console.warn(
304
+ `TabView only supports up to ${ANDROID_MAX_TABS} tabs on Android`
305
+ );
306
+ return navigationState.routes.slice(0, ANDROID_MAX_TABS);
307
+ }
308
+ return navigationState.routes;
309
+ }, [navigationState.routes]);
310
+
311
+ /**
312
+ * List of loaded tabs, tabs will be loaded when navigated to.
313
+ */
314
+ const [loaded, setLoaded] = React.useState<string[]>([focusedKey]);
315
+
316
+ if (!loaded.includes(focusedKey)) {
317
+ // Set the current tab to be loaded if it was not loaded before
318
+ setLoaded((loaded) => [...loaded, focusedKey]);
319
+ }
320
+
321
+ const icons = React.useMemo(
322
+ () =>
323
+ trimmedRoutes.map((route) =>
324
+ getIcon({
325
+ route,
326
+ // iOS uses UITabBarItem.selectedImage for selected and Liquid Glass hover states.
327
+ // Keep the base image unfocused so a selected tab can render unfocused while another tab is hovered.
328
+ focused: Platform.OS === 'ios' ? false : route.key === focusedKey,
329
+ })
330
+ ),
331
+ [focusedKey, getIcon, trimmedRoutes]
332
+ );
333
+
334
+ const focusedIcons = React.useMemo(
335
+ () =>
336
+ trimmedRoutes.map((route) =>
337
+ getIcon({
338
+ route,
339
+ focused: true,
340
+ })
341
+ ),
342
+ [getIcon, trimmedRoutes]
343
+ );
344
+
345
+ const items: TabViewItems = React.useMemo(
346
+ () =>
347
+ trimmedRoutes.map((route, index) => {
348
+ const icon = icons[index];
349
+ const isSfSymbol = isAppleSymbol(icon);
350
+ const focusedIcon = focusedIcons[index];
351
+ const isFocusedSfSymbol = isAppleSymbol(focusedIcon);
352
+
353
+ if (Platform.OS === 'android' && isSfSymbol) {
354
+ console.warn(
355
+ 'SF Symbols are not supported on Android. Use require() or pass uri to load an image instead.'
356
+ );
357
+ }
358
+
359
+ return {
360
+ key: route.key,
361
+ title: getLabelText({ route }) ?? route.key,
362
+ sfSymbol: isSfSymbol ? icon.sfSymbol : undefined,
363
+ focusedSfSymbol: isFocusedSfSymbol ? focusedIcon.sfSymbol : undefined,
364
+ badge: getBadge?.({ route }),
365
+ badgeBackgroundColor: processColor(
366
+ getBadgeBackgroundColor?.({ route })
367
+ ),
368
+ badgeTextColor: processColor(getBadgeTextColor?.({ route })),
369
+ activeTintColor: processColor(getActiveTintColor({ route })),
370
+ iconRenderingMode: getIconRenderingMode({ route }),
371
+ hidden: getHidden?.({ route }),
372
+ testID: getTestID?.({ route }),
373
+ role: getRole?.({ route }),
374
+ preventsDefault: getPreventsDefault?.({ route }),
375
+ };
376
+ }),
377
+ [
378
+ trimmedRoutes,
379
+ icons,
380
+ focusedIcons,
381
+ getLabelText,
382
+ getBadge,
383
+ getBadgeBackgroundColor,
384
+ getBadgeTextColor,
385
+ getActiveTintColor,
386
+ getIconRenderingMode,
387
+ getHidden,
388
+ getTestID,
389
+ getRole,
390
+ getPreventsDefault,
391
+ ]
392
+ );
393
+
394
+ const resolvedIconAssets: ImageSource[] = React.useMemo(
395
+ () =>
396
+ // Pass empty object for icons that are not provided to avoid index mismatch on native side.
397
+ icons.map((icon) =>
398
+ icon && !isAppleSymbol(icon)
399
+ ? // @ts-expect-error: TODO: Migrate of deep imports
400
+ Image.resolveAssetSource(icon)
401
+ : { uri: '' }
402
+ ),
403
+ [icons]
404
+ );
405
+
406
+ const resolvedFocusedIconAssets: ImageSource[] = React.useMemo(
407
+ () =>
408
+ // Pass empty object for icons that are not provided to avoid index mismatch on native side.
409
+ focusedIcons.map((icon) =>
410
+ icon && !isAppleSymbol(icon)
411
+ ? // @ts-expect-error: TODO: Migrate of deep imports
412
+ Image.resolveAssetSource(icon)
413
+ : { uri: '' }
414
+ ),
415
+ [focusedIcons]
416
+ );
417
+
418
+ const jumpTo = useLatestCallback((key: string) => {
419
+ const index = trimmedRoutes.findIndex((route) => route.key === key);
420
+
421
+ onIndexChange(index);
422
+ });
423
+
424
+ const handleTabLongPress = React.useCallback(
425
+ ({ nativeEvent: { key } }: { nativeEvent: OnPageSelectedEventData }) => {
426
+ const index = trimmedRoutes.findIndex((route) => route.key === key);
427
+ onTabLongPress?.(index);
428
+ },
429
+ [trimmedRoutes, onTabLongPress]
430
+ );
431
+
432
+ const handlePageSelected = React.useCallback(
433
+ ({ nativeEvent: { key } }: { nativeEvent: OnPageSelectedEventData }) => {
434
+ jumpTo(key);
435
+ },
436
+ [jumpTo]
437
+ );
438
+
439
+ const handleTabBarMeasured = React.useCallback(
440
+ ({ nativeEvent: { height } }: { nativeEvent: OnTabBarMeasured }) => {
441
+ setTabBarHeight(height);
442
+ },
443
+ [setTabBarHeight]
444
+ );
445
+
446
+ const handleNativeLayout = React.useCallback(
447
+ ({ nativeEvent: { width, height } }: { nativeEvent: OnNativeLayout }) => {
448
+ setMeasuredDimensions({ width, height });
449
+ },
450
+ [setMeasuredDimensions]
451
+ );
452
+
453
+ useLayoutEffect(() => {
454
+ // If we are rendering a custom tab bar, we need to measure it to set the tab bar height.
455
+ if (renderCustomTabBar && customTabBarWrapperRef.current) {
456
+ customTabBarWrapperRef.current.measure((_x, _y, _width, height) => {
457
+ setTabBarHeight(height);
458
+ });
459
+ }
460
+ }, [renderCustomTabBar]);
461
+
462
+ return (
463
+ <BottomTabBarHeightContext.Provider value={tabBarHeight}>
464
+ <NativeTabView
465
+ {...props}
466
+ {...tabLabelStyle}
467
+ style={styles.fullWidth}
468
+ items={items}
469
+ // When rendering a custom tab bar, icons can be React elements, which will not be properly resolved.
470
+ icons={renderCustomTabBar ? undefined : resolvedIconAssets}
471
+ focusedIcons={
472
+ renderCustomTabBar ? undefined : resolvedFocusedIconAssets
473
+ }
474
+ selectedPage={focusedKey}
475
+ tabBarHidden={tabBarHidden ?? !!renderCustomTabBar}
476
+ tabBarHideOnKeyboard={tabBarHideOnKeyboard}
477
+ onTabLongPress={handleTabLongPress}
478
+ onPageSelected={handlePageSelected}
479
+ onTabBarMeasured={handleTabBarMeasured}
480
+ onNativeLayout={handleNativeLayout}
481
+ hapticFeedbackEnabled={hapticFeedbackEnabled}
482
+ layoutDirection={layoutDirection}
483
+ activeTintColor={activeTintColor}
484
+ inactiveTintColor={inactiveTintColor}
485
+ experimentalBakedTintColors={experimentalBakedTintColors}
486
+ barTintColor={tabBarStyle?.backgroundColor}
487
+ rippleColor={rippleColor}
488
+ labeled={labeled}
489
+ >
490
+ {trimmedRoutes.map((route) => {
491
+ if (getLazy({ route }) !== false && !loaded.includes(route.key)) {
492
+ // Don't render a screen if we've never navigated to it
493
+ return (
494
+ <View
495
+ key={route.key}
496
+ collapsable={false}
497
+ style={styles.fullWidth}
498
+ />
499
+ );
500
+ }
501
+
502
+ const focused = route.key === focusedKey;
503
+ const freeze = !focused ? getFreezeOnBlur({ route }) : false;
504
+
505
+ const customStyle = getSceneStyle({ route });
506
+
507
+ return (
508
+ <View
509
+ key={route.key}
510
+ style={[
511
+ styles.screen,
512
+ renderCustomTabBar ? styles.fullWidth : measuredDimensions,
513
+ customStyle,
514
+ ]}
515
+ collapsable={false}
516
+ pointerEvents={focused ? 'auto' : 'none'}
517
+ accessibilityElementsHidden={!focused}
518
+ importantForAccessibility={
519
+ focused ? 'auto' : 'no-hide-descendants'
520
+ }
521
+ >
522
+ <DelayedFreeze freeze={!!freeze}>
523
+ {renderScene({
524
+ route,
525
+ jumpTo,
526
+ })}
527
+ </DelayedFreeze>
528
+ </View>
529
+ );
530
+ })}
531
+ {Platform.OS === 'ios' &&
532
+ parseFloat(Platform.Version) >= 26 &&
533
+ renderBottomAccessoryView &&
534
+ !renderCustomTabBar ? (
535
+ <BottomAccessoryView
536
+ renderBottomAccessoryView={renderBottomAccessoryView}
537
+ />
538
+ ) : null}
539
+ </NativeTabView>
540
+ {renderCustomTabBar ? (
541
+ <View ref={customTabBarWrapperRef}>{renderCustomTabBar()}</View>
542
+ ) : null}
543
+ </BottomTabBarHeightContext.Provider>
544
+ );
545
+ };
546
+
547
+ const styles = StyleSheet.create({
548
+ fullWidth: {
549
+ width: '100%',
550
+ height: '100%',
551
+ flex: 1,
552
+ },
553
+ screen: {
554
+ position: 'absolute',
555
+ },
556
+ });
557
+
558
+ export default TabView;