Skip to content
Merged
74 changes: 74 additions & 0 deletions patches/react-native-bottom-tabs@1.4.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
diff --git a/android/src/main/java/com/rcttabview/RCTTabView.kt b/android/src/main/java/com/rcttabview/RCTTabView.kt
index 3c3d4bdbe2b070f8cc177f6175429fdcdf2d2304..7e5384366e6b8930ac1b65e99b27de980098f7d6 100644
--- a/android/src/main/java/com/rcttabview/RCTTabView.kt
+++ b/android/src/main/java/com/rcttabview/RCTTabView.kt
@@ -35,6 +35,8 @@ import coil3.size.Scale
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.assets.ReactFontManager
import com.facebook.react.modules.core.ReactChoreographer
+import com.facebook.react.uimanager.PointerEvents
+import com.facebook.react.uimanager.ReactPointerEventsView
import com.facebook.react.views.text.ReactTypefaceUtils
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO
@@ -42,7 +44,11 @@ import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED
import com.google.android.material.transition.platform.MaterialFadeThrough

-class ExtendedBottomNavigationView(context: Context) : BottomNavigationView(context) {
+class ExtendedBottomNavigationView(context: Context) :
+ BottomNavigationView(context), ReactPointerEventsView {
+ override val pointerEvents: PointerEvents
+ get() = if (visibility == VISIBLE) PointerEvents.AUTO else PointerEvents.NONE
+
override fun getMaxItemCount(): Int {
return 100
}
@@ -100,25 +106,36 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
uiModeConfiguration = resources.configuration.uiMode

post {
- addOnLayoutChangeListener { _, left, top, right, bottom,
+ addOnLayoutChangeListener { _, _, _, _, _,
_, _, _, _ ->
- val newWidth = right - left
- val newHeight = bottom - top
-
// Notify about tab bar height.
onTabBarMeasuredListener?.invoke(Utils.convertPixelsToDp(context, bottomNavigation.height).toInt())

- if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) {
- val dpWidth = Utils.convertPixelsToDp(context, layoutHolder.width)
- val dpHeight = Utils.convertPixelsToDp(context, layoutHolder.height)
-
- onNativeLayoutListener?.invoke(dpWidth, dpHeight)
- lastReportedSize = Size(newWidth, newHeight)
- }
+ reportLayoutHolderSizeIfChanged()
+ }
+ // When only the tab bar visibility changes (tabBarHidden), the container keeps
+ // its bounds so the listener above never fires — only layoutHolder grows or
+ // shrinks. Observe layoutHolder itself so the new content size reaches JS.
+ layoutHolder.addOnLayoutChangeListener { _, _, _, _, _,
+ _, _, _, _ ->
+ reportLayoutHolderSizeIfChanged()
}
}
}

+ private fun reportLayoutHolderSizeIfChanged() {
+ val newWidth = layoutHolder.width
+ val newHeight = layoutHolder.height
+
+ if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) {
+ val dpWidth = Utils.convertPixelsToDp(context, newWidth)
+ val dpHeight = Utils.convertPixelsToDp(context, newHeight)
+
+ onNativeLayoutListener?.invoke(dpWidth, dpHeight)
+ lastReportedSize = Size(newWidth, newHeight)
+ }
+ }
+
private val layoutCallback = Choreographer.FrameCallback {
isLayoutEnqueued = false
refreshLayout()
11 changes: 6 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ patchedDependencies:
react-native-nitro-healthkit@1.0.0: patches/react-native-nitro-healthkit@1.0.0.patch
expo-widgets@57.0.8: patches/expo-widgets@57.0.8.patch
react-native-keyboard-controller@1.21.13: patches/react-native-keyboard-controller@1.21.13.patch
react-native-bottom-tabs@1.4.0: patches/react-native-bottom-tabs@1.4.0.patch

minimumReleaseAgeExclude:
- '@expo/cli@57.0.3'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';

const androidTabViewSource = readFileSync(
path.join(
process.cwd(),
'node_modules/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt',
),
'utf8',
);

// Guards patches/react-native-bottom-tabs@1.4.0.patch. The patch keeps the
// holder scene size synchronized and excludes the hidden Material bottom bar
// from React Native touch target traversal.
describe('patched react-native-bottom-tabs Android hidden bar behavior', () => {
it('mirrors the upstream layoutHolder reporting fix for issue 557', () => {
expect(androidTabViewSource).toContain('private fun reportLayoutHolderSizeIfChanged()');
expect(androidTabViewSource).toMatch(
/addOnLayoutChangeListener\s*\{[^}]*onTabBarMeasuredListener[^}]*reportLayoutHolderSizeIfChanged\(\)/,
);
expect(androidTabViewSource).toMatch(
/layoutHolder\.addOnLayoutChangeListener\s*\{[^}]*reportLayoutHolderSizeIfChanged\(\)/,
);
expect(androidTabViewSource).toContain('val newWidth = layoutHolder.width');
expect(androidTabViewSource).toContain('val newHeight = layoutHolder.height');
});

it('relies on Android visibility changes to request layout', () => {
const setTabBarHiddenSource = androidTabViewSource.match(
/fun setTabBarHidden\(isHidden: Boolean\) \{[\s\S]*?\n \}/,
)?.[0];

expect(setTabBarHiddenSource).toBeDefined();
expect(setTabBarHiddenSource).not.toContain('requestLayout()');
expect(setTabBarHiddenSource).toContain('bottomNavigation.visibility = GONE');
expect(setTabBarHiddenSource).toContain('bottomNavigation.visibility = VISIBLE');
});

it('removes the hidden native tab bar from React Native touch target traversal', () => {
expect(androidTabViewSource).toMatch(
/class ExtendedBottomNavigationView\(context: Context\)\s*:\s*BottomNavigationView\(context\), ReactPointerEventsView/,
);
expect(androidTabViewSource).toContain('override val pointerEvents: PointerEvents');
expect(androidTabViewSource).toContain(
'get() = if (visibility == VISIBLE) PointerEvents.AUTO else PointerEvents.NONE',
);
});
});
Loading