diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js
index 9e978c9e749c..b422c317292e 100644
--- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js
+++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js
@@ -392,7 +392,9 @@ export type ScrollViewPropsIOS = Readonly<{
* scrolls under the system UI. Across it, every value except `'never'` pads
* the content container by the insets, so content is laid out clear of the
* system UI, such as the rail on iPhone Duo or camera cutout in landscape,
- * while the container's background still extends edge to edge.
+ * while the container's background still extends edge to edge. On every
+ * edge, the `contentContainerStyle` padding counts toward the inset, so
+ * content sits at the larger of the two.
*
* @default `'never'`
* @platform ios
@@ -482,8 +484,7 @@ type StickyHeaderComponentType = component(
type ScrollViewBaseProps = Readonly<{
/**
- * These styles will be applied to the scroll view content container which
- * wraps all of the child views.
+ * Styles applied to the content container which wraps all child views.
*
* Example:
*
@@ -499,8 +500,25 @@ type ScrollViewBaseProps = Readonly<{
* }
* });
* ```
+ *
+ * **Interaction with `contentInsetAdjustmentBehavior`**
+ *
+ * Commonly, `contentContainerStyle` is used to pad items away from the
+ * screen edges or the system scrollbar.
+ *
+ * If setting `contentInsetAdjustmentBehavior` (recommended), `padding` is
+ * applied slightly differently than with a regular ``:
+ *
+ * - Pixel values: If a system safe area inset applies on a given edge, the
+ * **greater of** the `padding` or inset length will be applied. An edge
+ * without an inset, such as the top of a ScrollView below a header, keeps
+ * its full `padding`.
+ * - Percentage values: Across the scrollable axis, used directly, overriding
+ * any safe area padding. Along it, the safe area inset still applies, and
+ * the `padding` adds to it.
*/
contentContainerStyle?: ?ViewStyleProp,
+
/**
* Used to manually set the starting scroll offset.
*
diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollContentViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollContentViewComponentView.h
index 2d539ab2ca2a..1d418aa1afc1 100644
--- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollContentViewComponentView.h
+++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollContentViewComponentView.h
@@ -17,11 +17,19 @@ NS_ASSUME_NONNULL_BEGIN
@interface RCTScrollContentViewComponentView : RCTViewComponentView
/*
- * The safe area the content is laid out clear of, as padding on top of the content container's own. The enclosing
+ * The safe area the content is laid out clear of, as a floor on the content container's own padding. The enclosing
* scroll view sets the pair of insets across its scrollable axis, and zero while its `contentInsetAdjustmentBehavior`
* is `never`.
*/
-@property (nonatomic, assign) UIEdgeInsets safeAreaPadding;
+@property (nonatomic, readonly) UIEdgeInsets safeAreaPadding;
+
+/*
+ * The content inset UIKit adds for the safe area along the scrollable axis, which the content container's own padding
+ * on those edges counts toward. The enclosing scroll view sets the pair of insets along its scrollable axis.
+ */
+@property (nonatomic, readonly) UIEdgeInsets contentInsetAdjustment;
+
+- (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding contentInsetAdjustment:(UIEdgeInsets)contentInsetAdjustment;
@end
diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollContentViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollContentViewComponentView.mm
index 018d3c4ef00c..80c8a5985510 100644
--- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollContentViewComponentView.mm
+++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollContentViewComponentView.mm
@@ -30,27 +30,32 @@ - (instancetype)initWithFrame:(CGRect)frame
return self;
}
-- (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding
+- (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding contentInsetAdjustment:(UIEdgeInsets)contentInsetAdjustment
{
_safeAreaPadding = safeAreaPadding;
+ _contentInsetAdjustment = contentInsetAdjustment;
[self _updateStateIfNecessary];
}
+static EdgeInsets RCTRoundedEdgeInsets(UIEdgeInsets insets)
+{
+ return {
+ .left = RCTRoundPixelValue(insets.left),
+ .top = RCTRoundPixelValue(insets.top),
+ .right = RCTRoundPixelValue(insets.right),
+ .bottom = RCTRoundPixelValue(insets.bottom)};
+}
+
- (void)_updateStateIfNecessary
{
if (!_state) {
return;
}
- UIEdgeInsets insets = _safeAreaPadding;
- insets.left = RCTRoundPixelValue(insets.left);
- insets.top = RCTRoundPixelValue(insets.top);
- insets.right = RCTRoundPixelValue(insets.right);
- insets.bottom = RCTRoundPixelValue(insets.bottom);
-
RCTI18nUtil *i18nUtil = [RCTI18nUtil sharedInstance];
ScrollContentViewState newData;
- newData.safeAreaPadding = RCTEdgeInsetsFromUIEdgeInsets(insets);
+ newData.safeAreaPadding = RCTRoundedEdgeInsets(_safeAreaPadding);
+ newData.contentInsetAdjustment = RCTRoundedEdgeInsets(_contentInsetAdjustment);
newData.swapLeftAndRightInRTL = i18nUtil.isRTL && i18nUtil.doLeftAndRightSwapInRTL;
if (newData == _lastStateData) {
return;
@@ -90,6 +95,7 @@ - (void)prepareForRecycle
_state.reset();
_lastStateData = {};
_safeAreaPadding = UIEdgeInsetsZero;
+ _contentInsetAdjustment = UIEdgeInsetsZero;
}
@end
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 7550f08ae1ac..39def82fb6d4 100644
--- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
+++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
@@ -888,10 +888,16 @@ - (void)layoutSubviews
[self _updateContentViewSafeAreaPadding];
}
+- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView
+{
+ [self _updateContentViewSafeAreaPadding];
+}
+
/*
- * Hands the content container the safe area to pad its content by: the pair of insets across the scrollable axis,
- * unless `contentInsetAdjustmentBehavior` is `never`. Along the scrollable axis UIKit turns the safe area into content
- * insets instead, so content scrolls under the system UI.
+ * Hands the content container the safe area to lay its content out clear of, unless `contentInsetAdjustmentBehavior`
+ * is `never`: the pair of insets across the scrollable axis, which it pads by, and along the axis the content inset
+ * UIKit adds for the safe area, which its own padding counts toward. Content still scrolls under the system UI along
+ * the axis.
*/
- (void)_updateContentViewSafeAreaPadding
{
@@ -900,17 +906,24 @@ - (void)_updateContentViewSafeAreaPadding
}
const auto &props = static_cast(*_props);
UIEdgeInsets padding = UIEdgeInsetsZero;
+ UIEdgeInsets contentInset = UIEdgeInsetsZero;
if (props.contentInsetAdjustmentBehavior != ContentInsetAdjustmentBehavior::Never) {
UIEdgeInsets insets = self.safeAreaInsets;
+ UIEdgeInsets adjusted = _scrollView.adjustedContentInset;
+ UIEdgeInsets current = _scrollView.contentInset;
if (props.horizontal) {
padding.top = insets.top;
padding.bottom = insets.bottom;
+ contentInset.left = adjusted.left - current.left;
+ contentInset.right = adjusted.right - current.right;
} else {
padding.left = insets.left;
padding.right = insets.right;
+ contentInset.top = adjusted.top - current.top;
+ contentInset.bottom = adjusted.bottom - current.bottom;
}
}
- ((RCTScrollContentViewComponentView *)_contentView).safeAreaPadding = padding;
+ [(RCTScrollContentViewComponentView *)_contentView setSafeAreaPadding:padding contentInsetAdjustment:contentInset];
}
- (void)_handleScrollEndIfNeeded
diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewShadowNode.cpp
index 0632d156cb9d..67d97db8cf14 100644
--- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewShadowNode.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewShadowNode.cpp
@@ -7,6 +7,7 @@
#include "ScrollContentViewShadowNode.h"
+#include
#include
namespace facebook::react {
@@ -78,16 +79,21 @@ StyleLength resolvedPadding(
}
/*
+ * The safe area is a floor on the padding, as it is on UIKit's layout
+ * margins: the edge takes the larger of its style padding and the inset.
+ * Where UIKit already applies the inset as a content inset, the edge keeps
+ * what its padding exceeds that by, which lands the content on the same floor.
* A percentage padding keeps the edge as the style set it, with no inset
- * added, since Yoga holds one length per edge and a percentage has no point
- * value to add to.
+ * applied, since Yoga holds one length per edge and a percentage has no point
+ * value to compare.
*/
-StyleLength padded(StyleLength value, Float inset) {
- if (inset == 0 || value.isPercent()) {
+StyleLength padded(StyleLength value, Float inset, Float contentInset) {
+ if ((inset == 0 && contentInset == 0) || value.isPercent()) {
return value;
}
- auto base = value.isPoints() ? value.value().unwrap() : 0.0f;
- return StyleLength::points(base + inset);
+ auto base = value.isPoints() ? Float{value.value().unwrap()} : Float{0};
+ return StyleLength::points(
+ std::max(std::max(base, inset) - contentInset, Float{0}));
}
} // namespace
@@ -131,22 +137,31 @@ void ScrollContentViewShadowNode::applySafeAreaPadding() {
const ScrollContentViewState& state,
LayoutDirection layoutDirection) {
const auto& insets = state.safeAreaPadding;
+ const auto& contentInsets = state.contentInsetAdjustment;
auto swap = state.swapLeftAndRightInRTL;
- if (insets.top != 0 || insets.bottom != 0) {
+ if (insets.top != 0 || insets.bottom != 0 || contentInsets.top != 0 ||
+ contentInsets.bottom != 0) {
auto top = padded(
- resolvedPadding(style, Edge::Top, layoutDirection, swap), insets.top);
+ resolvedPadding(style, Edge::Top, layoutDirection, swap),
+ insets.top,
+ contentInsets.top);
auto bottom = padded(
resolvedPadding(style, Edge::Bottom, layoutDirection, swap),
- insets.bottom);
+ insets.bottom,
+ contentInsets.bottom);
style.setPadding(Edge::Top, top);
style.setPadding(Edge::Bottom, bottom);
}
- if (insets.left != 0 || insets.right != 0) {
+ if (insets.left != 0 || insets.right != 0 || contentInsets.left != 0 ||
+ contentInsets.right != 0) {
auto left = padded(
- resolvedPadding(style, Edge::Left, layoutDirection, swap), insets.left);
+ resolvedPadding(style, Edge::Left, layoutDirection, swap),
+ insets.left,
+ contentInsets.left);
auto right = padded(
resolvedPadding(style, Edge::Right, layoutDirection, swap),
- insets.right);
+ insets.right,
+ contentInsets.right);
// The logical edges take precedence over every other padding edge along
// the row. The physical edges are cleared, since at layout they are copied
// over the logical ones when left and right swap in RTL.
diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewShadowNode.h
index 8075ebc6d583..2cc900a2aa7f 100644
--- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewShadowNode.h
+++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewShadowNode.h
@@ -21,8 +21,9 @@ extern const char ScrollContentViewComponentName[];
/*
* `ShadowNode` for the component, the content container of
- * a . It pads its content by the safe area in its state, on top of
- * the padding its style sets.
+ * a . It lays its content out clear of the safe area in its state
+ * by at least the padding its style sets: across the scrollable axis as
+ * padding, and along it as what the padding exceeds UIKit's content inset by.
*/
class ScrollContentViewShadowNode final : public ConcreteViewShadowNode<
ScrollContentViewComponentName,
@@ -48,10 +49,11 @@ class ScrollContentViewShadowNode final : public ConcreteViewShadowNode<
void applySafeAreaPadding();
/*
- * Pads `style` by `state.safeAreaPadding`, on top of the padding `style`
- * resolves to on each edge, following the precedence Yoga gives the padding
- * edges for `layoutDirection`. An edge with a percentage padding keeps it
- * and gets no inset.
+ * Raises the padding `style` resolves to on each edge to at least
+ * `state.safeAreaPadding`, less `state.contentInsetAdjustment` where UIKit
+ * already applies the safe area as a content inset, following the precedence
+ * Yoga gives the padding edges for `layoutDirection`. An edge with a
+ * percentage padding keeps it and gets no inset.
*/
static void padForSafeArea(yoga::Style &style, const ScrollContentViewState &state, LayoutDirection layoutDirection);
diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewState.h b/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewState.h
index d1398350d912..b067d81333e8 100644
--- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewState.h
+++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollContentViewState.h
@@ -25,12 +25,19 @@ namespace facebook::react {
class ScrollContentViewState final {
public:
/*
- * The safe area the content is laid out clear of, as padding on top of the
- * padding the style sets. The enclosing scroll view sets the pair of edges
- * across its scrollable axis.
+ * The safe area the content is laid out clear of, as a floor on the padding
+ * the style sets. The enclosing scroll view sets the pair of edges across
+ * its scrollable axis.
*/
EdgeInsets safeAreaPadding{};
+ /*
+ * The content inset UIKit adds for the safe area along the scrollable axis,
+ * which the padding the style sets on those edges counts toward. The
+ * enclosing scroll view sets the pair of edges along its scrollable axis.
+ */
+ EdgeInsets contentInsetAdjustment{};
+
/*
* Whether left and right swap in RTL, which decides the padding edges the
* safe area adds to.
@@ -48,6 +55,11 @@ class ScrollContentViewState final {
(Float)data["top"].getDouble(),
(Float)data["right"].getDouble(),
(Float)data["bottom"].getDouble()}),
+ contentInsetAdjustment(
+ {(Float)data["contentInsetLeft"].getDouble(),
+ (Float)data["contentInsetTop"].getDouble(),
+ (Float)data["contentInsetRight"].getDouble(),
+ (Float)data["contentInsetBottom"].getDouble()}),
swapLeftAndRightInRTL(data["swapLeftAndRightInRTL"].getBool())
{
}
@@ -56,6 +68,8 @@ class ScrollContentViewState final {
{
return folly::dynamic::object("left", safeAreaPadding.left)("top", safeAreaPadding.top)(
"right", safeAreaPadding.right)("bottom", safeAreaPadding.bottom)(
+ "contentInsetLeft", contentInsetAdjustment.left)("contentInsetTop", contentInsetAdjustment.top)(
+ "contentInsetRight", contentInsetAdjustment.right)("contentInsetBottom", contentInsetAdjustment.bottom)(
"swapLeftAndRightInRTL", swapLeftAndRightInRTL);
}
#endif
diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/tests/ScrollContentViewTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/tests/ScrollContentViewTest.cpp
index 3dbd0e68e223..860b18967b58 100644
--- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/tests/ScrollContentViewTest.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/tests/ScrollContentViewTest.cpp
@@ -34,6 +34,13 @@ ScrollContentViewState stateWith(
return state;
}
+ScrollContentViewState stateWithContentInset(
+ EdgeInsets contentInsetAdjustment) {
+ ScrollContentViewState state;
+ state.contentInsetAdjustment = contentInsetAdjustment;
+ return state;
+}
+
constexpr auto kLTR = LayoutDirection::LeftToRight;
constexpr auto kRTL = LayoutDirection::RightToLeft;
@@ -82,7 +89,7 @@ TEST(ScrollContentViewShadowNodeTest, leavesStyleWithoutInsets) {
EXPECT_FALSE(style.padding(Edge::Start).isDefined());
}
-TEST(ScrollContentViewShadowNodeTest, padsRowOnTopOfResolvedPadding) {
+TEST(ScrollContentViewShadowNodeTest, raisesRowPaddingToTheInset) {
yoga::Style style;
style.setPadding(Edge::Horizontal, StyleLength::points(16));
@@ -90,20 +97,31 @@ TEST(ScrollContentViewShadowNodeTest, padsRowOnTopOfResolvedPadding) {
style, stateWith({.left = 0, .top = 0, .right = 84, .bottom = 0}), kLTR);
EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(16));
- EXPECT_EQ(style.padding(Edge::End), StyleLength::points(100));
+ EXPECT_EQ(style.padding(Edge::End), StyleLength::points(84));
EXPECT_FALSE(style.padding(Edge::Left).isDefined());
EXPECT_FALSE(style.padding(Edge::Right).isDefined());
}
+TEST(ScrollContentViewShadowNodeTest, keepsPaddingLargerThanTheInset) {
+ yoga::Style style;
+ style.setPadding(Edge::Horizontal, StyleLength::points(100));
+
+ ScrollContentViewShadowNode::padForSafeArea(
+ style, stateWith({.left = 0, .top = 0, .right = 84, .bottom = 0}), kLTR);
+
+ EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(100));
+ EXPECT_EQ(style.padding(Edge::End), StyleLength::points(100));
+}
+
TEST(ScrollContentViewShadowNodeTest, prefersLogicalOverPhysicalEdge) {
yoga::Style style;
style.setPadding(Edge::Left, StyleLength::points(4));
- style.setPadding(Edge::Start, StyleLength::points(12));
+ style.setPadding(Edge::Start, StyleLength::points(40));
ScrollContentViewShadowNode::padForSafeArea(
style, stateWith({.left = 20, .top = 0, .right = 0, .bottom = 0}), kLTR);
- EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(32));
+ EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(40));
}
TEST(ScrollContentViewShadowNodeTest, keepsPercentageAndSkipsInsetOnThatEdge) {
@@ -127,7 +145,7 @@ TEST(ScrollContentViewShadowNodeTest, padsPointsEdgeBesideAPercentageEdge) {
style, stateWith({.left = 20, .top = 0, .right = 84, .bottom = 0}), kLTR);
EXPECT_EQ(style.padding(Edge::Start), StyleLength::percent(10));
- EXPECT_EQ(style.padding(Edge::End), StyleLength::points(88));
+ EXPECT_EQ(style.padding(Edge::End), StyleLength::points(84));
}
TEST(ScrollContentViewShadowNodeTest, padsPhysicalEdgesInRTL) {
@@ -138,7 +156,7 @@ TEST(ScrollContentViewShadowNodeTest, padsPhysicalEdgesInRTL) {
style, stateWith({.left = 0, .top = 0, .right = 84, .bottom = 0}), kRTL);
// In RTL the start edge is on the right, where the inset is.
- EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(96));
+ EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(84));
EXPECT_FALSE(style.padding(Edge::End).isDefined());
}
@@ -153,15 +171,15 @@ TEST(ScrollContentViewShadowNodeTest, readsSwappedPhysicalEdgesInRTL) {
kRTL);
// Left is copied over start at layout, so it lands on the right, and the
- // inset on the left adds to the end edge.
+ // inset on the left raises the end edge.
EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(8));
- EXPECT_EQ(style.padding(Edge::End), StyleLength::points(86));
+ EXPECT_EQ(style.padding(Edge::End), StyleLength::points(84));
EXPECT_FALSE(style.padding(Edge::Left).isDefined());
}
TEST(ScrollContentViewShadowNodeTest, readsSwappedPhysicalEdgesInLTRSubtree) {
yoga::Style style;
- style.setPadding(Edge::Left, StyleLength::points(8));
+ style.setPadding(Edge::Left, StyleLength::points(100));
style.setPadding(Edge::Start, StyleLength::points(2));
ScrollContentViewShadowNode::padForSafeArea(
@@ -171,7 +189,7 @@ TEST(ScrollContentViewShadowNodeTest, readsSwappedPhysicalEdgesInLTRSubtree) {
// The swap applies to the whole tree, so left is still copied over start,
// which a node laid out left to right keeps on the left.
- EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(92));
+ EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(100));
EXPECT_FALSE(style.padding(Edge::End).isDefined());
}
@@ -182,11 +200,66 @@ TEST(ScrollContentViewShadowNodeTest, padsColumnForHorizontalScrollView) {
ScrollContentViewShadowNode::padForSafeArea(
style, stateWith({.left = 0, .top = 20, .right = 0, .bottom = 34}), kLTR);
- EXPECT_EQ(style.padding(Edge::Top), StyleLength::points(24));
- EXPECT_EQ(style.padding(Edge::Bottom), StyleLength::points(38));
+ EXPECT_EQ(style.padding(Edge::Top), StyleLength::points(20));
+ EXPECT_EQ(style.padding(Edge::Bottom), StyleLength::points(34));
EXPECT_EQ(style.padding(Edge::Vertical), StyleLength::points(4));
}
+TEST(ScrollContentViewShadowNodeTest, reducesColumnPaddingByTheContentInset) {
+ yoga::Style style;
+ style.setPadding(Edge::Vertical, StyleLength::points(20));
+
+ ScrollContentViewShadowNode::padForSafeArea(
+ style,
+ stateWithContentInset({.left = 0, .top = 59, .right = 0, .bottom = 34}),
+ kLTR);
+
+ // UIKit's content inset already keeps the content clear of the safe area
+ // along the scrollable axis, so a smaller padding sits inside it.
+ EXPECT_EQ(style.padding(Edge::Top), StyleLength::points(0));
+ EXPECT_EQ(style.padding(Edge::Bottom), StyleLength::points(0));
+}
+
+TEST(ScrollContentViewShadowNodeTest, keepsColumnPaddingBeyondTheContentInset) {
+ yoga::Style style;
+ style.setPadding(Edge::Vertical, StyleLength::points(100));
+
+ ScrollContentViewShadowNode::padForSafeArea(
+ style,
+ stateWithContentInset({.left = 0, .top = 59, .right = 0, .bottom = 34}),
+ kLTR);
+
+ EXPECT_EQ(style.padding(Edge::Top), StyleLength::points(41));
+ EXPECT_EQ(style.padding(Edge::Bottom), StyleLength::points(66));
+}
+
+TEST(ScrollContentViewShadowNodeTest, reducesRowPaddingByTheContentInsetInRTL) {
+ yoga::Style style;
+ style.setPadding(Edge::Horizontal, StyleLength::points(100));
+
+ ScrollContentViewShadowNode::padForSafeArea(
+ style,
+ stateWithContentInset({.left = 59, .top = 0, .right = 0, .bottom = 0}),
+ kRTL);
+
+ // In RTL the end edge is on the left, where the content inset is.
+ EXPECT_EQ(style.padding(Edge::Start), StyleLength::points(100));
+ EXPECT_EQ(style.padding(Edge::End), StyleLength::points(41));
+}
+
+TEST(ScrollContentViewShadowNodeTest, keepsPercentageBesideTheContentInset) {
+ yoga::Style style;
+ style.setPadding(Edge::Vertical, StyleLength::percent(10));
+
+ ScrollContentViewShadowNode::padForSafeArea(
+ style,
+ stateWithContentInset({.left = 0, .top = 59, .right = 0, .bottom = 0}),
+ kLTR);
+
+ EXPECT_EQ(style.padding(Edge::Top), StyleLength::percent(10));
+ EXPECT_EQ(style.padding(Edge::Bottom), StyleLength::percent(10));
+}
+
TEST(ScrollContentViewShadowNodeTest, cloningForChildCommitMustNotDirty) {
ComponentBuilder builder{registryWithScrollContentView()};
diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js
index 71d12f82c5c8..0ece3b0ca0be 100644
--- a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js
+++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js
@@ -360,7 +360,7 @@ const examples: Array = [
{
title: ' Content Inset Adjustment Behavior\n',
description:
- 'How the safe area modifies the content area of a full screen ScrollView: along the scrollable axis it becomes content insets, across it the content container is padded.',
+ 'How the safe area modifies the content area of a full screen ScrollView: along the scrollable axis it becomes content insets, across it the content container is padded, and on every edge the container padding counts toward it.',
platform: 'ios',
render(): React.Node {
return ;
@@ -1548,13 +1548,16 @@ const FULL_SCREEN_ITEMS = [...Array(40)].map((_, i) => `Item ${i}`);
const ContentInsetAdjustmentBehaviorExample = () => {
const [visible, setVisible] = useState(false);
const [horizontal, setHorizontal] = useState(false);
+ const [underHeader, setUnderHeader] = useState(false);
const [behavior, setBehavior] =
useState<(typeof CONTENT_INSET_ADJUSTMENT_BEHAVIORS)[number]>('automatic');
return (
Opens a full screen ScrollView that extends under the system UI,
- including the rail on iPhone Duo and the notch in landscape.
+ including the rail on iPhone Duo and the notch in landscape. "under
+ header" places a custom header above it, so it starts clear of the
+ status bar.
- {FULL_SCREEN_ITEMS.map(createItemRow)}
+ {FULL_SCREEN_ITEMS.map((msg, index) => (
+
+ ))}
@@ -1830,12 +1845,37 @@ const styles = StyleSheet.create({
fullScreenScrollView: {
backgroundColor: '#eeeeee',
},
+ // A stand-in for a navigation bar: tall enough to clear the status bar on
+ // every iPhone, with the title along its bottom edge.
+ fullScreenHeader: {
+ height: 104,
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ paddingBottom: 10,
+ backgroundColor: '#ffffff',
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: '#cccccc',
+ },
+ fullScreenHeaderTitle: {
+ fontSize: 19,
+ fontWeight: '600',
+ },
fullScreenContentContainer: {
backgroundColor: '#aae3b6',
+ padding: 20,
+ gap: 12,
},
fullScreenControls: {
- backgroundColor: '#eeeeee',
- paddingVertical: 10,
+ backgroundColor: '#ffffff',
+ borderRadius: 12,
+ padding: 8,
+ },
+ fullScreenItem: {
+ margin: 0,
+ paddingVertical: 14,
+ paddingHorizontal: 16,
+ backgroundColor: '#ffffff',
+ borderRadius: 12,
},
rowTitle: {
flex: 1,
diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
index abebe6d642de..d9c1957f5300 100644
--- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
+++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
@@ -1597,7 +1597,9 @@ interface RCTScheduler : public NSObject {
}
interface RCTScrollContentViewComponentView : public RCTViewComponentView {
- public @property (assign) UIEdgeInsets safeAreaPadding;
+ public @property (assign, readonly) UIEdgeInsets contentInsetAdjustment;
+ public @property (assign, readonly) UIEdgeInsets safeAreaPadding;
+ public virtual void setSafeAreaPadding:contentInsetAdjustment:(UIEdgeInsets safeAreaPadding, UIEdgeInsets contentInsetAdjustment);
}
interface RCTScrollEvent : public NSObject {
diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api
index e95a4e14f41d..fd8ac4d2f930 100644
--- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api
+++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api
@@ -1640,7 +1640,9 @@ interface RCTScheduler : public NSObject {
}
interface RCTScrollContentViewComponentView : public RCTViewComponentView {
- public @property (assign) UIEdgeInsets safeAreaPadding;
+ public @property (assign, readonly) UIEdgeInsets contentInsetAdjustment;
+ public @property (assign, readonly) UIEdgeInsets safeAreaPadding;
+ public virtual void setSafeAreaPadding:contentInsetAdjustment:(UIEdgeInsets safeAreaPadding, UIEdgeInsets contentInsetAdjustment);
}
interface RCTScrollEvent : public NSObject {
diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api
index fa99b41ba3dd..52fdf7d66710 100644
--- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api
+++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api
@@ -1596,7 +1596,9 @@ interface RCTScheduler : public NSObject {
}
interface RCTScrollContentViewComponentView : public RCTViewComponentView {
- public @property (assign) UIEdgeInsets safeAreaPadding;
+ public @property (assign, readonly) UIEdgeInsets contentInsetAdjustment;
+ public @property (assign, readonly) UIEdgeInsets safeAreaPadding;
+ public virtual void setSafeAreaPadding:contentInsetAdjustment:(UIEdgeInsets safeAreaPadding, UIEdgeInsets contentInsetAdjustment);
}
interface RCTScrollEvent : public NSObject {
diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
index 0d073640fafd..e791cccc5f97 100644
--- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
+++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
@@ -1597,7 +1597,9 @@ interface RCTScheduler : public NSObject {
}
interface RCTScrollContentViewComponentView : public RCTViewComponentView {
- public @property (assign) UIEdgeInsets safeAreaPadding;
+ public @property (assign, readonly) UIEdgeInsets contentInsetAdjustment;
+ public @property (assign, readonly) UIEdgeInsets safeAreaPadding;
+ public virtual void setSafeAreaPadding:contentInsetAdjustment:(UIEdgeInsets safeAreaPadding, UIEdgeInsets contentInsetAdjustment);
}
interface RCTScrollEvent : public NSObject {