Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ class TabsHostViewManager :
value: String?,
) = Unit

override fun setDirectionMode(
view: TabsHost,
value: String?,
) = Unit

@ReactProp(name = "tabBarHidden")
override fun setTabBarHidden(
view: TabsHost,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "nativeContainerBackgroundColor":
mViewManager.setNativeContainerBackgroundColor(view, ColorPropConverter.getColor(value, view.getContext()));
break;
case "directionMode":
mViewManager.setDirectionMode(view, (String) value);
break;
case "tabBarBackgroundColor":
mViewManager.setTabBarBackgroundColor(view, ColorPropConverter.getColor(value, view.getContext()));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
public interface RNSBottomTabsManagerInterface<T extends View> extends ViewManagerWithGeneratedInterface {
void setTabBarHidden(T view, boolean value);
void setNativeContainerBackgroundColor(T view, @Nullable Integer value);
void setDirectionMode(T view, @Nullable String value);
void setTabBarBackgroundColor(T view, @Nullable Integer value);
void setTabBarItemTitleFontFamily(T view, @Nullable String value);
void setTabBarItemTitleFontSize(T view, float value);
Expand Down
64 changes: 64 additions & 0 deletions apps/src/tests/issue-tests/Test3598.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';

import {
BottomTabsContainer,
type TabConfiguration,
} from '../../shared/gamma/containers/bottom-tabs/BottomTabsContainer';
import { CenteredLayoutView } from '../../shared/CenteredLayoutView';
import { I18nManager, Text } from 'react-native';

function makeTab(title: string) {
return function Tab() {
return (
<CenteredLayoutView>
<Text>{title}</Text>
<Text style={{ fontWeight: 'bold' }}>
{'Direction is ' + (I18nManager.isRTL ? 'RTL' : 'LTR')}
</Text>
</CenteredLayoutView>
);
};
}

function App() {
const TAB_CONFIGS: TabConfiguration[] = [
{
tabScreenProps: {
tabKey: 'Tab1',
title: 'قائمة ١',
icon: {
android: {
type: 'drawableResource',
name: 'sym_call_missed',
},
ios: {
type: 'sfSymbol',
name: 'sun.max',
},
},
},
component: makeTab('Tab 1'),
},
{
tabScreenProps: {
tabKey: 'Tab2',
title: 'قائمة ٢',
icon: {
android: {
type: 'drawableResource',
name: 'sym_call_incoming',
},
ios: {
type: 'sfSymbol',
name: 'snow',
},
},
},
component: makeTab('Tab 2'),
},
];

return <BottomTabsContainer tabConfigs={TAB_CONFIGS} />;
}

export default App;
1 change: 1 addition & 0 deletions apps/src/tests/issue-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export { default as Test3564 } from './Test3564';
export { default as Test3566 } from './Test3566';
export { default as Test3576 } from './Test3576';
export { default as Test3596 } from './Test3596';
export { default as Test3598 } from './Test3598';
export { default as Test3611 } from './Test3611';
export { default as Test3617 } from './Test3617';
export { default as TestScreenAnimation } from './TestScreenAnimation';
Expand Down
2 changes: 2 additions & 0 deletions ios/bottom-tabs/host/RNSBottomTabsHostComponentView.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ NS_ASSUME_NONNULL_BEGIN

@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;

@property (nonatomic, readonly) UISemanticContentAttribute directionMode;

#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
@property (nonatomic, readonly) UITabBarMinimizeBehavior tabBarMinimizeBehavior API_AVAILABLE(ios(26.0));
#endif // Check for iOS >= 26
Expand Down
36 changes: 36 additions & 0 deletions ios/bottom-tabs/host/RNSBottomTabsHostComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,24 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
}
}

if (newComponentProps.directionMode != oldComponentProps.directionMode) {
_directionMode =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use [RCTI18nUtil isRTL] instead of passing a prop, it should have an updated value as it reads it from [NSUserDefaults standardUserDefaults] (but it would be unable to react to dynamic changes). I'm not sure if we can consider this a stable API but it has been mentioned in blog post in react-native: https://reactnative.dev/blog/2016/08/19/right-to-left-support-for-react-native-apps.

cc @kkafar - let me know what you think is better

We can also leave the prop for now (it's not exposed as a part of API either way) and rethink our approach to RTL in separate PR - I'm wondering whether RTL should be handled via some top-level wrapper component for the entire hierarchy below it.

rnscreens::conversion::UISemanticContentAttributeFromTabsHostCppEquivalent(newComponentProps.directionMode);
#if RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
if (@available(iOS 17.0, *)) {
_controller.traitOverrides.layoutDirection = _directionMode == UISemanticContentAttributeForceRightToLeft
? UITraitEnvironmentLayoutDirectionRightToLeft
: UITraitEnvironmentLayoutDirectionLeftToRight;
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
{
_controller.view.semanticContentAttribute = _directionMode;
_controller.tabBar.semanticContentAttribute = _directionMode;
[[UIView appearanceWhenContainedInInstancesOfClasses:@[ _controller.tabBar.class ]]
setSemanticContentAttribute:_directionMode];
}
}

// Super call updates _props pointer. We should NOT update it before calling super.
[super updateProps:props oldProps:oldProps];
}
Expand Down Expand Up @@ -544,6 +562,24 @@ - (void)setTabBarControllerModeFromRNSTabBarControllerMode:(RNSTabBarControllerM
}
}

- (void)setDirectionMode:(UISemanticContentAttribute)directionMode
{
_directionMode = directionMode;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
if (@available(iOS 17.0, *)) {
_controller.traitOverrides.layoutDirection = _directionMode == UISemanticContentAttributeForceRightToLeft
? UITraitEnvironmentLayoutDirectionRightToLeft
: UITraitEnvironmentLayoutDirectionLeftToRight;
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
{
_controller.view.semanticContentAttribute = _directionMode;
_controller.tabBar.semanticContentAttribute = _directionMode;
[[UIView appearanceWhenContainedInInstancesOfClasses:@[ _controller.tabBar.class ]]
setSemanticContentAttribute:_directionMode];
}
}

- (void)setOnNativeFocusChange:(RCTDirectEventBlock)onNativeFocusChange
{
[self.reactEventEmitter setOnNativeFocusChange:onNativeFocusChange];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(directionMode, UISemanticContentAttribute);

// TODO: Missing prop
//@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;
Expand Down
15 changes: 15 additions & 0 deletions ios/conversion/RNSConversions-BottomTabs.mm
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,19 @@ UIUserInterfaceStyle UIUserInterfaceStyleFromBottomTabsScreenCppEquivalent(
}
}

UISemanticContentAttribute UISemanticContentAttributeFromTabsHostCppEquivalent(
react::RNSBottomTabsDirectionMode directionMode)
{
using enum facebook::react::RNSBottomTabsDirectionMode;
switch (directionMode) {
case Ltr:
return UISemanticContentAttributeForceLeftToRight;
case Rtl:
return UISemanticContentAttributeForceRightToLeft;
default:
RCTLogError(@"[RNScreens] unsupported direction mode");
break;
}
}

}; // namespace rnscreens::conversion
3 changes: 3 additions & 0 deletions ios/conversion/RNSConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ UIInterfaceOrientationMask UIInterfaceOrientationMaskFromRNSOrientation(RNSOrien
RNSOrientation RNSOrientationFromUIInterfaceOrientationMask(UIInterfaceOrientationMask orientationMask);
#endif // !TARGET_OS_TV

UISemanticContentAttribute UISemanticContentAttributeFromTabsHostCppEquivalent(
react::RNSBottomTabsDirectionMode directionMode);

#pragma mark SplitViewHost props

UISplitViewControllerSplitBehavior SplitViewPreferredSplitBehaviorFromHostProp(
Expand Down
9 changes: 3 additions & 6 deletions src/components/ScreenStackItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { FooterComponent } from './ScreenFooter';
import { SafeAreaViewProps } from './safe-area/SafeAreaView.types';
import SafeAreaView from './safe-area/SafeAreaView';
import { featureFlags } from '../flags';
import { isIOS26OrHigher } from './helpers/PlatformUtils';

type Props = Omit<
ScreenProps,
Expand Down Expand Up @@ -87,10 +88,7 @@ function ScreenStackItem(
headerConfig.blurEffect !== 'none';

warnOnce(
hasEdgeEffects &&
hasBlurEffect &&
Platform.OS === 'ios' &&
parseInt(Platform.Version, 10) >= 26,
hasEdgeEffects && hasBlurEffect && isIOS26OrHigher,
'[RNScreens] Using both `blurEffect` and `scrollEdgeEffects` simultaneously may cause overlapping effects.',
);

Expand All @@ -114,8 +112,7 @@ function ScreenStackItem(
contentStyle = contentWrapperStyles;
}

const shouldUseSafeAreaView =
Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 26;
const shouldUseSafeAreaView = isIOS26OrHigher;

const content = (
<>
Expand Down
4 changes: 4 additions & 0 deletions src/components/helpers/PlatformUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Platform } from 'react-native';

export const isIOS26OrHigher =
Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 26;
21 changes: 19 additions & 2 deletions src/components/tabs/TabsAccessory.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import BottomTabsAccessoryNativeComponent from '../../fabric/bottom-tabs/BottomTabsAccessoryNativeComponent';
import { TabsAccessoryProps } from './TabsAccessory.types';
import { StyleSheet } from 'react-native';
import { I18nManager, StyleSheet } from 'react-native';
import { isIOS26OrHigher } from '../helpers/PlatformUtils';

/**
* EXPERIMENTAL API, MIGHT CHANGE W/O ANY NOTICE
Expand All @@ -11,7 +12,23 @@ export default function TabsAccessory(props: TabsAccessoryProps) {
<BottomTabsAccessoryNativeComponent
{...props}
collapsable={false}
style={[props.style, StyleSheet.absoluteFill]}
style={[
props.style,
StyleSheet.absoluteFill,
styles.directionFromI18nManagerForIOS26,
]}
/>
);
}

const styles = StyleSheet.create({
// This style is needed on iOS 26+ to bring back valid direction as we're forcing
// `ltr` in TabsHost to ensure correct TabsBottomAccessory layout.
directionFromI18nManagerForIOS26: {
direction: isIOS26OrHigher
? I18nManager.isRTL
? 'rtl'
: 'ltr'
: undefined,
},
});
13 changes: 10 additions & 3 deletions src/components/tabs/TabsHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React, { useState } from 'react';
import {
I18nManager,
Platform,
StyleSheet,
findNodeHandle,
Expand All @@ -16,6 +17,7 @@ import { bottomTabsDebugLog } from '../../private/logging';
import TabsAccessory from './TabsAccessory';
import { TabsAccessoryEnvironment } from './TabsAccessory.types';
import TabsAccessoryContent from './TabsAccessoryContent';
import { isIOS26OrHigher } from '../helpers/PlatformUtils';

/**
* EXPERIMENTAL API, MIGHT CHANGE W/O ANY NOTICE
Expand Down Expand Up @@ -62,17 +64,17 @@ function TabsHost(props: TabsHostProps) {

return (
<BottomTabsNativeComponent
style={styles.fillParent}
style={[styles.fillParent, styles.forceLtrForIOS26]}
onNativeFocusChange={onNativeFocusChangeCallback}
controlNavigationStateInJS={experimentalControlNavigationStateInJS}
nativeContainerBackgroundColor={nativeContainerStyle?.backgroundColor}
directionMode={I18nManager.isRTL ? 'rtl' : 'ltr'}
// @ts-ignore suppress ref - debug only
ref={componentNodeRef}
{...filteredProps}>
{filteredProps.children}
{bottomAccessory &&
Platform.OS === 'ios' &&
parseInt(Platform.Version, 10) >= 26 &&
isIOS26OrHigher &&
(Platform.constants.reactNativeVersion.minor >= 82 ? (
<TabsAccessory>
<TabsAccessoryContent environment="regular">
Expand Down Expand Up @@ -102,4 +104,9 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
},
// For TabsBottomAccessory layout to work correctly, we need to use
// `ltr`. We restore direction in children views.
forceLtrForIOS26: {
direction: isIOS26OrHigher ? 'ltr' : undefined,
},
});
17 changes: 16 additions & 1 deletion src/components/tabs/TabsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from 'react';
import { Freeze } from 'react-freeze';
import {
I18nManager,
Image,
ImageResolvedAssetSource,
Platform,
Expand Down Expand Up @@ -34,6 +35,7 @@ import type {
PlatformIconAndroid,
PlatformIconIOS,
} from '../../types';
import { isIOS26OrHigher } from '../helpers/PlatformUtils';

/**
* EXPERIMENTAL API, MIGHT CHANGE W/O ANY NOTICE
Expand Down Expand Up @@ -130,7 +132,11 @@ function TabsScreen(props: TabsScreenProps) {
return (
<BottomTabsScreenNativeComponent
collapsable={false}
style={[style, styles.fillParent]}
style={[
style,
styles.fillParent,
styles.directionFromI18nManagerForIOS26,
]}
onWillAppear={onWillAppearCallback}
onDidAppear={onDidAppearCallback}
onWillDisappear={onWillDisappearCallback}
Expand Down Expand Up @@ -379,4 +385,13 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
},
// This style is needed on iOS 26+ to bring back valid direction as we're forcing
// `ltr` in TabsHost to ensure correct TabsBottomAccessory layout.
directionFromI18nManagerForIOS26: {
direction: isIOS26OrHigher
? I18nManager.isRTL
? 'rtl'
: 'ltr'
: undefined,
},
});
6 changes: 6 additions & 0 deletions src/fabric/bottom-tabs/BottomTabsNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type TabBarMinimizeBehavior =

type TabBarControllerMode = 'automatic' | 'tabBar' | 'tabSidebar';

type DirectionMode = 'rtl' | 'ltr';

export interface NativeProps extends ViewProps {
// Events
onNativeFocusChange?: CT.DirectEventHandler<NativeFocusChangeEvent>;
Expand All @@ -37,6 +39,10 @@ export interface NativeProps extends ViewProps {
tabBarHidden?: CT.WithDefault<boolean, false>;
nativeContainerBackgroundColor?: ColorValue;

// We can't use `direction` name for this prop as it's also used by
// direction style View prop.
directionMode?: CT.WithDefault<DirectionMode, 'ltr'>;

// Appearance
// tabBarAppearance?: TabBarAppearance; // Does not work due to codegen issue.

Expand Down
Loading