@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
@@ -0,0 +1,327 @@
1
+ import UIKit
2
+
3
+ /// Internal native host. React owns two fixed slots: menu, then foreground.
4
+ @objc(RCTBottomTabsDrawerContainerView)
5
+ final class RCTBottomTabsDrawerContainerView: UIView, UIGestureRecognizerDelegate {
6
+ @objc var enabled = false {
7
+ didSet {
8
+ guard enabled != oldValue else { return }
9
+ interruptGesture()
10
+ stopAnimation()
11
+ stableOpen = false
12
+ setProgress(0)
13
+ if enabled && open { settle(to: true, velocity: 0, user: false) }
14
+ }
15
+ }
16
+ @objc var open = false {
17
+ didSet {
18
+ guard open != oldValue else { return }
19
+ interruptGesture()
20
+ settle(to: enabled && open, velocity: 0, user: false)
21
+ }
22
+ }
23
+ @objc var swipeEnabled = false
24
+ @objc var hapticFeedbackEnabled = false
25
+ @objc var bottomBarHeight: Double = 0
26
+ @objc var onDrawerOpenChange: ((Bool) -> Void)?
27
+
28
+ private let menu = UIView()
29
+ private let menuShade = UIView()
30
+ private let foreground = UIView()
31
+ private let foregroundContent = UIView()
32
+ private let dim = UIButton(type: .custom)
33
+ private var slots: [UIView] = []
34
+ private var pan: UIPanGestureRecognizer!
35
+ private var progress: CGFloat = 0
36
+ private var stableOpen = false
37
+ private var gestureStartProgress: CGFloat = 0
38
+ private var gestureReturnOpen = false
39
+ private weak var touchTarget: UIView?
40
+ private var suppressCancellation = false
41
+ private var displayLink: CADisplayLink?
42
+ private var animationStart: CFTimeInterval = 0
43
+ private var animationFrom: CGFloat = 0
44
+ private var animationVelocity: CGFloat = 0
45
+ private var animationTarget = false
46
+ private var animationUser = false
47
+ private var previousSize = CGSize.zero
48
+
49
+ override init(frame: CGRect) {
50
+ super.init(frame: frame)
51
+ clipsToBounds = true
52
+ addSubview(menu)
53
+ menuShade.backgroundColor = .black
54
+ menuShade.isUserInteractionEnabled = false
55
+ menu.addSubview(menuShade)
56
+ addSubview(foreground)
57
+ foreground.addSubview(foregroundContent)
58
+ dim.backgroundColor = UIColor(white: 28.0 / 255.0, alpha: 1)
59
+ dim.accessibilityLabel = NSLocalizedString("Close menu", comment: "Close the tab drawer")
60
+ dim.addTarget(self, action: #selector(closeFromUser), for: .touchUpInside)
61
+ foreground.addSubview(dim)
62
+ foreground.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
63
+ foreground.layer.cornerCurve = .circular
64
+ foreground.clipsToBounds = true
65
+ pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
66
+ pan.maximumNumberOfTouches = 1
67
+ pan.delegate = self
68
+ addGestureRecognizer(pan)
69
+ setProgress(0)
70
+ }
71
+
72
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
73
+
74
+ @objc func insertChild(_ child: UIView, atIndex index: Int) {
75
+ guard index >= 0 && index <= slots.count && slots.count < 2 else { return }
76
+ slots.insert(child, at: index)
77
+ updateSlots()
78
+ }
79
+
80
+ @objc func removeChild(atIndex index: Int) {
81
+ guard slots.indices.contains(index) else { return }
82
+ slots.remove(at: index).removeFromSuperview()
83
+ updateSlots()
84
+ }
85
+
86
+ private func updateSlots() {
87
+ for (index, child) in slots.enumerated() {
88
+ (index == 0 ? menu : foregroundContent).insertSubview(child, at: 0)
89
+ }
90
+ menu.bringSubviewToFront(menuShade)
91
+ setNeedsLayout()
92
+ }
93
+
94
+ override func layoutSubviews() {
95
+ super.layoutSubviews()
96
+ if previousSize != bounds.size {
97
+ let dragging = pan.state == .began || pan.state == .changed
98
+ let target = displayLink != nil ? animationTarget : (dragging ? gestureReturnOpen : stableOpen)
99
+ let user = displayLink != nil && animationUser && previousSize != .zero
100
+ interruptGesture()
101
+ stopAnimation()
102
+ finish(target: enabled && (previousSize == .zero ? open : target), user: user)
103
+ previousSize = bounds.size
104
+ }
105
+ menu.transform = .identity
106
+ foreground.transform = .identity
107
+ menu.frame = CGRect(x: 0, y: 0, width: bounds.width * 0.78, height: bounds.height)
108
+ foreground.frame = bounds
109
+ foregroundContent.frame = foreground.bounds
110
+ dim.frame = foreground.bounds
111
+ menuShade.frame = menu.bounds
112
+ if slots.count > 0 { slots[0].frame = menu.bounds }
113
+ if slots.count > 1 { slots[1].frame = foreground.bounds }
114
+ setProgress(progress)
115
+ }
116
+
117
+ override func didMoveToWindow() {
118
+ super.didMoveToWindow()
119
+ if window == nil {
120
+ interruptGesture()
121
+ stopAnimation()
122
+ setProgress(stableOpen && enabled ? 1 : 0)
123
+ } else if enabled && stableOpen != open {
124
+ settle(to: open, velocity: 0, user: false)
125
+ }
126
+ }
127
+
128
+ override func accessibilityPerformEscape() -> Bool {
129
+ guard enabled && progress > 0 else { return false }
130
+ closeFromUser()
131
+ return true
132
+ }
133
+
134
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
135
+ guard enabled else { return false }
136
+ let start = touch.location(in: self)
137
+ guard start.y < bounds.height - CGFloat(max(0, bottomBarHeight)) else { return false }
138
+ touchTarget = touch.view
139
+ if progress > 0 { return start.x >= bounds.width * 0.78 * progress }
140
+ return swipeEnabled && !touchPathContainsExclusiveControl()
141
+ }
142
+
143
+ override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
144
+ guard enabled, bounds.width > 0 else { return false }
145
+ let velocity = pan.velocity(in: self)
146
+ guard abs(velocity.x) > abs(velocity.y) else { return false }
147
+ let location = pan.location(in: self)
148
+ let translation = pan.translation(in: self)
149
+ let start = CGPoint(x: location.x - translation.x, y: location.y - translation.y)
150
+ guard start.y < bounds.height - CGFloat(max(0, bottomBarHeight)) else { return false }
151
+ if progress > 0 {
152
+ return start.x >= bounds.width * 0.78 * progress
153
+ }
154
+ return swipeEnabled && velocity.x > 0 && !touchPathCanScrollRight()
155
+ }
156
+
157
+ // Let this recognizer make its one-time direction/boundary decision before
158
+ // descendant scroll pans. Returning false from shouldBegin releases them.
159
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
160
+ shouldBeRequiredToFailBy other: UIGestureRecognizer) -> Bool {
161
+ guard gestureRecognizer === pan,
162
+ let scroll = other.view as? UIScrollView,
163
+ other === scroll.panGestureRecognizer else { return false }
164
+ return scroll.isDescendant(of: foregroundContent)
165
+ }
166
+
167
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
168
+ shouldRequireFailureOf other: UIGestureRecognizer) -> Bool {
169
+ guard gestureRecognizer === pan else { return false }
170
+ // UIKit's navigation edge gesture retains priority over the drawer.
171
+ return other is UIScreenEdgePanGestureRecognizer
172
+ }
173
+
174
+ private func touchPathContainsExclusiveControl() -> Bool {
175
+ var view = touchTarget
176
+ while let current = view, current !== self {
177
+ if current is UISlider || current is UISwitch || current is UISegmentedControl ||
178
+ current is UITextField || current is UITextView { return true }
179
+ if let webClass = NSClassFromString("WKWebView"), current.isKind(of: webClass) { return true }
180
+ if current.layer is CAMetalLayer { return true }
181
+ view = current.superview
182
+ }
183
+ return false
184
+ }
185
+
186
+ private func pageController(for scroll: UIScrollView) -> UIPageViewController? {
187
+ var responder: UIResponder? = scroll.next
188
+ while let current = responder, current !== self {
189
+ if let page = current as? UIPageViewController, scroll.superview === page.view { return page }
190
+ responder = current.next
191
+ }
192
+ return nil
193
+ }
194
+
195
+ private func touchPathCanScrollRight() -> Bool {
196
+ var view = touchTarget
197
+ while let current = view, current !== self {
198
+ if let scroll = current as? UIScrollView, scroll.isScrollEnabled {
199
+ // The native smooth pager exposes this existing header exclusion. Read
200
+ // its getter instead of invoking UIKit's recognition lifecycle hook.
201
+ let selector = NSSelectorFromString("excludedHeaderView")
202
+ if scroll.responds(to: selector),
203
+ let header = scroll.perform(selector)?.takeUnretainedValue() as? UIView,
204
+ let target = touchTarget, target.isDescendant(of: header) {
205
+ view = current.superview
206
+ continue
207
+ }
208
+ if let page = pageController(for: scroll) {
209
+ // UIPageViewController keeps its private scroll view centered even at
210
+ // the first page. Its public before query describes physical right.
211
+ if page.navigationOrientation == .horizontal,
212
+ let displayed = page.viewControllers?.first,
213
+ page.dataSource?.pageViewController(page, viewControllerBefore: displayed) != nil { return true }
214
+ } else {
215
+ let inset = scroll.adjustedContentInset
216
+ let horizontal = scroll.contentSize.width + inset.left + inset.right > scroll.bounds.width + 0.5
217
+ if horizontal && scroll.contentOffset.x > -inset.left + 0.5 { return true }
218
+ }
219
+ }
220
+ view = current.superview
221
+ }
222
+ return false
223
+ }
224
+
225
+ @objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
226
+ switch recognizer.state {
227
+ case .began:
228
+ gestureReturnOpen = displayLink != nil && !animationUser ? animationTarget : stableOpen
229
+ stopAnimation()
230
+ gestureStartProgress = progress
231
+ case .changed:
232
+ guard recognizer.numberOfTouches <= 1 else {
233
+ interruptGesture()
234
+ settle(to: gestureReturnOpen, velocity: 0, user: false)
235
+ return
236
+ }
237
+ setProgress(gestureStartProgress + recognizer.translation(in: self).x / max(1, bounds.width * 0.78))
238
+ case .ended:
239
+ let velocity = min(8, max(-8, recognizer.velocity(in: self).x / max(1, bounds.width * 0.78)))
240
+ settle(to: progress + velocity * 0.12 >= 0.5, velocity: velocity, user: true)
241
+ case .cancelled, .failed:
242
+ if !suppressCancellation { settle(to: gestureReturnOpen, velocity: 0, user: false) }
243
+ default: break
244
+ }
245
+ }
246
+
247
+ private func interruptGesture() {
248
+ guard pan != nil else { return }
249
+ suppressCancellation = true
250
+ pan.isEnabled = false
251
+ pan.isEnabled = true
252
+ touchTarget = nil
253
+ suppressCancellation = false
254
+ }
255
+
256
+ @objc private func closeFromUser() {
257
+ interruptGesture()
258
+ settle(to: false, velocity: 0, user: true)
259
+ }
260
+
261
+ private func setProgress(_ value: CGFloat) {
262
+ progress = min(1, max(0, enabled ? value : 0))
263
+ foreground.transform = CGAffineTransform(translationX: bounds.width * 0.78 * progress, y: 0)
264
+ foreground.layer.cornerRadius = 64 * progress
265
+ dim.alpha = 0.7 * progress
266
+ dim.isHidden = progress == 0
267
+ foregroundContent.isUserInteractionEnabled = progress == 0
268
+ foregroundContent.accessibilityElementsHidden = progress > 0
269
+ menu.transform = CGAffineTransform(translationX: -0.12 * bounds.width * 0.78 * (1 - progress), y: 0)
270
+ menuShade.alpha = 0.12 * (1 - progress)
271
+ menu.isHidden = progress == 0
272
+ menu.accessibilityElementsHidden = progress == 0
273
+ }
274
+
275
+ private func settle(to target: Bool, velocity: CGFloat, user: Bool) {
276
+ stopAnimation()
277
+ let target = enabled && target
278
+ guard bounds.width > 0, window != nil else {
279
+ stableOpen = target
280
+ setProgress(target ? 1 : 0)
281
+ return
282
+ }
283
+ if progress == (target ? 1 : 0) {
284
+ finish(target: target, user: user)
285
+ return
286
+ }
287
+ animationFrom = progress
288
+ animationVelocity = min(8, max(-8, velocity))
289
+ animationTarget = target
290
+ animationUser = user
291
+ animationStart = CACurrentMediaTime()
292
+ let link = CADisplayLink(target: self, selector: #selector(tick))
293
+ link.add(to: .main, forMode: .common)
294
+ displayLink = link
295
+ }
296
+
297
+ @objc private func tick() {
298
+ let elapsed = CGFloat(CACurrentMediaTime() - animationStart)
299
+ let target: CGFloat = animationTarget ? 1 : 0
300
+ let a = animationFrom - target
301
+ let b = animationVelocity + 22 * a
302
+ setProgress(target + (a + b * elapsed) * exp(-22 * elapsed))
303
+ if elapsed >= 0.5 {
304
+ let target = animationTarget
305
+ let user = animationUser
306
+ stopAnimation()
307
+ finish(target: target, user: user)
308
+ }
309
+ }
310
+
311
+ private func finish(target: Bool, user: Bool) {
312
+ let changed = stableOpen != target
313
+ stableOpen = target
314
+ setProgress(target ? 1 : 0)
315
+ if user && (changed || target != open) {
316
+ if hapticFeedbackEnabled && changed { UIImpactFeedbackGenerator(style: .light).impactOccurred() }
317
+ onDrawerOpenChange?(target)
318
+ }
319
+ }
320
+
321
+ private func stopAnimation() {
322
+ displayLink?.invalidate()
323
+ displayLink = nil
324
+ }
325
+
326
+ deinit { displayLink?.invalidate() }
327
+ }
@@ -1,14 +1,15 @@
1
1
  "use strict";
2
2
 
3
- import React, { useLayoutEffect, useRef } from 'react';
4
- import { Image, Platform, StyleSheet, View, processColor } from 'react-native';
3
+ import React, { useLayoutEffect, useRef } from "react";
4
+ import { Image, Platform, StyleSheet, View, processColor } from "react-native";
5
5
  import { BottomTabBarHeightContext } from "./utils/BottomTabBarHeightContext.js";
6
6
 
7
7
  // eslint-disable-next-line @react-native/no-deep-imports
8
8
 
9
- import NativeBottomTabs from './BottomTabsNativeComponent';
10
- import useLatestCallback from 'use-latest-callback';
9
+ import NativeBottomTabs from "./BottomTabsNativeComponent";
10
+ import useLatestCallback from "use-latest-callback";
11
11
  import DelayedFreeze from "./DelayedFreeze.js";
12
+ import NativeBottomTabsDrawer from "./BottomTabsDrawerNativeComponent";
12
13
  import { BottomAccessoryView } from "./BottomAccessoryView.js";
13
14
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
15
  const isAppleSymbol = icon => icon?.sfSymbol;
@@ -19,6 +20,10 @@ const BottomTabs = ({
19
20
  renderLazyPlaceholder,
20
21
  onIndexChange,
21
22
  onTabDoublePress,
23
+ renderDrawerContent,
24
+ drawerOpen = false,
25
+ drawerSwipeEnabled = false,
26
+ onDrawerOpenChange,
22
27
  rippleColor,
23
28
  tabBarActiveTintColor: activeTintColor,
24
29
  tabBarInactiveTintColor: inactiveTintColor,
@@ -60,7 +65,7 @@ const BottomTabs = ({
60
65
  route
61
66
  }) => route.preventsDefault,
62
67
  hapticFeedbackEnabled = false,
63
- labeled = Platform.OS !== 'android' ? true : undefined,
68
+ labeled = Platform.OS !== "android" ? true : undefined,
64
69
  getFreezeOnBlur = ({
65
70
  route
66
71
  }) => route.freezeOnBlur,
@@ -69,21 +74,27 @@ const BottomTabs = ({
69
74
  tabLabelStyle,
70
75
  renderBottomAccessoryView,
71
76
  activeIndicatorColor,
72
- androidBottomBlurEnabled = Platform.OS === 'android' && Number(Platform.Version) >= 36,
77
+ androidBottomBlurEnabled = Platform.OS === "android" && Number(Platform.Version) >= 36,
73
78
  delayedFreeze = false,
79
+ onTabBarPlacementChange,
80
+ sideAccessoryIcon,
81
+ sideAccessoryLabel,
82
+ onSideAccessoryPress,
74
83
  ...props
75
84
  }) => {
76
85
  // @ts-ignore
77
86
  const focusedKey = navigationState.routes[navigationState.index].key;
78
87
  const customTabBarWrapperRef = useRef(null);
79
88
  const nativeBottomTabsRef = useRef(null);
80
- const [tabBarHeight, setTabBarHeight] = React.useState(0);
89
+ const [nativeTabBarHeight, setNativeTabBarHeight] = React.useState(0);
90
+ const [customTabBarHeight, setCustomTabBarHeight] = React.useState(0);
91
+ const tabBarHeight = renderCustomTabBar ? customTabBarHeight : props.tabBarHidden ? 0 : nativeTabBarHeight;
81
92
  const [measuredDimensions, setMeasuredDimensions] = React.useState({
82
- width: '100%',
83
- height: '100%'
93
+ width: "100%",
94
+ height: "100%"
84
95
  });
85
96
  const trimmedRoutes = React.useMemo(() => {
86
- if (Platform.OS === 'android' && navigationState.routes.length > ANDROID_MAX_TABS) {
97
+ if (Platform.OS === "android" && navigationState.routes.length > ANDROID_MAX_TABS) {
87
98
  console.warn(`BottomTabs only supports up to ${ANDROID_MAX_TABS} tabs on Android`);
88
99
  return navigationState.routes.slice(0, ANDROID_MAX_TABS);
89
100
  }
@@ -93,7 +104,7 @@ const BottomTabs = ({
93
104
  if (!loaded.includes(focusedKey)) {
94
105
  setLoaded(loaded => [...loaded, focusedKey]);
95
106
  }
96
- const isIOS = Platform.OS === 'ios';
107
+ const isIOS = Platform.OS === "ios";
97
108
 
98
109
  // Unfocused (outline) icons — independent of focusedKey so the array stays
99
110
  // stable across tab switches and the native side never reloads on switch.
@@ -120,8 +131,8 @@ const BottomTabs = ({
120
131
  const items = React.useMemo(() => trimmedRoutes.map((route, index) => {
121
132
  const icon = icons[index];
122
133
  const isSfSymbol = isAppleSymbol(icon);
123
- if (Platform.OS === 'android' && isSfSymbol) {
124
- console.warn('SF Symbols are not supported on Android. Use require() or pass uri to load an image instead.');
134
+ if (Platform.OS === "android" && isSfSymbol) {
135
+ console.warn("SF Symbols are not supported on Android. Use require() or pass uri to load an image instead.");
125
136
  }
126
137
  const color = processColor(getActiveTintColor({
127
138
  route
@@ -141,9 +152,9 @@ const BottomTabs = ({
141
152
  badge: getBadge?.({
142
153
  route
143
154
  }),
144
- badgeBackgroundColor: typeof badgeBgColor === 'number' ? badgeBgColor : undefined,
145
- badgeTextColor: typeof badgeTxtColor === 'number' ? badgeTxtColor : undefined,
146
- activeTintColor: typeof color === 'number' ? color : undefined,
155
+ badgeBackgroundColor: typeof badgeBgColor === "number" ? badgeBgColor : undefined,
156
+ badgeTextColor: typeof badgeTxtColor === "number" ? badgeTxtColor : undefined,
157
+ activeTintColor: typeof color === "number" ? color : undefined,
147
158
  hidden: getHidden?.({
148
159
  route
149
160
  }),
@@ -163,14 +174,14 @@ const BottomTabs = ({
163
174
  // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
164
175
  const resolved = Image.resolveAssetSource(icon);
165
176
  return {
166
- uri: resolved?.uri ?? '',
177
+ uri: resolved?.uri ?? "",
167
178
  width: resolved?.width ?? 0,
168
179
  height: resolved?.height ?? 0,
169
180
  scale: resolved?.scale ?? 1
170
181
  };
171
182
  }
172
183
  return {
173
- uri: '',
184
+ uri: "",
174
185
  width: 0,
175
186
  height: 0,
176
187
  scale: 1
@@ -184,14 +195,14 @@ const BottomTabs = ({
184
195
  // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
185
196
  const resolved = Image.resolveAssetSource(icon);
186
197
  return {
187
- uri: resolved?.uri ?? '',
198
+ uri: resolved?.uri ?? "",
188
199
  width: resolved?.width ?? 0,
189
200
  height: resolved?.height ?? 0,
190
201
  scale: resolved?.scale ?? 1
191
202
  };
192
203
  }
193
204
  return {
194
- uri: '',
205
+ uri: "",
195
206
  width: 0,
196
207
  height: 0,
197
208
  scale: 1
@@ -225,8 +236,8 @@ const BottomTabs = ({
225
236
  }
226
237
  }, [focusedKey, jumpTo]);
227
238
  const handleTabBarMeasured = React.useCallback(event => {
228
- setTabBarHeight(event.nativeEvent.height);
229
- }, [setTabBarHeight]);
239
+ setNativeTabBarHeight(event.nativeEvent.height);
240
+ }, [setNativeTabBarHeight]);
230
241
  const handleNativeLayout = React.useCallback(event => {
231
242
  const {
232
243
  width,
@@ -237,88 +248,134 @@ const BottomTabs = ({
237
248
  height
238
249
  });
239
250
  }, [setMeasuredDimensions]);
251
+ const handleTabBarPlacementChange = React.useCallback(event => {
252
+ onTabBarPlacementChange?.(event.nativeEvent.placement);
253
+ }, [onTabBarPlacementChange]);
254
+ const handleSideAccessoryPress = React.useCallback(() => {
255
+ onSideAccessoryPress?.();
256
+ }, [onSideAccessoryPress]);
257
+ const resolvedSideAccessoryIcon = React.useMemo(() => {
258
+ if (!isIOS || !sideAccessoryIcon) {
259
+ return undefined;
260
+ }
261
+ // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
262
+ const resolved = Image.resolveAssetSource(sideAccessoryIcon);
263
+ return resolved?.uri ? {
264
+ uri: resolved.uri,
265
+ width: resolved.width ?? 0,
266
+ height: resolved.height ?? 0,
267
+ scale: resolved.scale ?? 1
268
+ } : undefined;
269
+ }, [sideAccessoryIcon]);
240
270
  useLayoutEffect(() => {
241
271
  if (renderCustomTabBar && customTabBarWrapperRef.current) {
242
272
  customTabBarWrapperRef.current.measure((_x, _y, _width, height) => {
243
- setTabBarHeight(height);
273
+ setCustomTabBarHeight(height);
244
274
  });
245
275
  }
246
276
  }, [renderCustomTabBar]);
247
- return /*#__PURE__*/_jsxs(BottomTabBarHeightContext.Provider, {
277
+ return /*#__PURE__*/_jsx(BottomTabBarHeightContext.Provider, {
248
278
  value: tabBarHeight,
249
- children: [/*#__PURE__*/_jsxs(NativeBottomTabs, {
250
- ref: nativeBottomTabsRef,
251
- ...props,
252
- ...tabLabelStyle,
279
+ children: /*#__PURE__*/_jsxs(NativeBottomTabsDrawer, {
253
280
  style: styles.fullWidth,
254
- items: items,
255
- icons: renderCustomTabBar ? undefined : resolvedIconAssets,
256
- selectedIcons: isIOS && !renderCustomTabBar ? resolvedSelectedIconAssets : undefined,
257
- selectedPage: focusedKey,
258
- tabBarHidden: props.tabBarHidden ?? !!renderCustomTabBar,
259
- onTabDoublePress: handleTabDoublePress,
260
- onPageSelected: handlePageSelected,
261
- onTabBarMeasured: handleTabBarMeasured,
262
- onNativeLayout: handleNativeLayout,
281
+ enabled: !!renderDrawerContent,
282
+ open: drawerOpen,
283
+ swipeEnabled: drawerSwipeEnabled,
263
284
  hapticFeedbackEnabled: hapticFeedbackEnabled,
264
- activeTintColor: activeTintColor,
265
- inactiveTintColor: inactiveTintColor,
266
- barTintColor: tabBarStyle?.backgroundColor,
267
- rippleColor: rippleColor,
268
- activeIndicatorColor: activeIndicatorColor,
269
- androidBottomBlurEnabled: Platform.OS === 'android' && !renderCustomTabBar && androidBottomBlurEnabled,
270
- labeled: labeled,
271
- children: [trimmedRoutes.map(route => {
272
- if (getLazy({
273
- route
274
- }) !== false && !loaded.includes(route.key)) {
275
- return /*#__PURE__*/_jsx(View, {
276
- collapsable: false,
277
- style: styles.fullWidth,
278
- children: renderLazyPlaceholder?.({
285
+ bottomBarHeight: tabBarHeight ?? 0,
286
+ onDrawerOpenChange: event => onDrawerOpenChange?.(event.nativeEvent.open),
287
+ children: [/*#__PURE__*/_jsx(View, {
288
+ collapsable: false,
289
+ style: styles.drawerMenu,
290
+ children: Platform.OS !== "web" ? renderDrawerContent?.() : null
291
+ }), /*#__PURE__*/_jsxs(View, {
292
+ collapsable: false,
293
+ style: styles.fullWidth,
294
+ children: [/*#__PURE__*/_jsxs(NativeBottomTabs, {
295
+ ref: nativeBottomTabsRef,
296
+ ...props,
297
+ ...tabLabelStyle,
298
+ style: styles.fullWidth,
299
+ items: items,
300
+ icons: renderCustomTabBar ? undefined : resolvedIconAssets,
301
+ selectedIcons: isIOS && !renderCustomTabBar ? resolvedSelectedIconAssets : undefined,
302
+ selectedPage: focusedKey,
303
+ tabBarHidden: props.tabBarHidden ?? !!renderCustomTabBar,
304
+ onTabDoublePress: handleTabDoublePress,
305
+ onPageSelected: handlePageSelected,
306
+ onTabBarMeasured: handleTabBarMeasured,
307
+ onNativeLayout: handleNativeLayout,
308
+ onTabBarPlacementChange: handleTabBarPlacementChange,
309
+ sideAccessoryIcon: renderCustomTabBar ? undefined : resolvedSideAccessoryIcon,
310
+ sideAccessoryLabel: sideAccessoryLabel,
311
+ onSideAccessoryPress: handleSideAccessoryPress,
312
+ hapticFeedbackEnabled: hapticFeedbackEnabled,
313
+ activeTintColor: activeTintColor,
314
+ inactiveTintColor: inactiveTintColor,
315
+ barTintColor: tabBarStyle?.backgroundColor,
316
+ rippleColor: rippleColor,
317
+ activeIndicatorColor: activeIndicatorColor,
318
+ androidBottomBlurEnabled: Platform.OS === "android" && !renderCustomTabBar && androidBottomBlurEnabled,
319
+ labeled: labeled,
320
+ children: [trimmedRoutes.map(route => {
321
+ if (getLazy({
322
+ route
323
+ }) !== false && !loaded.includes(route.key)) {
324
+ return /*#__PURE__*/_jsx(View, {
325
+ collapsable: false,
326
+ style: styles.fullWidth,
327
+ children: renderLazyPlaceholder?.({
328
+ route
329
+ })
330
+ }, route.key);
331
+ }
332
+ const focused = route.key === focusedKey;
333
+ const freeze = !focused ? getFreezeOnBlur({
279
334
  route
280
- })
281
- }, route.key);
282
- }
283
- const focused = route.key === focusedKey;
284
- const freeze = !focused ? getFreezeOnBlur({
285
- route
286
- }) : false;
287
- const customStyle = getSceneStyle({
288
- route
289
- });
290
- return /*#__PURE__*/_jsx(View, {
291
- style: [styles.screen, renderCustomTabBar ? styles.fullWidth : measuredDimensions, customStyle],
292
- collapsable: false,
293
- pointerEvents: focused ? 'auto' : 'none',
294
- accessibilityElementsHidden: !focused,
295
- importantForAccessibility: focused ? 'auto' : 'no-hide-descendants',
296
- children: /*#__PURE__*/_jsx(DelayedFreeze, {
297
- freeze: !!freeze,
298
- delayedFreeze: delayedFreeze,
299
- children: renderScene({
300
- route,
301
- jumpTo
302
- })
303
- })
304
- }, route.key);
305
- }), Platform.OS === 'ios' && parseFloat(Platform.Version) >= 26 && renderBottomAccessoryView && !renderCustomTabBar ? /*#__PURE__*/_jsx(BottomAccessoryView, {
306
- renderBottomAccessoryView: renderBottomAccessoryView
307
- }) : null]
308
- }), renderCustomTabBar ? /*#__PURE__*/_jsx(View, {
309
- ref: customTabBarWrapperRef,
310
- children: renderCustomTabBar()
311
- }) : null]
335
+ }) : false;
336
+ const customStyle = getSceneStyle({
337
+ route
338
+ });
339
+ return /*#__PURE__*/_jsx(View, {
340
+ style: [styles.screen, renderCustomTabBar ? styles.fullWidth : measuredDimensions, customStyle],
341
+ collapsable: false,
342
+ pointerEvents: focused ? "auto" : "none",
343
+ accessibilityElementsHidden: !focused,
344
+ importantForAccessibility: focused ? "auto" : "no-hide-descendants",
345
+ children: /*#__PURE__*/_jsx(DelayedFreeze, {
346
+ freeze: !!freeze,
347
+ delayedFreeze: delayedFreeze,
348
+ children: renderScene({
349
+ route,
350
+ jumpTo
351
+ })
352
+ })
353
+ }, route.key);
354
+ }), Platform.OS === "ios" && parseFloat(Platform.Version) >= 26 && renderBottomAccessoryView && !renderCustomTabBar ? /*#__PURE__*/_jsx(BottomAccessoryView, {
355
+ renderBottomAccessoryView: renderBottomAccessoryView
356
+ }) : null]
357
+ }), renderCustomTabBar ? /*#__PURE__*/_jsx(View, {
358
+ ref: customTabBarWrapperRef,
359
+ onLayout: event => setCustomTabBarHeight(event.nativeEvent.layout.height),
360
+ children: renderCustomTabBar()
361
+ }) : null]
362
+ })]
363
+ })
312
364
  });
313
365
  };
314
366
  const styles = StyleSheet.create({
315
367
  fullWidth: {
316
- width: '100%',
317
- height: '100%',
368
+ width: "100%",
369
+ height: "100%",
318
370
  flex: 1
319
371
  },
372
+ drawerMenu: {
373
+ position: "absolute",
374
+ width: "78%",
375
+ height: "100%"
376
+ },
320
377
  screen: {
321
- position: 'absolute'
378
+ position: "absolute"
322
379
  }
323
380
  });
324
381
  export default BottomTabs;