diff --git a/FabricExample/src/constants/screenNames.ts b/FabricExample/src/constants/screenNames.ts index 1e885a4d6..90ed9f947 100644 --- a/FabricExample/src/constants/screenNames.ts +++ b/FabricExample/src/constants/screenNames.ts @@ -20,4 +20,5 @@ export enum ScreenNames { FOCUSED_INPUT_HANDLERS = "FOCUSED_INPUT_HANDLERS", TOOLBAR = "TOOLBAR", MODAL = "MODAL", + BOTTOM_TAB_BAR = "BOTTOM_TAB_BAR", } diff --git a/FabricExample/src/navigation/BottomTabBar/index.tsx b/FabricExample/src/navigation/BottomTabBar/index.tsx new file mode 100644 index 000000000..618dbba22 --- /dev/null +++ b/FabricExample/src/navigation/BottomTabBar/index.tsx @@ -0,0 +1,69 @@ +import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import * as React from "react"; +import { StyleSheet, Text, View } from "react-native"; + +import KeyboardAnimation from "../../screens/Examples/KeyboardAnimation"; + +const Tab = createBottomTabNavigator(); +const HomeStack = createNativeStackNavigator(); +const SettingsStack = createNativeStackNavigator(); + +const HomeStackScreens = () => { + return ( + + + + ); +}; + +const SettingsStackScreens = () => { + return ( + + + + ); +}; + +export default function BottomTabBar() { + return ( + ({ + tabBarBackground: () => , + tabBarIcon: () => + route.name === "HomeStack" ? ( + 🏠 + ) : ( + ⚙️ + ), + tabBarLabel: () => + route.name === "HomeStack" ? ( + Home + ) : ( + Settings + ), + headerShown: false, + })} + > + + + + ); +} + +const styles = StyleSheet.create({ + icon: { + color: "white", + fontSize: 20, + }, + label: { + color: "white", + marginHorizontal: 20, + }, + tabBarBackground: { + backgroundColor: "#2c2c2c", + width: "100%", + height: "100%", + }, +}); diff --git a/FabricExample/src/navigation/ExamplesStack/index.tsx b/FabricExample/src/navigation/ExamplesStack/index.tsx index 7b84e76f0..735f8450f 100644 --- a/FabricExample/src/navigation/ExamplesStack/index.tsx +++ b/FabricExample/src/navigation/ExamplesStack/index.tsx @@ -19,6 +19,7 @@ import ReanimatedChat from "../../screens/Examples/ReanimatedChat"; import ReanimatedChatFlatList from "../../screens/Examples/ReanimatedChatFlatList"; import StatusBar from "../../screens/Examples/StatusBar"; import ToolbarExample from "../../screens/Examples/Toolbar"; +import BottomTabBar from "../BottomTabBar"; import NativeStack from "../NestedStack"; export type ExamplesStackParamList = { @@ -40,6 +41,7 @@ export type ExamplesStackParamList = { [ScreenNames.FOCUSED_INPUT_HANDLERS]: undefined; [ScreenNames.TOOLBAR]: undefined; [ScreenNames.MODAL]: undefined; + [ScreenNames.BOTTOM_TAB_BAR]: undefined; }; const Stack = createStackNavigator(); @@ -100,6 +102,9 @@ const options = { [ScreenNames.MODAL]: { title: "Modal", }, + [ScreenNames.BOTTOM_TAB_BAR]: { + headerShown: false, + }, }; const ExamplesStack = () => ( @@ -194,6 +199,11 @@ const ExamplesStack = () => ( name={ScreenNames.MODAL} options={options[ScreenNames.MODAL]} /> + ); diff --git a/FabricExample/src/screens/Examples/Main/constants.ts b/FabricExample/src/screens/Examples/Main/constants.ts index f4cadff69..3c46ee966 100644 --- a/FabricExample/src/screens/Examples/Main/constants.ts +++ b/FabricExample/src/screens/Examples/Main/constants.ts @@ -111,4 +111,10 @@ export const examples: Example[] = [ info: ScreenNames.MODAL, icons: "🌎", }, + { + title: "Bottom tab bar", + testID: "bottom_tab_bar", + info: ScreenNames.BOTTOM_TAB_BAR, + icons: "🧱", + }, ]; diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt b/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt index 6e566fc88..9418cf1c8 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt @@ -1,6 +1,7 @@ package com.reactnativekeyboardcontroller.views import android.annotation.SuppressLint +import android.content.res.Configuration import android.os.Handler import android.os.Looper import android.util.Log @@ -68,6 +69,10 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R this.removeKeyboardCallbacks() } + + override fun onConfigurationChanged(newConfig: Configuration?) { + this.reApplyWindowInsets() + } // endregion // region State manager helpers @@ -83,25 +88,23 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R val shouldApplyZeroPaddingTop = !active || this.isStatusBarTranslucent val shouldApplyZeroPaddingBottom = !active || this.isNavigationBarTranslucent + val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + params.setMargins( - 0, + navBarInsets.left, if (shouldApplyZeroPaddingTop) { 0 } else { - ( - insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top - ?: 0 - ) + systemBarInsets.top }, - 0, + navBarInsets.right, if (shouldApplyZeroPaddingBottom) { 0 } else { - insets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom - ?: 0 + navBarInsets.bottom }, ) - content?.layoutParams = params val defaultInsets = ViewCompat.onApplyWindowInsets(v, insets) @@ -164,6 +167,11 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R // for more details Handler(Looper.getMainLooper()).post { view.removeSelf() } } + + private fun reApplyWindowInsets() { + this.setupWindowInsets() + this.requestApplyInsetsWhenAttached() + } // endregion // region State managers @@ -206,8 +214,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R fun forceStatusBarTranslucent(isStatusBarTranslucent: Boolean) { if (active && this.isStatusBarTranslucent != isStatusBarTranslucent) { this.isStatusBarTranslucent = isStatusBarTranslucent - this.setupWindowInsets() - this.requestApplyInsetsWhenAttached() + this.reApplyWindowInsets() } } // endregion diff --git a/e2e/kit/010-bottom-tab-bar-rotation.e2e.ts b/e2e/kit/010-bottom-tab-bar-rotation.e2e.ts new file mode 100644 index 000000000..817ccf850 --- /dev/null +++ b/e2e/kit/010-bottom-tab-bar-rotation.e2e.ts @@ -0,0 +1,48 @@ +import { expectBitmapsToBeEqual } from "./asserts"; +import { + closeKeyboard, + scrollDownUntilElementIsVisible, + waitAndTap, + waitForExpect, +} from "./helpers"; + +describe("Bottom tab bar", () => { + it("should navigate to `Bottom tab bar` screen", async () => { + await scrollDownUntilElementIsVisible("main_scroll_view", "bottom_tab_bar"); + await waitAndTap("bottom_tab_bar"); + }); + + it("should have expected state in portrait mode", async () => { + await waitForExpect(async () => { + await expectBitmapsToBeEqual("BottomTabBarPortrait", 0.25); + }); + }); + + it("should have expected state in landscape mode", async () => { + await device.setOrientation("landscape"); + await waitForExpect(async () => { + await expectBitmapsToBeEqual("BottomTabBarLandscape"); + }); + }); + + it("should have expected state in portrait mode after rotation", async () => { + await device.setOrientation("portrait"); + await waitForExpect(async () => { + await expectBitmapsToBeEqual("BottomTabBarPortraitAgain"); + }); + }); + + it("should have expected state when keyboard open", async () => { + await waitAndTap("keyboard_animation_text_input"); + await waitForExpect(async () => { + await expectBitmapsToBeEqual("BottomTabBarKeyboardIsShown"); + }); + }); + + it("should have expected state when emoji keyboard closed", async () => { + await closeKeyboard("keyboard_animation_text_input"); + await waitForExpect(async () => { + await expectBitmapsToBeEqual("BottomTabBarKeyboardIsHidden"); + }); + }); +}); diff --git a/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarKeyboardIsHidden.png b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarKeyboardIsHidden.png new file mode 100644 index 000000000..2c6b19dd7 Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarKeyboardIsHidden.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarKeyboardIsShown.png b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarKeyboardIsShown.png new file mode 100644 index 000000000..804042470 Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarKeyboardIsShown.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarLandscape.png b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarLandscape.png new file mode 100644 index 000000000..5ead14256 Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarLandscape.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarPortrait.png b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarPortrait.png new file mode 100644 index 000000000..72a540826 Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarPortrait.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarPortraitAgain.png b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarPortraitAgain.png new file mode 100644 index 000000000..72a540826 Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_28/BottomTabBarPortraitAgain.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarKeyboardIsHidden.png b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarKeyboardIsHidden.png new file mode 100644 index 000000000..0671fe31b Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarKeyboardIsHidden.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarKeyboardIsShown.png b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarKeyboardIsShown.png new file mode 100644 index 000000000..6d575b72e Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarKeyboardIsShown.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarLandscape.png b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarLandscape.png new file mode 100644 index 000000000..bb6422f51 Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarLandscape.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarPortrait.png b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarPortrait.png new file mode 100644 index 000000000..c1102b3b4 Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarPortrait.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarPortraitAgain.png b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarPortraitAgain.png new file mode 100644 index 000000000..c1102b3b4 Binary files /dev/null and b/e2e/kit/assets/android/e2e_emulator_31/BottomTabBarPortraitAgain.png differ diff --git a/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarKeyboardIsHidden.png b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarKeyboardIsHidden.png new file mode 100644 index 000000000..56791e669 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarKeyboardIsHidden.png differ diff --git a/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarKeyboardIsShown.png b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarKeyboardIsShown.png new file mode 100644 index 000000000..974c65159 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarKeyboardIsShown.png differ diff --git a/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarLandscape.png b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarLandscape.png new file mode 100644 index 000000000..d02ca64a9 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarLandscape.png differ diff --git a/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarPortrait.png b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarPortrait.png new file mode 100644 index 000000000..0dfd0fab5 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarPortrait.png differ diff --git a/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarPortraitAgain.png b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarPortraitAgain.png new file mode 100644 index 000000000..45cd44f62 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14 Pro/BottomTabBarPortraitAgain.png differ diff --git a/e2e/kit/assets/ios/iPhone 14/BottomTabBarKeyboardIsHidden.png b/e2e/kit/assets/ios/iPhone 14/BottomTabBarKeyboardIsHidden.png new file mode 100644 index 000000000..c12d46db6 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14/BottomTabBarKeyboardIsHidden.png differ diff --git a/e2e/kit/assets/ios/iPhone 14/BottomTabBarKeyboardIsShown.png b/e2e/kit/assets/ios/iPhone 14/BottomTabBarKeyboardIsShown.png new file mode 100644 index 000000000..c18a2d7d2 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14/BottomTabBarKeyboardIsShown.png differ diff --git a/e2e/kit/assets/ios/iPhone 14/BottomTabBarLandscape.png b/e2e/kit/assets/ios/iPhone 14/BottomTabBarLandscape.png new file mode 100644 index 000000000..35ce06a76 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14/BottomTabBarLandscape.png differ diff --git a/e2e/kit/assets/ios/iPhone 14/BottomTabBarPortrait.png b/e2e/kit/assets/ios/iPhone 14/BottomTabBarPortrait.png new file mode 100644 index 000000000..c12d46db6 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14/BottomTabBarPortrait.png differ diff --git a/e2e/kit/assets/ios/iPhone 14/BottomTabBarPortraitAgain.png b/e2e/kit/assets/ios/iPhone 14/BottomTabBarPortraitAgain.png new file mode 100644 index 000000000..bd91591be Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 14/BottomTabBarPortraitAgain.png differ diff --git a/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarKeyboardIsHidden.png b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarKeyboardIsHidden.png new file mode 100644 index 000000000..f5e4327f9 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarKeyboardIsHidden.png differ diff --git a/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarKeyboardIsShown.png b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarKeyboardIsShown.png new file mode 100644 index 000000000..78a82f47e Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarKeyboardIsShown.png differ diff --git a/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarLandscape.png b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarLandscape.png new file mode 100644 index 000000000..6c2a30ae7 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarLandscape.png differ diff --git a/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarPortrait.png b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarPortrait.png new file mode 100644 index 000000000..f5e4327f9 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarPortrait.png differ diff --git a/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarPortraitAgain.png b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarPortraitAgain.png new file mode 100644 index 000000000..1c45b0384 Binary files /dev/null and b/e2e/kit/assets/ios/iPhone 15 Pro/BottomTabBarPortraitAgain.png differ diff --git a/example/src/constants/screenNames.ts b/example/src/constants/screenNames.ts index 1e885a4d6..90ed9f947 100644 --- a/example/src/constants/screenNames.ts +++ b/example/src/constants/screenNames.ts @@ -20,4 +20,5 @@ export enum ScreenNames { FOCUSED_INPUT_HANDLERS = "FOCUSED_INPUT_HANDLERS", TOOLBAR = "TOOLBAR", MODAL = "MODAL", + BOTTOM_TAB_BAR = "BOTTOM_TAB_BAR", } diff --git a/example/src/navigation/BottomTabBar/index.tsx b/example/src/navigation/BottomTabBar/index.tsx new file mode 100644 index 000000000..618dbba22 --- /dev/null +++ b/example/src/navigation/BottomTabBar/index.tsx @@ -0,0 +1,69 @@ +import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import * as React from "react"; +import { StyleSheet, Text, View } from "react-native"; + +import KeyboardAnimation from "../../screens/Examples/KeyboardAnimation"; + +const Tab = createBottomTabNavigator(); +const HomeStack = createNativeStackNavigator(); +const SettingsStack = createNativeStackNavigator(); + +const HomeStackScreens = () => { + return ( + + + + ); +}; + +const SettingsStackScreens = () => { + return ( + + + + ); +}; + +export default function BottomTabBar() { + return ( + ({ + tabBarBackground: () => , + tabBarIcon: () => + route.name === "HomeStack" ? ( + 🏠 + ) : ( + ⚙️ + ), + tabBarLabel: () => + route.name === "HomeStack" ? ( + Home + ) : ( + Settings + ), + headerShown: false, + })} + > + + + + ); +} + +const styles = StyleSheet.create({ + icon: { + color: "white", + fontSize: 20, + }, + label: { + color: "white", + marginHorizontal: 20, + }, + tabBarBackground: { + backgroundColor: "#2c2c2c", + width: "100%", + height: "100%", + }, +}); diff --git a/example/src/navigation/ExamplesStack/index.tsx b/example/src/navigation/ExamplesStack/index.tsx index 7b84e76f0..735f8450f 100644 --- a/example/src/navigation/ExamplesStack/index.tsx +++ b/example/src/navigation/ExamplesStack/index.tsx @@ -19,6 +19,7 @@ import ReanimatedChat from "../../screens/Examples/ReanimatedChat"; import ReanimatedChatFlatList from "../../screens/Examples/ReanimatedChatFlatList"; import StatusBar from "../../screens/Examples/StatusBar"; import ToolbarExample from "../../screens/Examples/Toolbar"; +import BottomTabBar from "../BottomTabBar"; import NativeStack from "../NestedStack"; export type ExamplesStackParamList = { @@ -40,6 +41,7 @@ export type ExamplesStackParamList = { [ScreenNames.FOCUSED_INPUT_HANDLERS]: undefined; [ScreenNames.TOOLBAR]: undefined; [ScreenNames.MODAL]: undefined; + [ScreenNames.BOTTOM_TAB_BAR]: undefined; }; const Stack = createStackNavigator(); @@ -100,6 +102,9 @@ const options = { [ScreenNames.MODAL]: { title: "Modal", }, + [ScreenNames.BOTTOM_TAB_BAR]: { + headerShown: false, + }, }; const ExamplesStack = () => ( @@ -194,6 +199,11 @@ const ExamplesStack = () => ( name={ScreenNames.MODAL} options={options[ScreenNames.MODAL]} /> + ); diff --git a/example/src/screens/Examples/Main/constants.ts b/example/src/screens/Examples/Main/constants.ts index f4cadff69..3c46ee966 100644 --- a/example/src/screens/Examples/Main/constants.ts +++ b/example/src/screens/Examples/Main/constants.ts @@ -111,4 +111,10 @@ export const examples: Example[] = [ info: ScreenNames.MODAL, icons: "🌎", }, + { + title: "Bottom tab bar", + testID: "bottom_tab_bar", + info: ScreenNames.BOTTOM_TAB_BAR, + icons: "🧱", + }, ];