diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt
index 8ab37c8869..3eb6cc66e1 100644
--- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt
@@ -138,6 +138,7 @@ class ScreenStackHeaderConfig(
}
val isBackButtonDisplayed = toolbar.navigationIcon != null
+ val isRtl = toolbar.layoutDirection == LAYOUT_DIRECTION_RTL
val contentInsetStartEstimation =
if (isBackButtonDisplayed) {
@@ -149,8 +150,11 @@ class ScreenStackHeaderConfig(
// Assuming that there is nothing to the left of back button here, the content
// offset we're interested in in ShadowTree is the `left` of the subview left.
// In case it is not available we fallback to approximation.
+ // In RTL, the LEFT subview (Gravity.START) sits on the physical right, so we
+ // convert its physical `left` to a logical start-side distance.
val contentInsetStart =
- configSubviews.firstOrNull { it.type === ScreenStackHeaderSubview.Type.LEFT }?.left
+ configSubviews.firstOrNull { it.type === ScreenStackHeaderSubview.Type.LEFT }
+ ?.let { if (!isRtl) it.left else toolbar.width - it.left }
?: contentInsetStartEstimation
val contentInsetEnd = toolbar.currentContentInsetEnd + toolbar.paddingEnd
diff --git a/apps/src/tests/issue-tests/Test3438.tsx b/apps/src/tests/issue-tests/Test3438.tsx
new file mode 100644
index 0000000000..0b2ec80b67
--- /dev/null
+++ b/apps/src/tests/issue-tests/Test3438.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ I18nManager,
+ Button,
+ DevSettings,
+} from 'react-native';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+
+const Stack = createNativeStackNavigator();
+
+function toggleRTL(forceRTL: boolean) {
+ if (I18nManager.isRTL === forceRTL) return;
+ I18nManager.forceRTL(forceRTL);
+ I18nManager.allowRTL(forceRTL);
+ DevSettings.reload();
+}
+
+function HeaderTitle() {
+ return (
+ Custom Title
+ );
+}
+
+function HomeScreen() {
+ return (
+
+ Issue #3438
+ headerTitleAlign: center + headerTitle as function
+ RTL mode: {I18nManager.isRTL ? 'YES' : 'NO'}
+
+ The header title above should say "Custom Title".
+ {'\n'}If it's missing or there's extra space, the bug is present.
+
+
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 20,
+ gap: 8,
+ },
+ heading: {
+ fontWeight: 'bold',
+ fontSize: 16,
+ },
+ note: {
+ textAlign: 'center',
+ marginTop: 20,
+ color: 'gray',
+ },
+ buttons: {
+ flexDirection: 'row',
+ gap: 12,
+ marginTop: 20,
+ },
+});
+
+export default App;
diff --git a/apps/src/tests/issue-tests/index.ts b/apps/src/tests/issue-tests/index.ts
index 495dd4d88c..dfa2329973 100644
--- a/apps/src/tests/issue-tests/index.ts
+++ b/apps/src/tests/issue-tests/index.ts
@@ -167,6 +167,7 @@ export { default as Test3369 } from './Test3369';
export { default as Test3379 } from './Test3379';
export { default as Test3422 } from './Test3422';
export { default as Test3425 } from './Test3425';
+export { default as Test3438 } from './Test3438';
export { default as Test3443 } from './Test3443';
export { default as Test3446 } from './Test3446';
export { default as Test3450 } from './Test3450';
diff --git a/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h b/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h
index e9c97bd332..21f9187db7 100644
--- a/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h
+++ b/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h
@@ -37,12 +37,8 @@ class RNSScreenStackHeaderConfigComponentDescriptor final
#ifdef ANDROID
if (stateData.frameSize.width != 0) {
layoutableShadowNode.setSize({stateData.frameSize.width, YGUndefined});
- layoutableShadowNode.setPadding({
- stateData.paddingStart,
- 0,
- stateData.paddingEnd,
- 0,
- });
+ configShadowNode.setLogicalPadding(
+ stateData.paddingStart, stateData.paddingEnd);
}
#else
if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
diff --git a/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.cpp b/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.cpp
index b3ea640187..aaa0d28ff4 100644
--- a/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.cpp
+++ b/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.cpp
@@ -29,6 +29,18 @@ void RNSScreenStackHeaderConfigShadowNode::applyFrameCorrections() {
layoutMetrics_.frame.origin.y = -stateData.frameSize.height;
}
+void RNSScreenStackHeaderConfigShadowNode::setLogicalPadding(
+ Float start,
+ Float end) const {
+ ensureUnsealed();
+
+ auto style = yogaNode_.style();
+ style.setPadding(yoga::Edge::Start, yoga::StyleLength::points(start));
+ style.setPadding(yoga::Edge::End, yoga::StyleLength::points(end));
+ yogaNode_.setStyle(style);
+ yogaNode_.setDirty(true);
+}
+
#if !defined(ANDROID)
void RNSScreenStackHeaderConfigShadowNode::setImageLoader(
std::weak_ptr imageLoader) {
diff --git a/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.h b/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.h
index 4586ad2309..e71fb36547 100644
--- a/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.h
+++ b/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.h
@@ -29,6 +29,8 @@ class JSI_EXPORT RNSScreenStackHeaderConfigShadowNode final
#pragma mark - Custom interface
+ void setLogicalPadding(Float start, Float end) const;
+
#if !defined(ANDROID)
void setImageLoader(std::weak_ptr imageLoader);
#endif // !ANDROID && !NDEBUG