@onekeyfe/react-native-bottom-tabs 6.7.0-18 → 6.7.0-20

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 (28) hide show
  1. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerManager.kt +55 -0
  2. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerView.kt +496 -0
  3. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +1 -1
  4. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +3 -0
  5. package/docs/SPEC.md +163 -2
  6. package/ios/RCTBottomTabsComponentView.mm +37 -0
  7. package/ios/RCTBottomTabsContainerView.swift +192 -7
  8. package/ios/RCTBottomTabsDrawerComponentView.h +8 -0
  9. package/ios/RCTBottomTabsDrawerComponentView.mm +79 -0
  10. package/ios/RCTBottomTabsDrawerContainerView.swift +327 -0
  11. package/lib/module/BottomTabs.js +144 -87
  12. package/lib/module/BottomTabsDrawerNativeComponent.ts +19 -0
  13. package/lib/module/BottomTabsDrawerNativeComponent.web.js +13 -0
  14. package/lib/module/BottomTabsNativeComponent.ts +13 -0
  15. package/lib/typescript/src/BottomTabs.d.ts +36 -10
  16. package/lib/typescript/src/BottomTabsDrawerNativeComponent.d.ts +15 -0
  17. package/lib/typescript/src/BottomTabsDrawerNativeComponent.web.d.ts +4 -0
  18. package/lib/typescript/src/BottomTabsNativeComponent.d.ts +8 -0
  19. package/lib/typescript/src/index.d.ts +1 -1
  20. package/lib/typescript/src/types.d.ts +1 -0
  21. package/package.json +3 -2
  22. package/react-native.config.js +4 -1
  23. package/src/BottomTabs.tsx +235 -115
  24. package/src/BottomTabsDrawerNativeComponent.ts +19 -0
  25. package/src/BottomTabsDrawerNativeComponent.web.tsx +9 -0
  26. package/src/BottomTabsNativeComponent.ts +13 -0
  27. package/src/index.tsx +1 -1
  28. package/src/types.ts +3 -0
@@ -1,30 +1,38 @@
1
- import React, { useLayoutEffect, useRef } from 'react';
2
- import type { NativeSyntheticEvent } from 'react-native';
1
+ import React, { useLayoutEffect, useRef } from "react";
2
+ import type { NativeSyntheticEvent } from "react-native";
3
3
  import {
4
4
  type ColorValue,
5
5
  type DimensionValue,
6
6
  Image,
7
+ type ImageSourcePropType,
7
8
  Platform,
8
9
  type StyleProp,
9
10
  StyleSheet,
10
11
  View,
11
12
  type ViewStyle,
12
13
  processColor,
13
- } from 'react-native';
14
- import { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
14
+ } from "react-native";
15
+ import { BottomTabBarHeightContext } from "./utils/BottomTabBarHeightContext";
15
16
 
16
17
  // eslint-disable-next-line @react-native/no-deep-imports
17
- import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
18
+ import type { ImageSource } from "react-native/Libraries/Image/ImageSource";
18
19
  import NativeBottomTabs, {
19
20
  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';
21
+ } from "./BottomTabsNativeComponent";
22
+ import useLatestCallback from "use-latest-callback";
23
+ import type {
24
+ AppleIcon,
25
+ BaseRoute,
26
+ NavigationState,
27
+ TabBarPlacement,
28
+ TabRole,
29
+ } from "./types";
30
+ import DelayedFreeze from "./DelayedFreeze";
31
+ import NativeBottomTabsDrawer from "./BottomTabsDrawerNativeComponent";
24
32
  import {
25
33
  BottomAccessoryView,
26
34
  type RNBottomTabsAccessoryViewProps,
27
- } from './BottomAccessoryView';
35
+ } from "./BottomAccessoryView";
28
36
 
29
37
  const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
30
38
  icon?.sfSymbol;
@@ -34,6 +42,14 @@ interface Props<Route extends BaseRoute> {
34
42
  * Whether to show labels in tabs. When false, only icons will be displayed.
35
43
  */
36
44
  labeled?: boolean;
45
+ /** Optional physical-left menu; all motion and input are owned by native. */
46
+ renderDrawerContent?: () => React.ReactNode;
47
+ /** External drawer target. Mirror onDrawerOpenChange for user transitions. */
48
+ drawerOpen?: boolean;
49
+ /** Enables root-screen content swipes after inner horizontal views reach their boundary. */
50
+ drawerSwipeEnabled?: boolean;
51
+ /** Reports a completed user target so the external open state can follow it. */
52
+ onDrawerOpenChange?: (open: boolean) => void;
37
53
  /**
38
54
  * A tab bar style that adapts to each platform.
39
55
  *
@@ -56,12 +72,12 @@ interface Props<Route extends BaseRoute> {
56
72
  /**
57
73
  * Describes the appearance attributes for the tabBar to use when an observable scroll view is scrolled to the bottom. (iOS only)
58
74
  */
59
- scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent';
75
+ scrollEdgeAppearance?: "default" | "opaque" | "transparent";
60
76
 
61
77
  /**
62
78
  * Behavior for minimizing the tab bar. (iOS 26+)
63
79
  */
64
- minimizeBehavior?: 'automatic' | 'onScrollDown' | 'onScrollUp' | 'never';
80
+ minimizeBehavior?: "automatic" | "onScrollDown" | "onScrollUp" | "never";
65
81
  /**
66
82
  * Active tab color.
67
83
  */
@@ -190,7 +206,7 @@ interface Props<Route extends BaseRoute> {
190
206
  * A function that returns a React element to display as bottom accessory view.
191
207
  * iOS 26+ only.
192
208
  */
193
- renderBottomAccessoryView?: RNBottomTabsAccessoryViewProps['renderBottomAccessoryView'];
209
+ renderBottomAccessoryView?: RNBottomTabsAccessoryViewProps["renderBottomAccessoryView"];
194
210
  /**
195
211
  * Whether the tab bar is hidden.
196
212
  */
@@ -202,7 +218,7 @@ interface Props<Route extends BaseRoute> {
202
218
  /**
203
219
  * Visual style of the Android tab bar. Android only.
204
220
  */
205
- androidTabBarStyle?: 'default' | 'floating';
221
+ androidTabBarStyle?: "default" | "floating";
206
222
  /**
207
223
  * Whether the tab bar ignores the soft keyboard. Defaults to true, so the
208
224
  * keyboard covers the tab bar as on iOS. Set to false to pad the tab bar by
@@ -215,6 +231,24 @@ interface Props<Route extends BaseRoute> {
215
231
  * original delayed behavior.
216
232
  */
217
233
  delayedFreeze?: boolean;
234
+ /**
235
+ * Called when the system moves the tab bar between the bottom edge and a
236
+ * vertical bar on the leading or trailing edge (iPhone Duo). iOS only.
237
+ */
238
+ onTabBarPlacementChange?: (placement: TabBarPlacement) => void;
239
+ /**
240
+ * Icon of a round button shown above a vertical tab bar. The button only
241
+ * appears while the tab bar is vertical and visible. iOS only.
242
+ */
243
+ sideAccessoryIcon?: ImageSourcePropType;
244
+ /**
245
+ * Accessibility label of the side accessory button.
246
+ */
247
+ sideAccessoryLabel?: string;
248
+ /**
249
+ * Called when the side accessory button is pressed.
250
+ */
251
+ onSideAccessoryPress?: () => void;
218
252
  }
219
253
 
220
254
  const ANDROID_MAX_TABS = 100;
@@ -225,6 +259,10 @@ const BottomTabs = <Route extends BaseRoute>({
225
259
  renderLazyPlaceholder,
226
260
  onIndexChange,
227
261
  onTabDoublePress,
262
+ renderDrawerContent,
263
+ drawerOpen = false,
264
+ drawerSwipeEnabled = false,
265
+ onDrawerOpenChange,
228
266
  rippleColor,
229
267
  tabBarActiveTintColor: activeTintColor,
230
268
  tabBarInactiveTintColor: inactiveTintColor,
@@ -247,30 +285,41 @@ const BottomTabs = <Route extends BaseRoute>({
247
285
  getSceneStyle = ({ route }: { route: Route }) => route.style,
248
286
  getPreventsDefault = ({ route }: { route: Route }) => route.preventsDefault,
249
287
  hapticFeedbackEnabled = false,
250
- labeled = Platform.OS !== 'android' ? true : undefined,
288
+ labeled = Platform.OS !== "android" ? true : undefined,
251
289
  getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
252
290
  tabBar: renderCustomTabBar,
253
291
  tabBarStyle,
254
292
  tabLabelStyle,
255
293
  renderBottomAccessoryView,
256
294
  activeIndicatorColor,
257
- androidBottomBlurEnabled = Platform.OS === 'android' &&
295
+ androidBottomBlurEnabled = Platform.OS === "android" &&
258
296
  Number(Platform.Version) >= 36,
259
297
  delayedFreeze = false,
298
+ onTabBarPlacementChange,
299
+ sideAccessoryIcon,
300
+ sideAccessoryLabel,
301
+ onSideAccessoryPress,
260
302
  ...props
261
303
  }: Props<Route>) => {
262
304
  // @ts-ignore
263
305
  const focusedKey = navigationState.routes[navigationState.index].key;
264
306
  const customTabBarWrapperRef = useRef<View>(null) as React.RefObject<any>;
265
- const nativeBottomTabsRef = useRef<React.ElementRef<typeof NativeBottomTabs>>(null);
266
- const [tabBarHeight, setTabBarHeight] = React.useState<number | undefined>(0);
307
+ const nativeBottomTabsRef =
308
+ useRef<React.ElementRef<typeof NativeBottomTabs>>(null);
309
+ const [nativeTabBarHeight, setNativeTabBarHeight] = React.useState(0);
310
+ const [customTabBarHeight, setCustomTabBarHeight] = React.useState(0);
311
+ const tabBarHeight = renderCustomTabBar
312
+ ? customTabBarHeight
313
+ : props.tabBarHidden
314
+ ? 0
315
+ : nativeTabBarHeight;
267
316
  const [measuredDimensions, setMeasuredDimensions] = React.useState<
268
317
  { width: DimensionValue; height: DimensionValue } | undefined
269
- >({ width: '100%', height: '100%' });
318
+ >({ width: "100%", height: "100%" });
270
319
 
271
320
  const trimmedRoutes = React.useMemo(() => {
272
321
  if (
273
- Platform.OS === 'android' &&
322
+ Platform.OS === "android" &&
274
323
  navigationState.routes.length > ANDROID_MAX_TABS
275
324
  ) {
276
325
  console.warn(
@@ -287,7 +336,7 @@ const BottomTabs = <Route extends BaseRoute>({
287
336
  setLoaded((loaded) => [...loaded, focusedKey]);
288
337
  }
289
338
 
290
- const isIOS = Platform.OS === 'ios';
339
+ const isIOS = Platform.OS === "ios";
291
340
 
292
341
  // Unfocused (outline) icons — independent of focusedKey so the array stays
293
342
  // stable across tab switches and the native side never reloads on switch.
@@ -324,9 +373,9 @@ const BottomTabs = <Route extends BaseRoute>({
324
373
  const icon = icons[index];
325
374
  const isSfSymbol = isAppleSymbol(icon);
326
375
 
327
- if (Platform.OS === 'android' && isSfSymbol) {
376
+ if (Platform.OS === "android" && isSfSymbol) {
328
377
  console.warn(
329
- 'SF Symbols are not supported on Android. Use require() or pass uri to load an image instead.'
378
+ "SF Symbols are not supported on Android. Use require() or pass uri to load an image instead."
330
379
  );
331
380
  }
332
381
 
@@ -340,10 +389,10 @@ const BottomTabs = <Route extends BaseRoute>({
340
389
  sfSymbol: isSfSymbol ? icon.sfSymbol : undefined,
341
390
  badge: getBadge?.({ route }),
342
391
  badgeBackgroundColor:
343
- typeof badgeBgColor === 'number' ? badgeBgColor : undefined,
392
+ typeof badgeBgColor === "number" ? badgeBgColor : undefined,
344
393
  badgeTextColor:
345
- typeof badgeTxtColor === 'number' ? badgeTxtColor : undefined,
346
- activeTintColor: typeof color === 'number' ? color : undefined,
394
+ typeof badgeTxtColor === "number" ? badgeTxtColor : undefined,
395
+ activeTintColor: typeof color === "number" ? color : undefined,
347
396
  hidden: getHidden?.({ route }),
348
397
  testID: getTestID?.({ route }),
349
398
  role: getRole?.({ route }),
@@ -372,13 +421,13 @@ const BottomTabs = <Route extends BaseRoute>({
372
421
  // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
373
422
  const resolved = Image.resolveAssetSource(icon);
374
423
  return {
375
- uri: resolved?.uri ?? '',
424
+ uri: resolved?.uri ?? "",
376
425
  width: resolved?.width ?? 0,
377
426
  height: resolved?.height ?? 0,
378
427
  scale: resolved?.scale ?? 1,
379
428
  };
380
429
  }
381
- return { uri: '', width: 0, height: 0, scale: 1 };
430
+ return { uri: "", width: 0, height: 0, scale: 1 };
382
431
  }),
383
432
  [icons]
384
433
  );
@@ -392,13 +441,13 @@ const BottomTabs = <Route extends BaseRoute>({
392
441
  // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
393
442
  const resolved = Image.resolveAssetSource(icon);
394
443
  return {
395
- uri: resolved?.uri ?? '',
444
+ uri: resolved?.uri ?? "",
396
445
  width: resolved?.width ?? 0,
397
446
  height: resolved?.height ?? 0,
398
447
  scale: resolved?.scale ?? 1,
399
448
  };
400
449
  }
401
- return { uri: '', width: 0, height: 0, scale: 1 };
450
+ return { uri: "", width: 0, height: 0, scale: 1 };
402
451
  }),
403
452
  [focusedIcons]
404
453
  );
@@ -427,17 +476,19 @@ const BottomTabs = <Route extends BaseRoute>({
427
476
  const { key } = event.nativeEvent;
428
477
  const accepted = jumpTo(key);
429
478
  if (accepted === false) {
430
- nativeBottomTabsRef.current?.setNativeProps({ selectedPage: focusedKey });
479
+ nativeBottomTabsRef.current?.setNativeProps({
480
+ selectedPage: focusedKey,
481
+ });
431
482
  }
432
483
  },
433
- [focusedKey, jumpTo],
484
+ [focusedKey, jumpTo]
434
485
  );
435
486
 
436
487
  const handleTabBarMeasured = React.useCallback(
437
488
  (event: NativeSyntheticEvent<{ height: number }>) => {
438
- setTabBarHeight(event.nativeEvent.height);
489
+ setNativeTabBarHeight(event.nativeEvent.height);
439
490
  },
440
- [setTabBarHeight]
491
+ [setNativeTabBarHeight]
441
492
  );
442
493
 
443
494
  const handleNativeLayout = React.useCallback(
@@ -448,111 +499,180 @@ const BottomTabs = <Route extends BaseRoute>({
448
499
  [setMeasuredDimensions]
449
500
  );
450
501
 
502
+ const handleTabBarPlacementChange = React.useCallback(
503
+ (event: NativeSyntheticEvent<{ placement: string }>) => {
504
+ onTabBarPlacementChange?.(event.nativeEvent.placement as TabBarPlacement);
505
+ },
506
+ [onTabBarPlacementChange]
507
+ );
508
+
509
+ const handleSideAccessoryPress = React.useCallback(() => {
510
+ onSideAccessoryPress?.();
511
+ }, [onSideAccessoryPress]);
512
+
513
+ const resolvedSideAccessoryIcon = React.useMemo(() => {
514
+ if (!isIOS || !sideAccessoryIcon) {
515
+ return undefined;
516
+ }
517
+ // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
518
+ const resolved = Image.resolveAssetSource(sideAccessoryIcon);
519
+ return resolved?.uri
520
+ ? {
521
+ uri: resolved.uri,
522
+ width: resolved.width ?? 0,
523
+ height: resolved.height ?? 0,
524
+ scale: resolved.scale ?? 1,
525
+ }
526
+ : undefined;
527
+ }, [sideAccessoryIcon]);
528
+
451
529
  useLayoutEffect(() => {
452
530
  if (renderCustomTabBar && customTabBarWrapperRef.current) {
453
- customTabBarWrapperRef.current.measure((_x: number, _y: number, _width: number, height: number) => {
454
- setTabBarHeight(height);
455
- });
531
+ customTabBarWrapperRef.current.measure(
532
+ (_x: number, _y: number, _width: number, height: number) => {
533
+ setCustomTabBarHeight(height);
534
+ }
535
+ );
456
536
  }
457
537
  }, [renderCustomTabBar]);
458
538
 
459
539
  return (
460
540
  <BottomTabBarHeightContext.Provider value={tabBarHeight}>
461
- <NativeBottomTabs
462
- ref={nativeBottomTabsRef}
463
- {...props}
464
- {...tabLabelStyle}
541
+ <NativeBottomTabsDrawer
465
542
  style={styles.fullWidth}
466
- items={items}
467
- icons={renderCustomTabBar ? undefined : resolvedIconAssets}
468
- selectedIcons={
469
- isIOS && !renderCustomTabBar ? resolvedSelectedIconAssets : undefined
470
- }
471
- selectedPage={focusedKey}
472
- tabBarHidden={props.tabBarHidden ?? !!renderCustomTabBar}
473
- onTabDoublePress={handleTabDoublePress}
474
- onPageSelected={handlePageSelected}
475
- onTabBarMeasured={handleTabBarMeasured}
476
- onNativeLayout={handleNativeLayout}
543
+ enabled={!!renderDrawerContent}
544
+ open={drawerOpen}
545
+ swipeEnabled={drawerSwipeEnabled}
477
546
  hapticFeedbackEnabled={hapticFeedbackEnabled}
478
- activeTintColor={activeTintColor}
479
- inactiveTintColor={inactiveTintColor}
480
- barTintColor={tabBarStyle?.backgroundColor}
481
- rippleColor={rippleColor}
482
- activeIndicatorColor={activeIndicatorColor}
483
- androidBottomBlurEnabled={
484
- Platform.OS === 'android' &&
485
- !renderCustomTabBar &&
486
- androidBottomBlurEnabled
547
+ bottomBarHeight={tabBarHeight ?? 0}
548
+ onDrawerOpenChange={(event) =>
549
+ onDrawerOpenChange?.(event.nativeEvent.open)
487
550
  }
488
- labeled={labeled}
489
551
  >
490
- {trimmedRoutes.map((route) => {
491
- if (getLazy({ route }) !== false && !loaded.includes(route.key)) {
492
- return (
493
- <View
494
- key={route.key}
495
- collapsable={false}
496
- style={styles.fullWidth}
497
- >
498
- {renderLazyPlaceholder?.({ route })}
499
- </View>
500
- );
501
- }
502
-
503
- const focused = route.key === focusedKey;
504
- const freeze = !focused ? getFreezeOnBlur({ route }) : false;
505
-
506
- const customStyle = getSceneStyle({ route });
507
-
508
- return (
552
+ <View collapsable={false} style={styles.drawerMenu}>
553
+ {Platform.OS !== "web" ? renderDrawerContent?.() : null}
554
+ </View>
555
+ <View collapsable={false} style={styles.fullWidth}>
556
+ <NativeBottomTabs
557
+ ref={nativeBottomTabsRef}
558
+ {...props}
559
+ {...tabLabelStyle}
560
+ style={styles.fullWidth}
561
+ items={items}
562
+ icons={renderCustomTabBar ? undefined : resolvedIconAssets}
563
+ selectedIcons={
564
+ isIOS && !renderCustomTabBar
565
+ ? resolvedSelectedIconAssets
566
+ : undefined
567
+ }
568
+ selectedPage={focusedKey}
569
+ tabBarHidden={props.tabBarHidden ?? !!renderCustomTabBar}
570
+ onTabDoublePress={handleTabDoublePress}
571
+ onPageSelected={handlePageSelected}
572
+ onTabBarMeasured={handleTabBarMeasured}
573
+ onNativeLayout={handleNativeLayout}
574
+ onTabBarPlacementChange={handleTabBarPlacementChange}
575
+ sideAccessoryIcon={
576
+ renderCustomTabBar ? undefined : resolvedSideAccessoryIcon
577
+ }
578
+ sideAccessoryLabel={sideAccessoryLabel}
579
+ onSideAccessoryPress={handleSideAccessoryPress}
580
+ hapticFeedbackEnabled={hapticFeedbackEnabled}
581
+ activeTintColor={activeTintColor}
582
+ inactiveTintColor={inactiveTintColor}
583
+ barTintColor={tabBarStyle?.backgroundColor}
584
+ rippleColor={rippleColor}
585
+ activeIndicatorColor={activeIndicatorColor}
586
+ androidBottomBlurEnabled={
587
+ Platform.OS === "android" &&
588
+ !renderCustomTabBar &&
589
+ androidBottomBlurEnabled
590
+ }
591
+ labeled={labeled}
592
+ >
593
+ {trimmedRoutes.map((route) => {
594
+ if (getLazy({ route }) !== false && !loaded.includes(route.key)) {
595
+ return (
596
+ <View
597
+ key={route.key}
598
+ collapsable={false}
599
+ style={styles.fullWidth}
600
+ >
601
+ {renderLazyPlaceholder?.({ route })}
602
+ </View>
603
+ );
604
+ }
605
+
606
+ const focused = route.key === focusedKey;
607
+ const freeze = !focused ? getFreezeOnBlur({ route }) : false;
608
+
609
+ const customStyle = getSceneStyle({ route });
610
+
611
+ return (
612
+ <View
613
+ key={route.key}
614
+ style={[
615
+ styles.screen,
616
+ renderCustomTabBar ? styles.fullWidth : measuredDimensions,
617
+ customStyle,
618
+ ]}
619
+ collapsable={false}
620
+ pointerEvents={focused ? "auto" : "none"}
621
+ accessibilityElementsHidden={!focused}
622
+ importantForAccessibility={
623
+ focused ? "auto" : "no-hide-descendants"
624
+ }
625
+ >
626
+ <DelayedFreeze
627
+ freeze={!!freeze}
628
+ delayedFreeze={delayedFreeze}
629
+ >
630
+ {renderScene({
631
+ route,
632
+ jumpTo,
633
+ })}
634
+ </DelayedFreeze>
635
+ </View>
636
+ );
637
+ })}
638
+ {Platform.OS === "ios" &&
639
+ parseFloat(Platform.Version) >= 26 &&
640
+ renderBottomAccessoryView &&
641
+ !renderCustomTabBar ? (
642
+ <BottomAccessoryView
643
+ renderBottomAccessoryView={renderBottomAccessoryView}
644
+ />
645
+ ) : null}
646
+ </NativeBottomTabs>
647
+ {renderCustomTabBar ? (
509
648
  <View
510
- key={route.key}
511
- style={[
512
- styles.screen,
513
- renderCustomTabBar ? styles.fullWidth : measuredDimensions,
514
- customStyle,
515
- ]}
516
- collapsable={false}
517
- pointerEvents={focused ? 'auto' : 'none'}
518
- accessibilityElementsHidden={!focused}
519
- importantForAccessibility={
520
- focused ? 'auto' : 'no-hide-descendants'
649
+ ref={customTabBarWrapperRef}
650
+ onLayout={(event) =>
651
+ setCustomTabBarHeight(event.nativeEvent.layout.height)
521
652
  }
522
653
  >
523
- <DelayedFreeze freeze={!!freeze} delayedFreeze={delayedFreeze}>
524
- {renderScene({
525
- route,
526
- jumpTo,
527
- })}
528
- </DelayedFreeze>
654
+ {renderCustomTabBar()}
529
655
  </View>
530
- );
531
- })}
532
- {Platform.OS === 'ios' &&
533
- parseFloat(Platform.Version) >= 26 &&
534
- renderBottomAccessoryView &&
535
- !renderCustomTabBar ? (
536
- <BottomAccessoryView
537
- renderBottomAccessoryView={renderBottomAccessoryView}
538
- />
539
- ) : null}
540
- </NativeBottomTabs>
541
- {renderCustomTabBar ? (
542
- <View ref={customTabBarWrapperRef}>{renderCustomTabBar()}</View>
543
- ) : null}
656
+ ) : null}
657
+ </View>
658
+ </NativeBottomTabsDrawer>
544
659
  </BottomTabBarHeightContext.Provider>
545
660
  );
546
661
  };
547
662
 
548
663
  const styles = StyleSheet.create({
549
664
  fullWidth: {
550
- width: '100%',
551
- height: '100%',
665
+ width: "100%",
666
+ height: "100%",
552
667
  flex: 1,
553
668
  },
669
+ drawerMenu: {
670
+ position: "absolute",
671
+ width: "78%",
672
+ height: "100%",
673
+ },
554
674
  screen: {
555
- position: 'absolute',
675
+ position: "absolute",
556
676
  },
557
677
  });
558
678
 
@@ -0,0 +1,19 @@
1
+ import { codegenNativeComponent, type ViewProps } from "react-native";
2
+ import type {
3
+ DirectEventHandler,
4
+ Double,
5
+ WithDefault,
6
+ } from "react-native/Libraries/Types/CodegenTypes";
7
+
8
+ export interface BottomTabsDrawerNativeProps extends ViewProps {
9
+ enabled?: WithDefault<boolean, false>;
10
+ open?: WithDefault<boolean, false>;
11
+ swipeEnabled?: WithDefault<boolean, false>;
12
+ hapticFeedbackEnabled?: WithDefault<boolean, false>;
13
+ bottomBarHeight?: Double;
14
+ onDrawerOpenChange?: DirectEventHandler<Readonly<{ open: boolean }>>;
15
+ }
16
+
17
+ export default codegenNativeComponent<BottomTabsDrawerNativeProps>(
18
+ "RNBottomTabsDrawer"
19
+ );
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import type { BottomTabsDrawerNativeProps } from "./BottomTabsDrawerNativeComponent";
3
+
4
+ // The fixed first slot is the menu; Web only renders the foreground slot.
5
+ export default function BottomTabsDrawerWeb({
6
+ children,
7
+ }: React.PropsWithChildren<BottomTabsDrawerNativeProps>) {
8
+ return <>{React.Children.toArray(children)[1]}</>;
9
+ }
@@ -22,6 +22,14 @@ export type OnNativeLayout = Readonly<{
22
22
  height: Double;
23
23
  }>;
24
24
 
25
+ // Where the system draws the tab bar: 'bottom', or 'leading' / 'trailing' for
26
+ // a vertical bar (the iPhone Duo side strip, iOS 27.1+).
27
+ export type OnTabBarPlacementChange = Readonly<{
28
+ placement: string;
29
+ }>;
30
+
31
+ export type OnSideAccessoryPress = Readonly<{}>;
32
+
25
33
  export type BottomTabsItems = ReadonlyArray<{
26
34
  key: string;
27
35
  title: string;
@@ -65,6 +73,11 @@ export interface BottomTabsNativeProps extends ViewProps {
65
73
  ignoreBottomInsets?: boolean;
66
74
  androidTabBarStyle?: WithDefault<'default' | 'floating', 'default'>;
67
75
  ignoreKeyboardInsets?: WithDefault<boolean, true>;
76
+ onTabBarPlacementChange?: DirectEventHandler<OnTabBarPlacementChange>;
77
+ // A round button drawn above a vertical tab bar; iOS only.
78
+ sideAccessoryIcon?: ImageSource;
79
+ sideAccessoryLabel?: string;
80
+ onSideAccessoryPress?: DirectEventHandler<OnSideAccessoryPress>;
68
81
  }
69
82
 
70
83
  export default codegenNativeComponent<BottomTabsNativeProps>('RNBottomTabsView');
package/src/index.tsx CHANGED
@@ -15,4 +15,4 @@ export { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
15
15
  /**
16
16
  * Types
17
17
  */
18
- export type { AppleIcon, TabRole } from './types';
18
+ export type { AppleIcon, TabBarPlacement, TabRole } from './types';
package/src/types.ts CHANGED
@@ -7,6 +7,9 @@ export type AppleIcon = { sfSymbol: SFSymbol };
7
7
 
8
8
  export type TabRole = 'search';
9
9
 
10
+ // 'leading' and 'trailing' mean a vertical tab bar on that edge.
11
+ export type TabBarPlacement = 'bottom' | 'leading' | 'trailing';
12
+
10
13
  export type BaseRoute = {
11
14
  key: string;
12
15
  title?: string;