Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions native-views/react-native-bottom-tabs/docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -326,6 +331,12 @@ 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 `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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)])
Comment thread
huhuanming marked this conversation as resolved.
Resolved
}

guard placement != "bottom",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 7 additions & 2 deletions native-views/react-native-bottom-tabs/src/BottomTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,12 @@ const BottomTabs = <Route extends BaseRoute>({
useRef<React.ElementRef<typeof NativeBottomTabs>>(null);
const [nativeTabBarHeight, setNativeTabBarHeight] = React.useState(0);
const [customTabBarHeight, setCustomTabBarHeight] = React.useState(0);
const [tabBarPlacement, setTabBarPlacement] =
React.useState<TabBarPlacement>("bottom");
// A vertical bar measures its full column height but takes no bottom space.
const tabBarHeight = renderCustomTabBar
? customTabBarHeight
: props.tabBarHidden
: props.tabBarHidden || tabBarPlacement !== "bottom"
Comment thread
huhuanming marked this conversation as resolved.
Resolved
? 0
: nativeTabBarHeight;
const [measuredDimensions, setMeasuredDimensions] = React.useState<
Expand Down Expand Up @@ -566,7 +569,9 @@ const BottomTabs = <Route extends BaseRoute>({

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]
);
Expand Down
Loading