@onekeyfe/react-native-bottom-tabs 0.0.0-stage → 6.7.0-alpha.339

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 (77) hide show
  1. package/LICENSE +46 -0
  2. package/README.md +86 -2
  3. package/android/build.gradle +119 -0
  4. package/android/gradle.properties +4 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/AndroidManifestNew.xml +2 -0
  7. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +16 -0
  8. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +1419 -0
  9. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +216 -0
  10. package/android/src/main/java/com/onekey/bottomtabs/FloatingTabBarStyle.kt +50 -0
  11. package/android/src/main/java/com/onekey/bottomtabs/ImageSource.kt +86 -0
  12. package/android/src/main/java/com/onekey/bottomtabs/TabInfo.kt +13 -0
  13. package/android/src/main/java/com/onekey/bottomtabs/Utils.kt +31 -0
  14. package/android/src/main/java/com/onekey/bottomtabs/events/OnNativeLayoutEvent.kt +20 -0
  15. package/android/src/main/java/com/onekey/bottomtabs/events/OnTabBarMeasuredEvent.kt +19 -0
  16. package/android/src/main/java/com/onekey/bottomtabs/events/PageSelectedEvent.kt +21 -0
  17. package/android/src/main/java/com/onekey/bottomtabs/events/TabDoublePressEvent.kt +19 -0
  18. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewComponentDescriptor.h +32 -0
  19. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.cpp +18 -0
  20. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.h +35 -0
  21. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.cpp +15 -0
  22. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.h +25 -0
  23. package/docs/SPEC.md +101 -0
  24. package/ios/Extensions.swift +46 -0
  25. package/ios/RCTBottomAccessoryComponentView.h +12 -0
  26. package/ios/RCTBottomAccessoryComponentView.mm +67 -0
  27. package/ios/RCTBottomAccessoryContainerView.swift +51 -0
  28. package/ios/RCTBottomTabsComponentView.h +12 -0
  29. package/ios/RCTBottomTabsComponentView.mm +338 -0
  30. package/ios/RCTBottomTabsContainerView.swift +777 -0
  31. package/ios/RCTBottomTabsLog.h +7 -0
  32. package/ios/RCTBottomTabsLog.m +32 -0
  33. package/ios/SVG/CoreSVG.h +13 -0
  34. package/ios/SVG/CoreSVG.mm +177 -0
  35. package/ios/SVG/SvgDecoder.h +10 -0
  36. package/ios/SVG/SvgDecoder.mm +32 -0
  37. package/ios/TabBarFontSize.swift +55 -0
  38. package/lib/module/BottomAccessoryView.js +45 -0
  39. package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
  40. package/lib/module/BottomTabs.js +323 -0
  41. package/lib/module/BottomTabsNativeComponent.ts +69 -0
  42. package/lib/module/DelayedFreeze.js +31 -0
  43. package/lib/module/NativeSVGDecoder.js +5 -0
  44. package/lib/module/SceneMap.js +28 -0
  45. package/lib/module/codegen-types.d.js +2 -0
  46. package/lib/module/index.js +20 -0
  47. package/lib/module/package.json +1 -0
  48. package/lib/module/types.js +4 -0
  49. package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
  50. package/lib/module/utils/useBottomTabBarHeight.js +12 -0
  51. package/lib/typescript/package.json +1 -0
  52. package/lib/typescript/src/BottomAccessoryView.d.ts +8 -0
  53. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +16 -0
  54. package/lib/typescript/src/BottomTabs.d.ts +202 -0
  55. package/lib/typescript/src/BottomTabsNativeComponent.d.ts +58 -0
  56. package/lib/typescript/src/DelayedFreeze.d.ts +9 -0
  57. package/lib/typescript/src/NativeSVGDecoder.d.ts +6 -0
  58. package/lib/typescript/src/SceneMap.d.ts +10 -0
  59. package/lib/typescript/src/index.d.ts +16 -0
  60. package/lib/typescript/src/types.d.ts +29 -0
  61. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +3 -0
  62. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +2 -0
  63. package/package.json +117 -4
  64. package/react-native-bottom-tabs.podspec +36 -0
  65. package/react-native.config.js +13 -0
  66. package/src/BottomAccessoryView.tsx +58 -0
  67. package/src/BottomAccessoryViewNativeComponent.ts +27 -0
  68. package/src/BottomTabs.tsx +545 -0
  69. package/src/BottomTabsNativeComponent.ts +69 -0
  70. package/src/DelayedFreeze.tsx +33 -0
  71. package/src/NativeSVGDecoder.ts +5 -0
  72. package/src/SceneMap.tsx +34 -0
  73. package/src/codegen-types.d.ts +28 -0
  74. package/src/index.tsx +18 -0
  75. package/src/types.ts +31 -0
  76. package/src/utils/BottomTabBarHeightContext.ts +5 -0
  77. package/src/utils/useBottomTabBarHeight.ts +15 -0
@@ -0,0 +1,545 @@
1
+ import React, { useLayoutEffect, useRef } from 'react';
2
+ import type { NativeSyntheticEvent } from 'react-native';
3
+ import {
4
+ type ColorValue,
5
+ type DimensionValue,
6
+ Image,
7
+ Platform,
8
+ type StyleProp,
9
+ StyleSheet,
10
+ View,
11
+ type ViewStyle,
12
+ processColor,
13
+ } from 'react-native';
14
+ import { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
15
+
16
+ // eslint-disable-next-line @react-native/no-deep-imports
17
+ import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
18
+ import NativeBottomTabs, {
19
+ type BottomTabsItems,
20
+ } from './BottomTabsNativeComponent';
21
+ import useLatestCallback from 'use-latest-callback';
22
+ import type { AppleIcon, BaseRoute, NavigationState, TabRole } from './types';
23
+ import DelayedFreeze from './DelayedFreeze';
24
+ import {
25
+ BottomAccessoryView,
26
+ type RNBottomTabsAccessoryViewProps,
27
+ } from './BottomAccessoryView';
28
+
29
+ const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
30
+ icon?.sfSymbol;
31
+
32
+ interface Props<Route extends BaseRoute> {
33
+ /*
34
+ * Whether to show labels in tabs. When false, only icons will be displayed.
35
+ */
36
+ labeled?: boolean;
37
+ /**
38
+ * A tab bar style that adapts to each platform.
39
+ *
40
+ * Tab views using the sidebar adaptable style have an appearance
41
+ * that varies depending on the platform:
42
+ * - iPadOS displays a top tab bar that can adapt into a sidebar.
43
+ * - iOS displays a bottom tab bar.
44
+ * - macOS and tvOS always show a sidebar.
45
+ * - visionOS shows an ornament and also shows a sidebar for secondary tabs within a `TabSection`.
46
+ */
47
+ sidebarAdaptable?: boolean;
48
+ /**
49
+ * Whether to disable page animations between tabs. (iOS only) Defaults to `false`.
50
+ */
51
+ disablePageAnimations?: boolean;
52
+ /**
53
+ * Whether to enable haptic feedback. Defaults to `false`.
54
+ */
55
+ hapticFeedbackEnabled?: boolean;
56
+ /**
57
+ * Describes the appearance attributes for the tabBar to use when an observable scroll view is scrolled to the bottom. (iOS only)
58
+ */
59
+ scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent';
60
+
61
+ /**
62
+ * Behavior for minimizing the tab bar. (iOS 26+)
63
+ */
64
+ minimizeBehavior?: 'automatic' | 'onScrollDown' | 'onScrollUp' | 'never';
65
+ /**
66
+ * Active tab color.
67
+ */
68
+ tabBarActiveTintColor?: ColorValue;
69
+ /**
70
+ * Inactive tab color.
71
+ */
72
+ tabBarInactiveTintColor?: ColorValue;
73
+ /**
74
+ * State for the tab view.
75
+ */
76
+ navigationState: NavigationState<Route>;
77
+ /**
78
+ * Function which takes an object with the route and returns a React element.
79
+ */
80
+ renderScene: (props: {
81
+ route: Route;
82
+ jumpTo: (key: string) => void;
83
+ }) => React.ReactNode | null;
84
+ /**
85
+ * Function which takes a route and returns a placeholder for an unloaded lazy scene.
86
+ */
87
+ renderLazyPlaceholder?: (props: { route: Route }) => React.ReactNode | null;
88
+ /**
89
+ * Callback which is called on tab change, receives the index of the new tab as argument.
90
+ */
91
+ onIndexChange: (index: number) => boolean | void;
92
+ /**
93
+ * Callback which is called when the same tab is pressed twice within the
94
+ * platform double-tap timeout, receives the index of the tab as argument.
95
+ * Each press still goes through `onIndexChange` / reselection first.
96
+ */
97
+ onTabDoublePress?: (index: number) => void;
98
+ /**
99
+ * Get lazy for the current screen. Uses true by default.
100
+ */
101
+ getLazy?: (props: { route: Route }) => boolean | undefined;
102
+ /**
103
+ * Get label text for the tab, uses `route.title` by default.
104
+ */
105
+ getLabelText?: (props: { route: Route }) => string | undefined;
106
+ /**
107
+ * Get badge for the tab, uses `route.badge` by default.
108
+ */
109
+ getBadge?: (props: { route: Route }) => string | undefined;
110
+ /**
111
+ * Get badge background color for the tab. (Android only)
112
+ */
113
+ getBadgeBackgroundColor?: (props: { route: Route }) => ColorValue | undefined;
114
+ /**
115
+ * Get badge text color for the tab. (Android only)
116
+ */
117
+ getBadgeTextColor?: (props: { route: Route }) => ColorValue | undefined;
118
+ /**
119
+ * Get active tint color for the tab.
120
+ */
121
+ getActiveTintColor?: (props: { route: Route }) => ColorValue | undefined;
122
+ /**
123
+ * Determines whether the tab prevents default action on press.
124
+ */
125
+ getPreventsDefault?: (props: { route: Route }) => boolean | undefined;
126
+ /**
127
+ * Get icon for the tab.
128
+ */
129
+ getIcon?: (props: {
130
+ route: Route;
131
+ focused: boolean;
132
+ }) => ImageSource | AppleIcon | undefined | null;
133
+
134
+ /**
135
+ * Get hidden for the tab.
136
+ */
137
+ getHidden?: (props: { route: Route }) => boolean | undefined;
138
+
139
+ /**
140
+ * Get testID for the tab.
141
+ */
142
+ getTestID?: (props: { route: Route }) => string | undefined;
143
+
144
+ /**
145
+ * Get role for the tab. (iOS only)
146
+ */
147
+ getRole?: (props: { route: Route }) => TabRole | undefined;
148
+
149
+ /**
150
+ * Custom tab bar to render.
151
+ */
152
+ tabBar?: () => React.ReactNode;
153
+
154
+ /**
155
+ * Get freezeOnBlur for the current screen.
156
+ */
157
+ getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined;
158
+
159
+ /**
160
+ * Get style for the scene.
161
+ */
162
+ getSceneStyle?: (props: { route: Route }) => StyleProp<ViewStyle>;
163
+
164
+ tabBarStyle?: {
165
+ backgroundColor?: ColorValue;
166
+ };
167
+
168
+ /**
169
+ * A Boolean value that indicates whether the tab bar is translucent. (iOS only)
170
+ */
171
+ translucent?: boolean;
172
+ rippleColor?: ColorValue;
173
+ /**
174
+ * Color of tab indicator. (Android only)
175
+ */
176
+ activeIndicatorColor?: ColorValue;
177
+ tabLabelStyle?: {
178
+ fontFamily?: string;
179
+ fontWeight?: string;
180
+ fontSize?: number;
181
+ };
182
+ /**
183
+ * A function that returns a React element to display as bottom accessory view.
184
+ * iOS 26+ only.
185
+ */
186
+ renderBottomAccessoryView?: RNBottomTabsAccessoryViewProps['renderBottomAccessoryView'];
187
+ /**
188
+ * Whether the tab bar is hidden.
189
+ */
190
+ tabBarHidden?: boolean;
191
+ /**
192
+ * Whether to ignore bottom system insets (navigation bar). Android only.
193
+ */
194
+ ignoreBottomInsets?: boolean;
195
+ /**
196
+ * Visual style of the Android tab bar. Android only.
197
+ */
198
+ androidTabBarStyle?: 'default' | 'floating';
199
+ /**
200
+ * Whether the tab bar ignores the soft keyboard. Defaults to true, so the
201
+ * keyboard covers the tab bar as on iOS. Set to false to pad the tab bar by
202
+ * the keyboard height so it rises above the keyboard. Android only.
203
+ */
204
+ ignoreKeyboardInsets?: boolean;
205
+ /**
206
+ * Whether to delay freeze/unfreeze by 200ms on tab switch.
207
+ * Defaults to false (immediate freeze). Set to true to restore the
208
+ * original delayed behavior.
209
+ */
210
+ delayedFreeze?: boolean;
211
+ }
212
+
213
+ const ANDROID_MAX_TABS = 100;
214
+
215
+ const BottomTabs = <Route extends BaseRoute>({
216
+ navigationState,
217
+ renderScene,
218
+ renderLazyPlaceholder,
219
+ onIndexChange,
220
+ onTabDoublePress,
221
+ rippleColor,
222
+ tabBarActiveTintColor: activeTintColor,
223
+ tabBarInactiveTintColor: inactiveTintColor,
224
+ getBadge = ({ route }: { route: Route }) => route.badge,
225
+ getBadgeBackgroundColor = ({ route }: { route: Route }) =>
226
+ route.badgeBackgroundColor,
227
+ getBadgeTextColor = ({ route }: { route: Route }) => route.badgeTextColor,
228
+ getLazy = ({ route }: { route: Route }) => route.lazy,
229
+ getLabelText = ({ route }: { route: Route }) => route.title,
230
+ getIcon = ({ route, focused }: { route: Route; focused: boolean }) =>
231
+ route.unfocusedIcon
232
+ ? focused
233
+ ? route.focusedIcon
234
+ : route.unfocusedIcon
235
+ : route.focusedIcon,
236
+ getHidden = ({ route }: { route: Route }) => route.hidden,
237
+ getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor,
238
+ getTestID = ({ route }: { route: Route }) => route.testID,
239
+ getRole = ({ route }: { route: Route }) => route.role,
240
+ getSceneStyle = ({ route }: { route: Route }) => route.style,
241
+ getPreventsDefault = ({ route }: { route: Route }) => route.preventsDefault,
242
+ hapticFeedbackEnabled = false,
243
+ labeled = Platform.OS !== 'android' ? true : undefined,
244
+ getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
245
+ tabBar: renderCustomTabBar,
246
+ tabBarStyle,
247
+ tabLabelStyle,
248
+ renderBottomAccessoryView,
249
+ activeIndicatorColor,
250
+ delayedFreeze = false,
251
+ ...props
252
+ }: Props<Route>) => {
253
+ // @ts-ignore
254
+ const focusedKey = navigationState.routes[navigationState.index].key;
255
+ const customTabBarWrapperRef = useRef<View>(null) as React.RefObject<any>;
256
+ const nativeBottomTabsRef = useRef<React.ElementRef<typeof NativeBottomTabs>>(null);
257
+ const [tabBarHeight, setTabBarHeight] = React.useState<number | undefined>(0);
258
+ const [measuredDimensions, setMeasuredDimensions] = React.useState<
259
+ { width: DimensionValue; height: DimensionValue } | undefined
260
+ >({ width: '100%', height: '100%' });
261
+
262
+ const trimmedRoutes = React.useMemo(() => {
263
+ if (
264
+ Platform.OS === 'android' &&
265
+ navigationState.routes.length > ANDROID_MAX_TABS
266
+ ) {
267
+ console.warn(
268
+ `BottomTabs only supports up to ${ANDROID_MAX_TABS} tabs on Android`
269
+ );
270
+ return navigationState.routes.slice(0, ANDROID_MAX_TABS);
271
+ }
272
+ return navigationState.routes;
273
+ }, [navigationState.routes]);
274
+
275
+ const [loaded, setLoaded] = React.useState<string[]>([focusedKey]);
276
+
277
+ if (!loaded.includes(focusedKey)) {
278
+ setLoaded((loaded) => [...loaded, focusedKey]);
279
+ }
280
+
281
+ const isIOS = Platform.OS === 'ios';
282
+
283
+ // Unfocused (outline) icons — independent of focusedKey so the array stays
284
+ // stable across tab switches and the native side never reloads on switch.
285
+ const unfocusedIcons = React.useMemo(
286
+ () => trimmedRoutes.map((route) => getIcon({ route, focused: false })),
287
+ [getIcon, trimmedRoutes]
288
+ );
289
+ // Focused (solid) icons — also stable; handed to iOS as selectedImage.
290
+ const focusedIcons = React.useMemo(
291
+ () => trimmedRoutes.map((route) => getIcon({ route, focused: true })),
292
+ [getIcon, trimmedRoutes]
293
+ );
294
+ // Legacy single array driven by focusedKey. Android still swaps the icon in
295
+ // JS (it has no selectedImage equivalent).
296
+ const focusKeyedIcons = React.useMemo(
297
+ () =>
298
+ trimmedRoutes.map((route) =>
299
+ getIcon({
300
+ route,
301
+ focused: route.key === focusedKey,
302
+ })
303
+ ),
304
+ [focusedKey, getIcon, trimmedRoutes]
305
+ );
306
+
307
+ // iOS hands both states to UIKit (image + selectedImage) so the focused
308
+ // artwork swaps natively, in sync with the selection animation, instead of
309
+ // waiting for a JS round-trip. Android keeps the focusedKey-driven array.
310
+ const icons = isIOS ? unfocusedIcons : focusKeyedIcons;
311
+
312
+ const items: BottomTabsItems[number][] = React.useMemo(
313
+ () =>
314
+ trimmedRoutes.map((route, index) => {
315
+ const icon = icons[index];
316
+ const isSfSymbol = isAppleSymbol(icon);
317
+
318
+ if (Platform.OS === 'android' && isSfSymbol) {
319
+ console.warn(
320
+ 'SF Symbols are not supported on Android. Use require() or pass uri to load an image instead.'
321
+ );
322
+ }
323
+
324
+ const color = processColor(getActiveTintColor({ route }));
325
+ const badgeBgColor = processColor(getBadgeBackgroundColor?.({ route }));
326
+ const badgeTxtColor = processColor(getBadgeTextColor?.({ route }));
327
+
328
+ return {
329
+ key: route.key,
330
+ title: getLabelText({ route }) ?? route.key,
331
+ sfSymbol: isSfSymbol ? icon.sfSymbol : undefined,
332
+ badge: getBadge?.({ route }),
333
+ badgeBackgroundColor:
334
+ typeof badgeBgColor === 'number' ? badgeBgColor : undefined,
335
+ badgeTextColor:
336
+ typeof badgeTxtColor === 'number' ? badgeTxtColor : undefined,
337
+ activeTintColor: typeof color === 'number' ? color : undefined,
338
+ hidden: getHidden?.({ route }),
339
+ testID: getTestID?.({ route }),
340
+ role: getRole?.({ route }),
341
+ preventsDefault: getPreventsDefault?.({ route }),
342
+ };
343
+ }),
344
+ [
345
+ trimmedRoutes,
346
+ icons,
347
+ getLabelText,
348
+ getBadge,
349
+ getBadgeBackgroundColor,
350
+ getBadgeTextColor,
351
+ getActiveTintColor,
352
+ getHidden,
353
+ getTestID,
354
+ getRole,
355
+ getPreventsDefault,
356
+ ]
357
+ );
358
+
359
+ const resolvedIconAssets = React.useMemo(
360
+ () =>
361
+ icons.map((icon) => {
362
+ if (icon && !isAppleSymbol(icon)) {
363
+ // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
364
+ const resolved = Image.resolveAssetSource(icon);
365
+ return {
366
+ uri: resolved?.uri ?? '',
367
+ width: resolved?.width ?? 0,
368
+ height: resolved?.height ?? 0,
369
+ scale: resolved?.scale ?? 1,
370
+ };
371
+ }
372
+ return { uri: '', width: 0, height: 0, scale: 1 };
373
+ }),
374
+ [icons]
375
+ );
376
+
377
+ // Focused (solid) assets passed to iOS as selectedImage. iOS only — Android
378
+ // ignores selectedIcons and keeps swapping via the focusedKey array above.
379
+ const resolvedSelectedIconAssets = React.useMemo(
380
+ () =>
381
+ focusedIcons.map((icon) => {
382
+ if (icon && !isAppleSymbol(icon)) {
383
+ // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
384
+ const resolved = Image.resolveAssetSource(icon);
385
+ return {
386
+ uri: resolved?.uri ?? '',
387
+ width: resolved?.width ?? 0,
388
+ height: resolved?.height ?? 0,
389
+ scale: resolved?.scale ?? 1,
390
+ };
391
+ }
392
+ return { uri: '', width: 0, height: 0, scale: 1 };
393
+ }),
394
+ [focusedIcons]
395
+ );
396
+
397
+ const jumpTo = useLatestCallback((key: string) => {
398
+ const index = trimmedRoutes.findIndex((route) => route.key === key);
399
+ if (index === -1) {
400
+ return;
401
+ }
402
+ return onIndexChange(index);
403
+ });
404
+
405
+ const handleTabDoublePress = React.useCallback(
406
+ (event: NativeSyntheticEvent<{ key: string }>) => {
407
+ const { key } = event.nativeEvent;
408
+ const index = trimmedRoutes.findIndex((route) => route.key === key);
409
+ if (index !== -1) {
410
+ onTabDoublePress?.(index);
411
+ }
412
+ },
413
+ [trimmedRoutes, onTabDoublePress]
414
+ );
415
+
416
+ const handlePageSelected = React.useCallback(
417
+ (event: NativeSyntheticEvent<{ key: string }>) => {
418
+ const { key } = event.nativeEvent;
419
+ const accepted = jumpTo(key);
420
+ if (accepted === false) {
421
+ nativeBottomTabsRef.current?.setNativeProps({ selectedPage: focusedKey });
422
+ }
423
+ },
424
+ [focusedKey, jumpTo],
425
+ );
426
+
427
+ const handleTabBarMeasured = React.useCallback(
428
+ (event: NativeSyntheticEvent<{ height: number }>) => {
429
+ setTabBarHeight(event.nativeEvent.height);
430
+ },
431
+ [setTabBarHeight]
432
+ );
433
+
434
+ const handleNativeLayout = React.useCallback(
435
+ (event: NativeSyntheticEvent<{ width: number; height: number }>) => {
436
+ const { width, height } = event.nativeEvent;
437
+ setMeasuredDimensions({ width, height });
438
+ },
439
+ [setMeasuredDimensions]
440
+ );
441
+
442
+ useLayoutEffect(() => {
443
+ if (renderCustomTabBar && customTabBarWrapperRef.current) {
444
+ customTabBarWrapperRef.current.measure((_x: number, _y: number, _width: number, height: number) => {
445
+ setTabBarHeight(height);
446
+ });
447
+ }
448
+ }, [renderCustomTabBar]);
449
+
450
+ return (
451
+ <BottomTabBarHeightContext.Provider value={tabBarHeight}>
452
+ <NativeBottomTabs
453
+ ref={nativeBottomTabsRef}
454
+ {...props}
455
+ {...tabLabelStyle}
456
+ style={styles.fullWidth}
457
+ items={items}
458
+ icons={renderCustomTabBar ? undefined : resolvedIconAssets}
459
+ selectedIcons={
460
+ isIOS && !renderCustomTabBar ? resolvedSelectedIconAssets : undefined
461
+ }
462
+ selectedPage={focusedKey}
463
+ tabBarHidden={props.tabBarHidden ?? !!renderCustomTabBar}
464
+ onTabDoublePress={handleTabDoublePress}
465
+ onPageSelected={handlePageSelected}
466
+ onTabBarMeasured={handleTabBarMeasured}
467
+ onNativeLayout={handleNativeLayout}
468
+ hapticFeedbackEnabled={hapticFeedbackEnabled}
469
+ activeTintColor={activeTintColor}
470
+ inactiveTintColor={inactiveTintColor}
471
+ barTintColor={tabBarStyle?.backgroundColor}
472
+ rippleColor={rippleColor}
473
+ activeIndicatorColor={activeIndicatorColor}
474
+ labeled={labeled}
475
+ >
476
+ {trimmedRoutes.map((route) => {
477
+ if (getLazy({ route }) !== false && !loaded.includes(route.key)) {
478
+ return (
479
+ <View
480
+ key={route.key}
481
+ collapsable={false}
482
+ style={styles.fullWidth}
483
+ >
484
+ {renderLazyPlaceholder?.({ route })}
485
+ </View>
486
+ );
487
+ }
488
+
489
+ const focused = route.key === focusedKey;
490
+ const freeze = !focused ? getFreezeOnBlur({ route }) : false;
491
+
492
+ const customStyle = getSceneStyle({ route });
493
+
494
+ return (
495
+ <View
496
+ key={route.key}
497
+ style={[
498
+ styles.screen,
499
+ renderCustomTabBar ? styles.fullWidth : measuredDimensions,
500
+ customStyle,
501
+ ]}
502
+ collapsable={false}
503
+ pointerEvents={focused ? 'auto' : 'none'}
504
+ accessibilityElementsHidden={!focused}
505
+ importantForAccessibility={
506
+ focused ? 'auto' : 'no-hide-descendants'
507
+ }
508
+ >
509
+ <DelayedFreeze freeze={!!freeze} delayedFreeze={delayedFreeze}>
510
+ {renderScene({
511
+ route,
512
+ jumpTo,
513
+ })}
514
+ </DelayedFreeze>
515
+ </View>
516
+ );
517
+ })}
518
+ {Platform.OS === 'ios' &&
519
+ parseFloat(Platform.Version) >= 26 &&
520
+ renderBottomAccessoryView &&
521
+ !renderCustomTabBar ? (
522
+ <BottomAccessoryView
523
+ renderBottomAccessoryView={renderBottomAccessoryView}
524
+ />
525
+ ) : null}
526
+ </NativeBottomTabs>
527
+ {renderCustomTabBar ? (
528
+ <View ref={customTabBarWrapperRef}>{renderCustomTabBar()}</View>
529
+ ) : null}
530
+ </BottomTabBarHeightContext.Provider>
531
+ );
532
+ };
533
+
534
+ const styles = StyleSheet.create({
535
+ fullWidth: {
536
+ width: '100%',
537
+ height: '100%',
538
+ flex: 1,
539
+ },
540
+ screen: {
541
+ position: 'absolute',
542
+ },
543
+ });
544
+
545
+ export default BottomTabs;
@@ -0,0 +1,69 @@
1
+ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
2
+ import type { ColorValue, ProcessedColorValue, ViewProps } from 'react-native';
3
+ import type {
4
+ DirectEventHandler,
5
+ Double,
6
+ Int32,
7
+ WithDefault,
8
+ } from 'react-native/Libraries/Types/CodegenTypes';
9
+ //@ts-ignore
10
+ import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
11
+
12
+ export type OnPageSelectedEventData = Readonly<{
13
+ key: string;
14
+ }>;
15
+
16
+ export type OnTabBarMeasured = Readonly<{
17
+ height: Int32;
18
+ }>;
19
+
20
+ export type OnNativeLayout = Readonly<{
21
+ width: Double;
22
+ height: Double;
23
+ }>;
24
+
25
+ export type BottomTabsItems = ReadonlyArray<{
26
+ key: string;
27
+ title: string;
28
+ sfSymbol?: string;
29
+ badge?: string;
30
+ badgeBackgroundColor?: ProcessedColorValue | null;
31
+ badgeTextColor?: ProcessedColorValue | null;
32
+ activeTintColor?: ProcessedColorValue | null;
33
+ hidden?: boolean;
34
+ testID?: string;
35
+ role?: string;
36
+ preventsDefault?: boolean;
37
+ }>;
38
+
39
+ export interface BottomTabsNativeProps extends ViewProps {
40
+ items: BottomTabsItems;
41
+ selectedPage: string;
42
+ onPageSelected?: DirectEventHandler<OnPageSelectedEventData>;
43
+ onTabDoublePress?: DirectEventHandler<OnPageSelectedEventData>;
44
+ onTabBarMeasured?: DirectEventHandler<OnTabBarMeasured>;
45
+ onNativeLayout?: DirectEventHandler<OnNativeLayout>;
46
+ icons?: ReadonlyArray<ImageSource>;
47
+ selectedIcons?: ReadonlyArray<ImageSource>;
48
+ tabBarHidden?: boolean;
49
+ labeled?: boolean;
50
+ sidebarAdaptable?: boolean;
51
+ scrollEdgeAppearance?: string;
52
+ barTintColor?: ColorValue;
53
+ translucent?: WithDefault<boolean, true>;
54
+ rippleColor?: ColorValue;
55
+ activeTintColor?: ColorValue;
56
+ inactiveTintColor?: ColorValue;
57
+ disablePageAnimations?: boolean;
58
+ activeIndicatorColor?: ColorValue;
59
+ hapticFeedbackEnabled?: boolean;
60
+ minimizeBehavior?: string;
61
+ fontFamily?: string;
62
+ fontWeight?: string;
63
+ fontSize?: Int32;
64
+ ignoreBottomInsets?: boolean;
65
+ androidTabBarStyle?: WithDefault<'default' | 'floating', 'default'>;
66
+ ignoreKeyboardInsets?: WithDefault<boolean, true>;
67
+ }
68
+
69
+ export default codegenNativeComponent<BottomTabsNativeProps>('RNBottomTabsView');
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { Freeze } from 'react-freeze';
3
+
4
+ const ANIMATION_DELAY = 200;
5
+
6
+ interface FreezeWrapperProps {
7
+ freeze: boolean;
8
+ delayedFreeze?: boolean;
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ function DelayedFreeze({ freeze, delayedFreeze, children }: FreezeWrapperProps) {
13
+ const [freezeState, setFreezeState] = React.useState(false);
14
+
15
+ React.useEffect(() => {
16
+ if (!delayedFreeze) {
17
+ return;
18
+ }
19
+ const id = setTimeout(() => {
20
+ setFreezeState(freeze);
21
+ }, ANIMATION_DELAY);
22
+
23
+ return () => {
24
+ clearTimeout(id);
25
+ };
26
+ }, [freeze, delayedFreeze]);
27
+
28
+ const shouldFreeze = delayedFreeze ? (freeze ? freezeState : false) : freeze;
29
+
30
+ return <Freeze freeze={shouldFreeze}>{children}</Freeze>;
31
+ }
32
+
33
+ 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
+ }