From 1d9849805fc8a1d6673a7a3f22df2353efc6ce2b Mon Sep 17 00:00:00 2001 From: Ahmed Awaad Date: Tue, 3 Feb 2026 12:42:34 +0300 Subject: [PATCH 01/25] feat(iOS, Bottom Tabs): add support for direction property in bottom tabs --- ios/RNSConvert.h | 3 +++ ios/RNSConvert.mm | 13 +++++++++++++ .../host/RNSBottomTabsHostComponentView.h | 2 ++ .../host/RNSBottomTabsHostComponentView.mm | 13 +++++++++++++ .../host/RNSBottomTabsHostComponentViewManager.mm | 1 + src/components/tabs/TabsHost.tsx | 2 ++ src/components/tabs/TabsHost.types.ts | 13 +++++++++++++ src/fabric/bottom-tabs/BottomTabsNativeComponent.ts | 3 +++ 8 files changed, 50 insertions(+) diff --git a/ios/RNSConvert.h b/ios/RNSConvert.h index 7d6e35462f..b8b4acecef 100644 --- a/ios/RNSConvert.h +++ b/ios/RNSConvert.h @@ -17,6 +17,9 @@ namespace react = facebook::react; + (UISemanticContentAttribute)UISemanticContentAttributeFromCppEquivalent: (react::RNSScreenStackHeaderConfigDirection)direction; ++ (UISemanticContentAttribute)UISemanticContentAttributeFromBottomTabsDirection: + (react::RNSBottomTabsDirection)direction; + + (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent: (react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode; diff --git a/ios/RNSConvert.mm b/ios/RNSConvert.mm index 905a03b549..f8930ca4d7 100644 --- a/ios/RNSConvert.mm +++ b/ios/RNSConvert.mm @@ -21,6 +21,19 @@ + (UISemanticContentAttribute)UISemanticContentAttributeFromCppEquivalent: } } ++ (UISemanticContentAttribute)UISemanticContentAttributeFromBottomTabsDirection: + (react::RNSBottomTabsDirection)direction +{ + switch (direction) { + using enum react::RNSBottomTabsDirection; + + case Rtl: + return UISemanticContentAttributeForceRightToLeft; + case Ltr: + return UISemanticContentAttributeForceLeftToRight; + } +} + + (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent: (react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode { diff --git a/ios/bottom-tabs/host/RNSBottomTabsHostComponentView.h b/ios/bottom-tabs/host/RNSBottomTabsHostComponentView.h index b0f65b8b2e..c3e4c40ac8 100644 --- a/ios/bottom-tabs/host/RNSBottomTabsHostComponentView.h +++ b/ios/bottom-tabs/host/RNSBottomTabsHostComponentView.h @@ -56,6 +56,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS; +@property (nonatomic) UISemanticContentAttribute direction; + #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) @property (nonatomic, readonly) UITabBarMinimizeBehavior tabBarMinimizeBehavior API_AVAILABLE(ios(26.0)); #endif // Check for iOS >= 26 diff --git a/ios/bottom-tabs/host/RNSBottomTabsHostComponentView.mm b/ios/bottom-tabs/host/RNSBottomTabsHostComponentView.mm index fc6257e36e..1718ca9b53 100644 --- a/ios/bottom-tabs/host/RNSBottomTabsHostComponentView.mm +++ b/ios/bottom-tabs/host/RNSBottomTabsHostComponentView.mm @@ -352,6 +352,12 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props } } + if (newComponentProps.direction != oldComponentProps.direction) { + _direction = [RNSConvert UISemanticContentAttributeFromBottomTabsDirection:newComponentProps.direction]; + _controller.view.semanticContentAttribute = _direction; + _controller.tabBar.semanticContentAttribute = _direction; + } + // Super call updates _props pointer. We should NOT update it before calling super. [super updateProps:props oldProps:oldProps]; } @@ -544,6 +550,13 @@ - (void)setTabBarControllerModeFromRNSTabBarControllerMode:(RNSTabBarControllerM } } +- (void)setDirection:(UISemanticContentAttribute)direction +{ + _direction = direction; + _controller.view.semanticContentAttribute = _direction; + _controller.tabBar.semanticContentAttribute = _direction; +} + - (void)setOnNativeFocusChange:(RCTDirectEventBlock)onNativeFocusChange { [self.reactEventEmitter setOnNativeFocusChange:onNativeFocusChange]; diff --git a/ios/bottom-tabs/host/RNSBottomTabsHostComponentViewManager.mm b/ios/bottom-tabs/host/RNSBottomTabsHostComponentViewManager.mm index 35e72854dd..8fa3b30631 100644 --- a/ios/bottom-tabs/host/RNSBottomTabsHostComponentViewManager.mm +++ b/ios/bottom-tabs/host/RNSBottomTabsHostComponentViewManager.mm @@ -34,6 +34,7 @@ - (UIView *)view // This remapping allows us to store UITabBarControllerMode in the component while accepting a custom enum as input // from JS. RCT_REMAP_VIEW_PROPERTY(tabBarControllerMode, tabBarControllerModeFromRNSTabBarControllerMode, RNSTabBarControllerMode); +RCT_EXPORT_VIEW_PROPERTY(direction, UISemanticContentAttribute); // TODO: Missing prop //@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS; diff --git a/src/components/tabs/TabsHost.tsx b/src/components/tabs/TabsHost.tsx index 8e5c049021..22776be499 100644 --- a/src/components/tabs/TabsHost.tsx +++ b/src/components/tabs/TabsHost.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { + I18nManager, Platform, StyleSheet, findNodeHandle, @@ -66,6 +67,7 @@ function TabsHost(props: TabsHostProps) { onNativeFocusChange={onNativeFocusChangeCallback} controlNavigationStateInJS={experimentalControlNavigationStateInJS} nativeContainerBackgroundColor={nativeContainerStyle?.backgroundColor} + direction={I18nManager.isRTL ? 'rtl' : 'ltr'} // @ts-ignore suppress ref - debug only ref={componentNodeRef} {...filteredProps}> diff --git a/src/components/tabs/TabsHost.types.ts b/src/components/tabs/TabsHost.types.ts index b4acf60a41..855a4aee12 100644 --- a/src/components/tabs/TabsHost.types.ts +++ b/src/components/tabs/TabsHost.types.ts @@ -33,6 +33,9 @@ export type TabBarMinimizeBehavior = // iOS-specific export type TabBarControllerMode = 'automatic' | 'tabBar' | 'tabSidebar'; +// iOS-specific +export type DirectionType = 'rtl' | 'ltr'; + export type TabsHostNativeContainerStyleProps = { /** * @summary Specifies the background color of the native container. @@ -273,6 +276,16 @@ export interface TabsHostProps { * @supported iOS 18 or higher */ tabBarControllerMode?: TabBarControllerMode; + /** + * @summary Sets the layout direction for the tab bar. + * + * Automatically detected from I18nManager.isRTL but can be overridden. + * + * @default 'ltr' + * + * @platform ios + */ + direction?: DirectionType; // #endregion iOS-only // #region Experimental support diff --git a/src/fabric/bottom-tabs/BottomTabsNativeComponent.ts b/src/fabric/bottom-tabs/BottomTabsNativeComponent.ts index 45181b60ce..bf9af5552e 100644 --- a/src/fabric/bottom-tabs/BottomTabsNativeComponent.ts +++ b/src/fabric/bottom-tabs/BottomTabsNativeComponent.ts @@ -29,6 +29,8 @@ type TabBarMinimizeBehavior = type TabBarControllerMode = 'automatic' | 'tabBar' | 'tabSidebar'; +type DirectionType = 'rtl' | 'ltr'; + export interface NativeProps extends ViewProps { // Events onNativeFocusChange?: CT.DirectEventHandler; @@ -63,6 +65,7 @@ export interface NativeProps extends ViewProps { tabBarTintColor?: ColorValue; tabBarMinimizeBehavior?: CT.WithDefault; tabBarControllerMode?: CT.WithDefault; + direction?: CT.WithDefault; // Control From aa3b0e1d810211217aab66acbd136a8f2c99fa1c Mon Sep 17 00:00:00 2001 From: Ahmed Awaad Date: Tue, 3 Feb 2026 18:22:03 +0300 Subject: [PATCH 02/25] feat(Android, Tabs): add setDirection method to TabsHostViewManager --- .../swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt index 873b73cbd9..e411d05fed 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabsHostViewManager.kt @@ -142,6 +142,11 @@ class TabsHostViewManager : value: String?, ) = Unit + override fun setDirection( + view: TabsHost, + value: String?, + ) = Unit + @ReactProp(name = "tabBarHidden") override fun setTabBarHidden( view: TabsHost, From 75757a2100a1456a4c3d6b57c8bac8a5394c11ac Mon Sep 17 00:00:00 2001 From: Ahmed Awaad Date: Tue, 3 Feb 2026 18:37:53 +0300 Subject: [PATCH 03/25] feat(tests): add Test3598 for bottom tabs configuration and mode switching --- apps/src/tests/issue-tests/Test3598.tsx | 101 ++++++++++++++++++++++++ apps/src/tests/issue-tests/index.ts | 1 + 2 files changed, 102 insertions(+) create mode 100644 apps/src/tests/issue-tests/Test3598.tsx diff --git a/apps/src/tests/issue-tests/Test3598.tsx b/apps/src/tests/issue-tests/Test3598.tsx new file mode 100644 index 0000000000..a7447e89c9 --- /dev/null +++ b/apps/src/tests/issue-tests/Test3598.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; + +import { TabBarControllerMode } from 'react-native-screens'; +import ConfigWrapperContext, { + type Configuration, + DEFAULT_GLOBAL_CONFIGURATION, +} from '../../shared/gamma/containers/bottom-tabs/ConfigWrapperContext'; +import { + BottomTabsContainer, + type TabConfiguration, +} from '../../shared/gamma/containers/bottom-tabs/BottomTabsContainer'; +import { CenteredLayoutView } from '../../shared/CenteredLayoutView'; +import { Text } from 'react-native'; +import { Button } from '../../shared'; + +function makeTab( + title: string, + controllerMode: TabBarControllerMode, + setControllerMode: (mode: TabBarControllerMode) => void, +) { + return function Tab() { + return ( + + {title} +