fix view culling in RTL languages (#52602)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52602

changelog: [internal]

fixes RTL issue in View Culling where scroll view offset was not correctly adjusted for RTL. The example failing case is described in a test.

Reviewed By: lenaic

Differential Revision: D78322759

fbshipit-source-id: d60d98aa45d4d9b576b133990f64ef941e6618e8
This commit is contained in:
Samuel Susla
2025-07-15 06:34:39 -07:00
committed by Facebook GitHub Bot
parent f004cd39bc
commit f3b6eeb96e
2 changed files with 65 additions and 0 deletions
@@ -2556,3 +2556,56 @@ describe('culling inside ScrollView with overflow visible', () => {
);
});
});
describe('horizontal ScrollView in RTL script', () => {
it('renders item 1', () => {
const root = Fantom.createRoot({viewportWidth: 100, viewportHeight: 100});
Fantom.runTask(() => {
root.render(
<ScrollView
style={{direction: 'rtl', height: 100, width: 100}}
horizontal={true}>
<View nativeID={'item1'} style={{height: 90, width: 90, margin: 5}} />
<View nativeID={'item2'} style={{height: 90, width: 90, margin: 5}} />
</ScrollView>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "ScrollView", nativeID: (N/A)}',
'Create {type: "AndroidHorizontalScrollContentView", nativeID: (N/A)}',
'Create {type: "View", nativeID: "item1"}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "item1"}',
'Insert {type: "AndroidHorizontalScrollContentView", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
});
it('takes contentOffset into account', () => {
const root = Fantom.createRoot({viewportWidth: 100, viewportHeight: 100});
Fantom.runTask(() => {
root.render(
<ScrollView
style={{direction: 'rtl', height: 100, width: 100}}
horizontal={true}
contentOffset={{x: 100, y: 0}}>
<View nativeID={'item1'} style={{height: 90, width: 90, margin: 5}} />
<View nativeID={'item2'} style={{height: 90, width: 90, margin: 5}} />
</ScrollView>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "ScrollView", nativeID: (N/A)}',
'Create {type: "AndroidHorizontalScrollContentView", nativeID: (N/A)}',
'Create {type: "View", nativeID: "item2"}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "item2"}',
'Insert {type: "AndroidHorizontalScrollContentView", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
});
});
@@ -25,12 +25,24 @@ CullingContext CullingContext::adjustCullingContextIfNeeded(
dynamic_cast<const ScrollViewShadowNode*>(pair.shadowNode)) {
if (scrollViewShadowNode->getConcreteProps().yogaStyle.overflow() !=
yoga::Overflow::Visible) {
auto layoutMetrics = scrollViewShadowNode->getLayoutMetrics();
cullingContext.frame.origin =
-scrollViewShadowNode->getContentOriginOffset(
/* includeTransform */ true);
cullingContext.frame.size =
scrollViewShadowNode->getLayoutMetrics().frame.size;
cullingContext.transform = Transform::Identity();
if (layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
// In RTL, content offset is flipped horizontally.
// We need to flip the culling context frame to match.
// See:
// https://github.com/facebook/react-native/blob/c2f39cfdd87c32b9a59efe8a788b8a03f02b0ea0/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm#L579
auto stateData = scrollViewShadowNode->getStateData();
cullingContext.frame.origin.x =
stateData.contentBoundingRect.size.width -
layoutMetrics.frame.size.width - cullingContext.frame.origin.x;
}
} else {
cullingContext = {};
}