Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -12,6 +12,14 @@

NS_ASSUME_NONNULL_BEGIN

/*
* Returns the default `contentInsetAdjustmentBehavior` to apply when the JS
* prop is not set. On iOS 26+ (liquid glass) it defaults to `scrollableAxes` so
* scroll views adjust their content for translucent system chrome, unless the
* app opts into `UIDesignRequiresCompatibility`. Otherwise it stays `never`.
*/
UIScrollViewContentInsetAdjustmentBehavior RCTDefaultContentInsetAdjustmentBehavior(void);

/*
* Many `UIScrollView` customizations normally require creating a subclass which is not always convenient.
* `RCTEnhancedScrollView` has a delegate (conforming to this protocol) that allows customizing such behaviors without
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@
#import <React/RCTUtils.h>
#import <react/utils/FloatComparison.h>

UIScrollViewContentInsetAdjustmentBehavior RCTDefaultContentInsetAdjustmentBehavior(void)
{
// On iOS 26+, Apple's liquid glass design uses translucent system chrome
// (tab bars, toolbars), so scroll views should adjust their content insets
// for it unless the app opted into compatibility mode.
if (@available(iOS 26, *)) {
NSNumber *requiresCompatibility =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"];
if (!requiresCompatibility.boolValue) {
return UIScrollViewContentInsetAdjustmentScrollableAxes;
}
}
// Pre-iOS 26 (or compatibility mode): keep the historical opt-in behavior so
// iOS doesn't do weird things to UIScrollView insets automatically.
return UIScrollViewContentInsetAdjustmentNever;
}

@interface RCTEnhancedScrollView () <UIScrollViewDelegate>
@end

Expand All @@ -31,10 +48,7 @@ + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// We set the default behavior to "never" so that iOS
// doesn't do weird things to UIScrollView insets automatically
// and keeps it as an opt-in behavior.
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
self.contentInsetAdjustmentBehavior = RCTDefaultContentInsetAdjustmentBehavior();

// We intentionally force `UIScrollView`s `semanticContentAttribute` to `LTR` here
// because this attribute affects a position of vertical scrollbar; we don't want this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,15 +436,26 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &

if ((oldScrollViewProps.contentInsetAdjustmentBehavior != newScrollViewProps.contentInsetAdjustmentBehavior) ||
_shouldUpdateContentInsetAdjustmentBehavior) {
const auto contentInsetAdjustmentBehavior = newScrollViewProps.contentInsetAdjustmentBehavior;
if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Never) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Automatic) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::ScrollableAxes) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentScrollableAxes;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Always) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
// `nullopt` means the prop was not set from JS, so fall back to the host
// platform default (which adapts to iOS 26 liquid glass). An explicit value
// from JS - including `never` - is always honored.
if (newScrollViewProps.contentInsetAdjustmentBehavior.has_value()) {
switch (*newScrollViewProps.contentInsetAdjustmentBehavior) {
case ContentInsetAdjustmentBehavior::Never:
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
break;
case ContentInsetAdjustmentBehavior::Automatic:
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
break;
case ContentInsetAdjustmentBehavior::ScrollableAxes:
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentScrollableAxes;
break;
case ContentInsetAdjustmentBehavior::Always:
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
break;
}
} else {
scrollView.contentInsetAdjustmentBehavior = RCTDefaultContentInsetAdjustmentBehavior();
}
_shouldUpdateContentInsetAdjustmentBehavior = NO;
}
Expand Down Expand Up @@ -698,10 +709,7 @@ - (void)prepareForRecycle
_contentSize = CGSizeZero;
// Reset contentInset to prevent stale insets leaking into recycled scroll views.
_scrollView.contentInset = UIEdgeInsetsZero;
// We set the default behavior to "never" so that iOS
// doesn't do weird things to UIScrollView insets automatically
// and keeps it as an opt-in behavior.
_scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
_scrollView.contentInsetAdjustmentBehavior = RCTDefaultContentInsetAdjustmentBehavior();
_shouldUpdateContentInsetAdjustmentBehavior = YES;
_isUserTriggeredScrolling = NO;
CGRect oldFrame = self.frame;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ BaseScrollViewProps::BaseScrollViewProps(
rawProps,
"contentInsetAdjustmentBehavior",
sourceProps.contentInsetAdjustmentBehavior,
{ContentInsetAdjustmentBehavior::Never})),
{})),
scrollToOverflowEnabled(
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
? sourceProps.scrollToOverflowEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ class BaseScrollViewProps : public ViewProps {
std::vector<Float> snapToOffsets{};
bool snapToStart{true};
bool snapToEnd{true};
ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior{ContentInsetAdjustmentBehavior::Never};
// `nullopt` means the prop was not set from JS, in which case the host
// platform decides the default (e.g. on iOS 26+ liquid glass defaults to
// `scrollableAxes`). An explicit JS value (including `never`) is preserved.
std::optional<ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior{};
bool scrollToOverflowEnabled{false};
bool isInvertedVirtualizedList{false};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,19 +352,21 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps(

if (contentInsetAdjustmentBehavior !=
oldProps->contentInsetAdjustmentBehavior) {
switch (contentInsetAdjustmentBehavior) {
case ContentInsetAdjustmentBehavior::Never:
result["contentInsetAdjustmentBehavior"] = "never";
break;
case ContentInsetAdjustmentBehavior::Automatic:
result["contentInsetAdjustmentBehavior"] = "automatic";
break;
case ContentInsetAdjustmentBehavior::ScrollableAxes:
result["contentInsetAdjustmentBehavior"] = "scrollableAxes";
break;
case ContentInsetAdjustmentBehavior::Always:
result["contentInsetAdjustmentBehavior"] = "always";
break;
if (contentInsetAdjustmentBehavior.has_value()) {
switch (*contentInsetAdjustmentBehavior) {
case ContentInsetAdjustmentBehavior::Never:
result["contentInsetAdjustmentBehavior"] = "never";
break;
case ContentInsetAdjustmentBehavior::Automatic:
result["contentInsetAdjustmentBehavior"] = "automatic";
break;
case ContentInsetAdjustmentBehavior::ScrollableAxes:
result["contentInsetAdjustmentBehavior"] = "scrollableAxes";
break;
case ContentInsetAdjustmentBehavior::Always:
result["contentInsetAdjustmentBehavior"] = "always";
break;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1728,7 +1728,6 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Float decelerationRate;
Expand All @@ -1742,6 +1741,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<facebook::react::Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1722,7 +1722,6 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Float decelerationRate;
Expand All @@ -1736,6 +1735,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<facebook::react::Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1726,7 +1726,6 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Float decelerationRate;
Expand All @@ -1740,6 +1739,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<facebook::react::Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -4317,7 +4317,6 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Float decelerationRate;
Expand All @@ -4331,6 +4330,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<facebook::react::Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -4300,7 +4300,6 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Float decelerationRate;
Expand All @@ -4314,6 +4313,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<facebook::react::Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -4315,7 +4315,6 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Float decelerationRate;
Expand All @@ -4329,6 +4328,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<facebook::react::Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1060,13 +1060,13 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Point contentOffset;
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1055,13 +1055,13 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Point contentOffset;
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
2 changes: 1 addition & 1 deletion scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1058,13 +1058,13 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool showsVerticalScrollIndicator;
public bool snapToEnd;
public bool snapToStart;
public facebook::react::ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
public facebook::react::EdgeInsets contentInset;
public facebook::react::EdgeInsets scrollIndicatorInsets;
public facebook::react::Point contentOffset;
public facebook::react::ScrollViewIndicatorStyle indicatorStyle;
public facebook::react::ScrollViewKeyboardDismissMode keyboardDismissMode;
public facebook::react::ScrollViewSnapToAlignment snapToAlignment;
public std::optional<facebook::react::ContentInsetAdjustmentBehavior> contentInsetAdjustmentBehavior;
public std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition;
public std::vector<Float> snapToOffsets;
public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value);
Expand Down
Loading