From fa0d948c6556c8b7d0192a8c4b08e71d38e866e9 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 4 Nov 2026 16:01:07 +0800 Subject: [PATCH 1/3] fix(bottom-tabs): report tab bar height on placement change The height was only reported on creation and visibility changes, so a bar moving from a vertical strip to the bottom kept its column height. Co-Authored-By: Claude Opus 5.5 --- native-views/react-native-bottom-tabs/docs/SPEC.md | 4 ++++ .../ios/RCTBottomTabsContainerView.swift | 3 +++ 2 files changed, 7 insertions(+) diff --git a/native-views/react-native-bottom-tabs/docs/SPEC.md b/native-views/react-native-bottom-tabs/docs/SPEC.md index 2e8546741..7634dd85b 100644 --- a/native-views/react-native-bottom-tabs/docs/SPEC.md +++ b/native-views/react-native-bottom-tabs/docs/SPEC.md @@ -326,6 +326,10 @@ placement and can draw one round accessory button above the vertical bar. tab bar controller. It fires once after the bar is created and again on every change (rotation, folding, window resizing). Builds without the iOS 27.1 SDK and earlier systems always report `bottom`. Android never fires it. +- **Height on placement change:** every placement event is followed by + `onTabBarMeasured` with the bar's current height, because the height reported + for the previous placement no longer applies. A vertical bar reports its full + column height, so consumers MUST NOT treat that value as bottom space. - **Side accessory:** `sideAccessoryIcon` (an image source, loaded with the React image loader and drawn as a template tinted with `inactiveTintColor`), `sideAccessoryLabel` (accessibility label), and `onSideAccessoryPress`. The diff --git a/native-views/react-native-bottom-tabs/ios/RCTBottomTabsContainerView.swift b/native-views/react-native-bottom-tabs/ios/RCTBottomTabsContainerView.swift index f9ab0b85f..713390f5a 100644 --- a/native-views/react-native-bottom-tabs/ios/RCTBottomTabsContainerView.swift +++ b/native-views/react-native-bottom-tabs/ios/RCTBottomTabsContainerView.swift @@ -344,6 +344,9 @@ class RCTBottomTabsContainerView: UIView { if placement != reportedTabBarPlacement { reportedTabBarPlacement = placement onTabBarPlacementChange?(["placement": placement]) + // The bar's frame follows its placement, so the height reported for the + // previous placement is stale. + onTabBarMeasured?(["height": tabBarHidden ? 0 : Double(tbc.tabBar.frame.size.height)]) } guard placement != "bottom", From d723e0d4c27c25e0b1f2e1375d96e8e2b18fdcb2 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 5 Nov 2026 08:51:06 +0800 Subject: [PATCH 2/3] fix(bottom-tabs): expose zero bar height for vertical placement A vertical bar measures its full column height, which the drawer and height hooks consumed as bottom space, rejecting most drawer swipes. Co-Authored-By: Claude Opus 5.5 --- native-views/react-native-bottom-tabs/docs/SPEC.md | 4 +++- native-views/react-native-bottom-tabs/src/BottomTabs.tsx | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/native-views/react-native-bottom-tabs/docs/SPEC.md b/native-views/react-native-bottom-tabs/docs/SPEC.md index 7634dd85b..8559f0973 100644 --- a/native-views/react-native-bottom-tabs/docs/SPEC.md +++ b/native-views/react-native-bottom-tabs/docs/SPEC.md @@ -329,7 +329,9 @@ placement and can draw one round accessory button above the vertical bar. - **Height on placement change:** every placement event is followed by `onTabBarMeasured` with the bar's current height, because the height reported for the previous placement no longer applies. A vertical bar reports its full - column height, so consumers MUST NOT treat that value as bottom space. + column height, so `BottomTabs` MUST expose 0 through + `BottomTabBarHeightContext` and the drawer's `bottomBarHeight` for `leading` + and `trailing` placement, and the measured height only for `bottom`. - **Side accessory:** `sideAccessoryIcon` (an image source, loaded with the React image loader and drawn as a template tinted with `inactiveTintColor`), `sideAccessoryLabel` (accessibility label), and `onSideAccessoryPress`. The diff --git a/native-views/react-native-bottom-tabs/src/BottomTabs.tsx b/native-views/react-native-bottom-tabs/src/BottomTabs.tsx index 90c4f99ac..24f931349 100644 --- a/native-views/react-native-bottom-tabs/src/BottomTabs.tsx +++ b/native-views/react-native-bottom-tabs/src/BottomTabs.tsx @@ -399,9 +399,12 @@ const BottomTabs = ({ useRef>(null); const [nativeTabBarHeight, setNativeTabBarHeight] = React.useState(0); const [customTabBarHeight, setCustomTabBarHeight] = React.useState(0); + const [tabBarPlacement, setTabBarPlacement] = + React.useState("bottom"); + // A vertical bar measures its full column height but takes no bottom space. const tabBarHeight = renderCustomTabBar ? customTabBarHeight - : props.tabBarHidden + : props.tabBarHidden || tabBarPlacement !== "bottom" ? 0 : nativeTabBarHeight; const [measuredDimensions, setMeasuredDimensions] = React.useState< @@ -566,7 +569,9 @@ const BottomTabs = ({ const handleTabBarPlacementChange = React.useCallback( (event: NativeSyntheticEvent<{ placement: string }>) => { - onTabBarPlacementChange?.(event.nativeEvent.placement as TabBarPlacement); + const placement = event.nativeEvent.placement as TabBarPlacement; + setTabBarPlacement(placement); + onTabBarPlacementChange?.(placement); }, [onTabBarPlacementChange] ); From a7563fc5ae98b2a8ff628327defef4bc5c5ee848 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 5 Nov 2026 14:01:53 +0800 Subject: [PATCH 3/3] fix(bottom-tabs): keep native tab bar touches out of the drawer With a vertical bar reporting no bottom height, a rightward drag on the bar could open the drawer and interrupt the system tab gesture. Co-Authored-By: Claude Opus 5.5 --- .../react-native-bottom-tabs/docs/SPEC.md | 5 +++++ .../RCTBottomTabsDrawerContainerView.swift | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/native-views/react-native-bottom-tabs/docs/SPEC.md b/native-views/react-native-bottom-tabs/docs/SPEC.md index 8559f0973..988a6a6f7 100644 --- a/native-views/react-native-bottom-tabs/docs/SPEC.md +++ b/native-views/react-native-bottom-tabs/docs/SPEC.md @@ -104,6 +104,11 @@ height: a rendered custom bar's own measured height takes precedence; otherwise `tabBarHidden` forces zero. Native hide measurements MUST NOT overwrite a custom bar's height, including across native/custom presentation switches. +The iOS drawer MUST NOT receive touches inside the native tab bar at any +placement, because a vertical bar reports no bottom height. That covers +everything under the tab controller's view outside tab content, including the +floating item platter and side accessory. + With opening swipes enabled, a one-finger rightward gesture may start anywhere in the foreground content. At the first confirmed direction, the actual native touch target and its ancestor chain determine ownership. A horizontal scroll diff --git a/native-views/react-native-bottom-tabs/ios/RCTBottomTabsDrawerContainerView.swift b/native-views/react-native-bottom-tabs/ios/RCTBottomTabsDrawerContainerView.swift index 7ed8c94cf..57584d8ae 100644 --- a/native-views/react-native-bottom-tabs/ios/RCTBottomTabsDrawerContainerView.swift +++ b/native-views/react-native-bottom-tabs/ios/RCTBottomTabsDrawerContainerView.swift @@ -135,6 +135,7 @@ final class RCTBottomTabsDrawerContainerView: UIView, UIGestureRecognizerDelegat guard enabled else { return false } let start = touch.location(in: self) guard start.y < bounds.height - CGFloat(max(0, bottomBarHeight)) else { return false } + guard !isInsideNativeTabBar(touch.view) else { return false } touchTarget = touch.view if progress > 0 { return start.x >= bounds.width * 0.78 * progress } return swipeEnabled && !touchPathContainsExclusiveControl() @@ -171,6 +172,24 @@ final class RCTBottomTabsDrawerContainerView: UIView, UIGestureRecognizerDelegat return other is UIScreenEdgePanGestureRecognizer } + // A vertical bar reports no bottom height, and UIKit draws its items in a + // floating platter outside UITabBar, so match anything under the tab + // controller's view that is not tab content. + private func isInsideNativeTabBar(_ target: UIView?) -> Bool { + var path: [UIView] = [] + var view = target + while let current = view, current !== self { + if current is UITabBar { return true } + if let tabs = current.next as? UITabBarController { + let content = (tabs.viewControllers ?? []).compactMap { $0.viewIfLoaded } + return !path.contains { step in content.contains { $0 === step } } + } + path.append(current) + view = current.superview + } + return false + } + private func touchPathContainsExclusiveControl() -> Bool { var view = touchTarget while let current = view, current !== self {