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 @@ -138,6 +138,7 @@ class ScreenStackHeaderConfig(
}

val isBackButtonDisplayed = toolbar.navigationIcon != null
val isRtl = toolbar.layoutDirection == LAYOUT_DIRECTION_RTL

Comment on lines 140 to 142
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

LAYOUT_DIRECTION_RTL / LAYOUT_DIRECTION_LTR are used unqualified in this file, but there is no import for these constants. This won’t compile unless they’re referenced via View.LAYOUT_DIRECTION_* or imported explicitly.

Copilot uses AI. Check for mistakes.
val contentInsetStartEstimation =
if (isBackButtonDisplayed) {
Expand All @@ -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
Expand Down
87 changes: 87 additions & 0 deletions apps/src/tests/issue-tests/Test3438.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Custom Title</Text>
);
}

function HomeScreen() {
return (
<View style={styles.container}>
<Text style={styles.heading}>Issue #3438</Text>
<Text>headerTitleAlign: center + headerTitle as function</Text>
<Text>RTL mode: {I18nManager.isRTL ? 'YES' : 'NO'}</Text>
<Text style={styles.note}>
The header title above should say "Custom Title".
{'\n'}If it's missing or there's extra space, the bug is present.
</Text>
<View style={styles.buttons}>
<Button title="Force RTL" onPress={() => toggleRTL(true)} />
<Button title="Force LTR" onPress={() => toggleRTL(false)} />
</View>
</View>
);
}

function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
headerTitleAlign: 'center',
headerTitle: HeaderTitle,
}}
/>
</Stack.Navigator>
Comment on lines +46 to +58
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This reproduction screen is the root of the stack, so Android typically won’t show a back button/navigation icon. If the original issue depends on the start inset created by the back button, this example may not trigger the bug. Consider adding a second screen and navigating to it so the header renders with a back button (and/or add a headerRight) to reliably reproduce #3438 in RTL.

Copilot uses AI. Check for mistakes.
</NavigationContainer>
);
}

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;
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 @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> imageLoader) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> imageLoader);
#endif // !ANDROID && !NDEBUG
Expand Down