diff --git a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-viewCulling-itest.js b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-viewCulling-itest.js index 3c33c659e72..6e4c661b78f 100644 --- a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-viewCulling-itest.js +++ b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-viewCulling-itest.js @@ -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( + + + + , + ); + }); + + 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( + + + + , + ); + }); + + 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)}', + ]); + }); +}); diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/internal/CullingContext.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/internal/CullingContext.cpp index b061451ac0f..52c539965de 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/internal/CullingContext.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/internal/CullingContext.cpp @@ -25,12 +25,24 @@ CullingContext CullingContext::adjustCullingContextIfNeeded( dynamic_cast(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 = {}; }