Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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,30 +138,40 @@ 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 =
val startInsetEstimation =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Unnecessary renaming. This value name should stay untouched.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also applies to other names.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point dug into it and Yoga does handle logical edges correctly, but the existing setPadding() call maps paddingStart/paddingEnd to physical Edge::Left/Edge::Right, so the values never reach Yoga as logical. Pushed a commit that routes them through Edge::Start/Edge::End instead; tested in FabricExample and my production app, renders correctly in LTR and RTL.

if (isBackButtonDisplayed) {
toolbar.currentContentInsetStart + toolbar.paddingStart
} else {
max(toolbar.currentContentInsetStart, toolbar.paddingStart)
}

// 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.
val contentInsetStart =
configSubviews.firstOrNull { it.type === ScreenStackHeaderSubview.Type.LEFT }?.left
?: contentInsetStartEstimation

val contentInsetEnd = toolbar.currentContentInsetEnd + toolbar.paddingEnd
val endInset = toolbar.currentContentInsetEnd + toolbar.paddingEnd
val leftSubview =
configSubviews.firstOrNull { it.type === ScreenStackHeaderSubview.Type.LEFT }

// The shadow tree expects physical left/right padding, but the toolbar APIs
// return logical start/end values which swap sides in RTL.
val paddingLeft: Int
val paddingRight: Int

if (!isRtl) {
paddingLeft = leftSubview?.left ?: startInsetEstimation
paddingRight = endInset
} else {
paddingLeft = endInset
paddingRight =
if (leftSubview != null) toolbar.width - leftSubview.left else startInsetEstimation
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.

In RTL, toolbar.width - leftSubview.left overestimates the physical right padding because it includes the left-subview’s width (it measures from the subview’s left edge, not its right edge). This can still collapse the available width for the centered title. Compute the right-side inset from the toolbar’s right edge to the subview’s right edge instead (or otherwise subtract the subview width).

Suggested change
if (leftSubview != null) toolbar.width - leftSubview.left else startInsetEstimation
if (leftSubview != null) toolbar.width - leftSubview.right else startInsetEstimation

Copilot uses AI. Check for mistakes.
}

headerHeightUpdateProxy.updateHeaderHeightIfNeeded(this, screen)

updateHeaderConfigState(
toolbar.width,
toolbar.height,
contentInsetStart,
contentInsetEnd,
paddingLeft,
paddingRight,
)
}

Expand Down
58 changes: 58 additions & 0 deletions apps/src/tests/issue-tests/Test3438.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { View, Text, StyleSheet, I18nManager } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

function HomeScreen() {
return (
<View style={styles.container}>
<Text>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>
);
}

function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
headerTitleAlign: 'center',
headerTitle: () => (
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>
Custom Title
</Text>
),
}}
/>
</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,
},
note: {
textAlign: 'center',
marginTop: 20,
color: 'gray',
},
});

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