diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index ba1f7606..78c8d6f9 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' }, ]; function App() { diff --git a/apps/example/src/Examples/FourTabsRTL.tsx b/apps/example/src/Examples/FourTabsRTL.tsx new file mode 100644 index 00000000..6ff9914d --- /dev/null +++ b/apps/example/src/Examples/FourTabsRTL.tsx @@ -0,0 +1,95 @@ +import TabView, { SceneMap } from 'react-native-bottom-tabs'; +import React 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'; +import type { LayoutDirection } from 'react-native-bottom-tabs/src/types'; + +interface Props { + disablePageAnimations?: boolean; + scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent'; + backgroundColor?: ColorValue; + translucent?: boolean; + hideOneTab?: boolean; + rippleColor?: ColorValue; + activeIndicatorColor?: ColorValue; + layoutDirection?: LayoutDirection; +} + +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 = 'locale', +}: Props) { + React.useLayoutEffect(() => { + if (layoutDirection === 'rtl') { + I18nManager.allowRTL(true); + I18nManager.forceRTL(true); + } + return () => { + if (layoutDirection === 'rtl') { + I18nManager.allowRTL(false); + I18nManager.forceRTL(false); + } + }; + }, [layoutDirection]); + const [index, setIndex] = React.useState(0); + const [routes] = React.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/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 23cd8759..1d0c4047 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -127,6 +127,10 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { layout(left, top, right, bottom) } + fun applyDirection(dir: Int) { + bottomNavigation.layoutDirection = dir + } + override fun requestLayout() { super.requestLayout() @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt index dad16c28..3f130e11 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt @@ -167,6 +167,15 @@ class RCTTabViewManager(context: ReactApplicationContext) : view.isHapticFeedbackEnabled = value } + override fun setLayoutDirection(view: ReactBottomNavigationView, value: String?) { + val direction = when (value) { + "rtl" -> View.LAYOUT_DIRECTION_RTL + "ltr" -> View.LAYOUT_DIRECTION_LTR + else -> View.LAYOUT_DIRECTION_LOCALE + } + view.applyDirection(direction) + } + override fun setFontFamily(view: ReactBottomNavigationView?, value: String?) { view?.setFontFamily(value) } 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..52089075 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -11,6 +11,16 @@ struct NewTabView: AnyTabView { @ViewBuilder var body: some View { + var effectiveLayoutDirection: LayoutDirection { + let dir = props.layoutDirection ?? "locale" + if let mapped = ["rtl": LayoutDirection.rightToLeft, + "ltr": LayoutDirection.leftToRight][dir] { + return mapped + } + let system = UIView.userInterfaceLayoutDirection(for: .unspecified) + return system == .rightToLeft ? .rightToLeft : .leftToRight +} + TabView(selection: $props.selectedPage) { ForEach(props.children) { child in if let index = props.children.firstIndex(of: child), @@ -49,6 +59,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..9cfb29a9 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..deac524d 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -95,6 +95,11 @@ 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 diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index 41b9cb37..4e93f090 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -22,7 +22,13 @@ import { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext'; import type { ImageSource } from 'react-native/Libraries/Image/ImageSource'; import NativeTabView from './TabViewNativeComponent'; import useLatestCallback from 'use-latest-callback'; -import type { AppleIcon, BaseRoute, NavigationState, TabRole } from './types'; +import type { + AppleIcon, + BaseRoute, + LayoutDirection, + NavigationState, + TabRole, +} from './types'; import DelayedFreeze from './DelayedFreeze'; import { BottomAccessoryView, @@ -201,6 +207,11 @@ interface Props { * @platform ios */ renderBottomAccessoryView?: BottomAccessoryViewProps['renderBottomAccessoryView']; + /** + * The direction of the layout. + * @default 'locale' + */ + layoutDirection?: LayoutDirection; } const ANDROID_MAX_TABS = 100; @@ -239,6 +250,7 @@ const TabView = ({ tabBarStyle, tabLabelStyle, renderBottomAccessoryView, + layoutDirection = 'locale', ...props }: Props) => { // @ts-ignore @@ -398,6 +410,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; diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts index 612dcd4f..83a9b2bb 100644 --- a/packages/react-native-bottom-tabs/src/types.ts +++ b/packages/react-native-bottom-tabs/src/types.ts @@ -7,6 +7,8 @@ export type AppleIcon = { sfSymbol: SFSymbol }; export type TabRole = 'search'; +export type LayoutDirection = 'ltr' | 'rtl' | 'locale'; + export type BaseRoute = { key: string; title?: string; diff --git a/packages/react-navigation/src/navigators/createNativeBottomTabNavigator.tsx b/packages/react-navigation/src/navigators/createNativeBottomTabNavigator.tsx index b76bdeb0..98cdd6e0 100644 --- a/packages/react-navigation/src/navigators/createNativeBottomTabNavigator.tsx +++ b/packages/react-navigation/src/navigators/createNativeBottomTabNavigator.tsx @@ -43,6 +43,7 @@ function NativeBottomTabNavigator({ screenOptions, tabBarActiveTintColor: customActiveTintColor, tabBarInactiveTintColor: customInactiveTintColor, + layoutDirection = 'locale', ...rest }: NativeBottomTabNavigatorProps) { const { colors } = useTheme(); @@ -77,6 +78,7 @@ function NativeBottomTabNavigator({ ); }