diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h index 6402bc6c86c..66e2163f37f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h @@ -52,6 +52,13 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL snapToEnd; @property (nonatomic, copy) NSArray *snapToOffsets; +/* + * Whether this scroll view is mirrored by a `scaleY(-1)` transform, as inverted virtualized lists + * are. UIKit does not account for that transform when it positions iOS 26 scroll edge effects, so + * they have to be compensated for here. + */ +@property (nonatomic, assign) BOOL inverted; + /* * Makes `setContentOffset:` method no-op when given `block` is executed. * The block is being executed synchronously. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm index b3481c1b98b..ef212e09cb6 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm @@ -51,6 +51,26 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (void)setInverted:(BOOL)inverted +{ + if (_inverted == inverted) { + return; + } + _inverted = inverted; + + if (@available(iOS 26.0, *)) { + // UIKit positions the scroll edge effects in this scroll view's own coordinate space, and the + // `scaleY(-1)` mirror turns that space upside down, so neither effect lands where it belongs. + // Against a navigation bar, `topEdgeEffect` darkens the lower half of the screen rather than + // the content passing under the bar; against a bottom bar, `bottomEdgeEffect` dims the whole + // screen. Neither produces anything useful at any scroll position while mirrored, so both are + // hidden. The bars cannot be given correct effects from here, because UIKit has no notion of + // the transform. + self.topEdgeEffect.hidden = inverted; + self.bottomEdgeEffect.hidden = inverted; + } +} + - (void)preserveContentOffsetWithBlock:(void (^)())block { if (!block) { 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 14a08b49cb7..ffd10bc6f23 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -455,6 +455,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } [super updateProps:props oldProps:oldProps]; + + // Must run after `super updateProps:` so it is based on the latest calculated view. + ((RCTEnhancedScrollView *)_scrollView).inverted = [self isInverted]; } - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index a8f040724fa..4d9e635ea72 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -1035,6 +1035,7 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode { interface RCTEnhancedScrollView : public UIScrollView { public @property (assign) BOOL centerContent; public @property (assign) BOOL disableIntervalMomentum; + public @property (assign) BOOL inverted; public @property (assign) BOOL pinchGestureEnabled; public @property (assign) BOOL snapToEnd; public @property (assign) BOOL snapToStart; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 3c0a535fb10..ab260583128 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -1035,6 +1035,7 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode { interface RCTEnhancedScrollView : public UIScrollView { public @property (assign) BOOL centerContent; public @property (assign) BOOL disableIntervalMomentum; + public @property (assign) BOOL inverted; public @property (assign) BOOL pinchGestureEnabled; public @property (assign) BOOL snapToEnd; public @property (assign) BOOL snapToStart; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 958586fc805..7ce061e8001 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -1035,6 +1035,7 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode { interface RCTEnhancedScrollView : public UIScrollView { public @property (assign) BOOL centerContent; public @property (assign) BOOL disableIntervalMomentum; + public @property (assign) BOOL inverted; public @property (assign) BOOL pinchGestureEnabled; public @property (assign) BOOL snapToEnd; public @property (assign) BOOL snapToStart;