From 5433e8f14c66abca04f63ae576d042bb1dff4552 Mon Sep 17 00:00:00 2001 From: Ahmed Awaad Date: Tue, 16 Dec 2025 15:49:42 +0300 Subject: [PATCH] feat: Add `layoutDirection` prop to `TabView` for iOS RTL support, including a new example. --- apps/example/src/App.tsx | 5 + apps/example/src/Examples/FourTabsRTL.tsx | 94 +++++++++++++++++++ .../example/src/Examples/NativeBottomTabs.tsx | 1 + .../ios/RCTTabViewComponentView.mm | 4 + .../ios/TabView/NewTabView.swift | 7 ++ .../ios/TabViewProps.swift | 1 + .../ios/TabViewProvider.swift | 8 ++ .../react-native-bottom-tabs/src/TabView.tsx | 8 ++ .../src/TabViewNativeComponent.ts | 1 + 9 files changed, 129 insertions(+) create mode 100644 apps/example/src/Examples/FourTabsRTL.tsx diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index ba1f7606..22604d9e 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -23,6 +23,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'; import JSBottomTabs from './Examples/JSBottomTabs'; import ThreeTabs from './Examples/ThreeTabs'; import FourTabs from './Examples/FourTabs'; +import FourTabsRTL from './Examples/FourTabsRTL'; import MaterialBottomTabs from './Examples/MaterialBottomTabs'; import SFSymbols from './Examples/SFSymbols'; import LabeledTabs from './Examples/Labeled'; @@ -72,6 +73,9 @@ const FourTabsActiveIndicatorColor = () => { const UnlabeledTabs = () => { return ; }; +const FourTabsRightToLeft = () => { + return ; +}; const examples = [ { @@ -161,6 +165,7 @@ const examples = [ name: 'Bottom Accessory View', screenOptions: { headerShown: false }, }, + { component: FourTabsRightToLeft, name: 'Four Tabs - RTL', platform: 'ios' }, ]; function App() { diff --git a/apps/example/src/Examples/FourTabsRTL.tsx b/apps/example/src/Examples/FourTabsRTL.tsx new file mode 100644 index 00000000..46e43fc2 --- /dev/null +++ b/apps/example/src/Examples/FourTabsRTL.tsx @@ -0,0 +1,94 @@ +import TabView, { SceneMap } from 'react-native-bottom-tabs'; +import React, { useState } from 'react'; +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; +import { I18nManager, type ColorValue } from 'react-native'; + +interface Props { + disablePageAnimations?: boolean; + scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent'; + backgroundColor?: ColorValue; + translucent?: boolean; + hideOneTab?: boolean; + rippleColor?: ColorValue; + activeIndicatorColor?: ColorValue; + layoutDirection?: 'leftToRight' | 'rightToLeft'; +} + +const renderScene = SceneMap({ + article: Article, + albums: Albums, + contacts: Contacts, + chat: Chat, +}); + +export default function FourTabsRTL({ + disablePageAnimations = false, + scrollEdgeAppearance = 'default', + backgroundColor, + translucent = true, + hideOneTab = false, + rippleColor, + activeIndicatorColor, + layoutDirection = 'leftToRight', +}: Props) { + React.useLayoutEffect(() => { + if (layoutDirection === 'rightToLeft') { + I18nManager.allowRTL(true); + I18nManager.forceRTL(true); + } + return () => { + if (layoutDirection === 'rightToLeft') { + I18nManager.allowRTL(false); + I18nManager.forceRTL(false); + } + }; + }, [layoutDirection]); + const [index, setIndex] = useState(0); + const [routes] = useState([ + { + key: 'article', + title: 'المقالات', + focusedIcon: require('../../assets/icons/article_dark.png'), + unfocusedIcon: require('../../assets/icons/chat_dark.png'), + badge: '!', + }, + { + key: 'albums', + title: 'البومات', + focusedIcon: require('../../assets/icons/grid_dark.png'), + badge: '5', + hidden: hideOneTab, + }, + { + key: 'contacts', + focusedIcon: require('../../assets/icons/person_dark.png'), + title: 'المتراسلين', + badge: ' ', + }, + { + key: 'chat', + focusedIcon: require('../../assets/icons/chat_dark.png'), + title: 'المحادثات', + role: 'search', + }, + ]); + + return ( + + ); +} diff --git a/apps/example/src/Examples/NativeBottomTabs.tsx b/apps/example/src/Examples/NativeBottomTabs.tsx index 190db787..7e89aa3a 100644 --- a/apps/example/src/Examples/NativeBottomTabs.tsx +++ b/apps/example/src/Examples/NativeBottomTabs.tsx @@ -15,6 +15,7 @@ function NativeBottomTabs() { initialRouteName="Chat" labeled={true} hapticFeedbackEnabled={false} + layoutDirection="leftToRight" tabBarInactiveTintColor="#C57B57" tabBarActiveTintColor="#F7DBA7" tabBarStyle={{ diff --git a/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm b/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm index c41f9316..92cab0e7 100644 --- a/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm +++ b/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm @@ -160,6 +160,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _tabViewProvider.hapticFeedbackEnabled = newViewProps.hapticFeedbackEnabled; } + if (oldViewProps.layoutDirection != newViewProps.layoutDirection) { + _tabViewProvider.layoutDirection = RCTNSStringFromStringNilIfEmpty(newViewProps.layoutDirection); + } + if (oldViewProps.fontSize != newViewProps.fontSize) { _tabViewProvider.fontSize = [NSNumber numberWithInt:newViewProps.fontSize]; } diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index d6993158..f878f8ca 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -11,6 +11,12 @@ struct NewTabView: AnyTabView { @ViewBuilder var body: some View { + var effectiveLayoutDirection: LayoutDirection { + if let layoutDirectionString = props.layoutDirection { + return layoutDirectionString == "rightToLeft" ? .rightToLeft : .leftToRight + } + return .leftToRight + } TabView(selection: $props.selectedPage) { ForEach(props.children) { child in if let index = props.children.firstIndex(of: child), @@ -49,6 +55,7 @@ struct NewTabView: AnyTabView { } } } + .environment(\.layoutDirection, effectiveLayoutDirection) .measureView { size in onLayout(size) } diff --git a/packages/react-native-bottom-tabs/ios/TabViewProps.swift b/packages/react-native-bottom-tabs/ios/TabViewProps.swift index cd098c07..2670ac99 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProps.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProps.swift @@ -66,6 +66,7 @@ class TabViewProps: ObservableObject { @Published var translucent: Bool = true @Published var disablePageAnimations: Bool = false @Published var hapticFeedbackEnabled: Bool = false + @Published var layoutDirection: String? @Published var fontSize: Int? @Published var fontFamily: String? @Published var fontWeight: String? diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index 013032f0..a03378f7 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -95,6 +95,12 @@ public final class TabInfo: NSObject { } } + @objc public var layoutDirection: NSString? { + didSet { + props.layoutDirection = layoutDirection as? String + } + } + @objc public var scrollEdgeAppearance: NSString? { didSet { props.scrollEdgeAppearance = scrollEdgeAppearance as? String @@ -155,6 +161,8 @@ public final class TabInfo: NSObject { } } + + @objc public var itemsData: [TabInfo] = [] { didSet { props.items = itemsData diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index 41b9cb37..3c04172d 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -201,6 +201,12 @@ interface Props { * @platform ios */ renderBottomAccessoryView?: BottomAccessoryViewProps['renderBottomAccessoryView']; + /** + * The direction of the layout. (iOS only) + * @platform ios + * @default 'leftToRight' + */ + layoutDirection?: 'leftToRight' | 'rightToLeft'; } const ANDROID_MAX_TABS = 100; @@ -239,6 +245,7 @@ const TabView = ({ tabBarStyle, tabLabelStyle, renderBottomAccessoryView, + layoutDirection = 'leftToRight', ...props }: Props) => { // @ts-ignore @@ -398,6 +405,7 @@ const TabView = ({ onTabBarMeasured={handleTabBarMeasured} onNativeLayout={handleNativeLayout} hapticFeedbackEnabled={hapticFeedbackEnabled} + layoutDirection={layoutDirection} activeTintColor={activeTintColor} inactiveTintColor={inactiveTintColor} barTintColor={tabBarStyle?.backgroundColor} diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts index 7949e156..50082c55 100644 --- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts +++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts @@ -56,6 +56,7 @@ export interface TabViewProps extends ViewProps { disablePageAnimations?: boolean; activeIndicatorColor?: ColorValue; hapticFeedbackEnabled?: boolean; + layoutDirection?: string; minimizeBehavior?: string; fontFamily?: string; fontWeight?: string;