From 216606a13722791d20f28508af1503626b12f47b Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Sun, 3 May 2026 13:47:30 +0800 Subject: [PATCH] Fix maintainVisibleContentPosition ignoring contentInset in Fabric ScrollView The Fabric implementation of `_adjustForMaintainVisibleContentPosition` did not account for `contentInset` when computing the autoscroll threshold comparison or the scroll target position, unlike the Paper implementation (`RCTScrollView.m`) which already handles this correctly. When a non-zero `contentInset` is set on a scroll view, two bugs occurred: 1. The threshold comparison used raw `contentOffset` instead of the inset-adjusted value, causing autoscroll to fire incorrectly. 2. The autoscroll target was hardcoded to `0` instead of `-inset`, causing the scroll view to jump to the wrong position. Fix: compute `bottomInset`/`leftInset` (respecting `isInverted`) and use them to adjust both the threshold comparison and the `scrollToOffset` target, matching the existing Paper logic in `RCTScrollView.m`. No behavior change when `contentInset` is zero. --- .../ScrollView/RCTScrollViewComponentView.mm | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index a087536f3af0..b4cfa3670a15 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -1096,13 +1096,14 @@ - (void)_adjustForMaintainVisibleContentPosition if (horizontal) { CGFloat deltaX = _firstVisibleView.frame.origin.x - _prevFirstVisibleFrame.origin.x; if (ABS(deltaX) > 0.5) { - CGFloat x = _scrollView.contentOffset.x; + CGFloat leftInset = [self isInverted] ? _scrollView.contentInset.right : _scrollView.contentInset.left; + CGFloat x = _scrollView.contentOffset.x + leftInset; [self _forceDispatchNextScrollEvent]; _scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x + deltaX, _scrollView.contentOffset.y); if (autoscrollThreshold) { // If the offset WAS within the threshold of the start, animate to the start. if (x <= autoscrollThreshold.value()) { - [self scrollToOffset:CGPointMake(0, _scrollView.contentOffset.y) animated:YES]; + [self scrollToOffset:CGPointMake(-leftInset, _scrollView.contentOffset.y) animated:YES]; } } } @@ -1110,13 +1111,14 @@ - (void)_adjustForMaintainVisibleContentPosition CGRect newFrame = _firstVisibleView.frame; CGFloat deltaY = newFrame.origin.y - _prevFirstVisibleFrame.origin.y; if (ABS(deltaY) > 0.5) { - CGFloat y = _scrollView.contentOffset.y; + CGFloat bottomInset = [self isInverted] ? _scrollView.contentInset.top : _scrollView.contentInset.bottom; + CGFloat y = _scrollView.contentOffset.y + bottomInset; [self _forceDispatchNextScrollEvent]; _scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, _scrollView.contentOffset.y + deltaY); if (autoscrollThreshold) { // If the offset WAS within the threshold of the start, animate to the start. if (y <= autoscrollThreshold.value()) { - [self scrollToOffset:CGPointMake(_scrollView.contentOffset.x, 0) animated:YES]; + [self scrollToOffset:CGPointMake(_scrollView.contentOffset.x, -bottomInset) animated:YES]; } } }