Repository navigation
Conversation
## Summary: Follow-up to #59194. `<ScrollView contentInsetAdjustmentBehavior>` keeps content clear of the safe area across the scrollable axis, such as the rail on iPhone Duo, by padding the content container. This moves that padding from JS into the content container's native component, so the safe area reaches the layout without a round trip through React. - `RCTScrollContentView`, which `<ScrollView>` already renders as its content container on iOS, becomes a component of its own instead of an alias of `View`. Its shadow node pads the Yoga style by the safe area in its state, on top of the padding the style resolves to on each edge, following the precedence Yoga gives the padding edges for the layout direction. - The scroll view hands its content container the pair of safe area insets across the scrollable axis whenever its own insets, props or window change, and zero while the behavior is `'never'`. - `<ScrollView>` no longer observes `experimental_onSafeAreaInsetsChange`, merges padding into `contentContainerStyle` or re-renders when the insets change. The JS padding helper and its tests go with it. Notes: - A scroll view moving within the safe area now costs a state update and layout per change rather than a React render. - Android is unchanged: it never renders `RCTScrollContentView`, and the shared view name mapping only loses the iOS alias. ## Changelog: [INTERNAL] [CHANGED] - Pad the iOS ScrollView content container for the safe area natively instead of from JS ## Test Plan: ``` ReactCommon/react/renderer/components/scrollview/tests/ScrollContentViewTest.cpp ``` ✅ Run locally against the real headers with a gtest stand-in: padding on top of `paddingHorizontal`, the logical edge over the physical one, a percentage on an inset edge, RTL with and without the left/right swap, and the column for a horizontal scroll view. ✅ Flow, ESLint, Jest under `Libraries/Components`, and clang-format. **RNTester on the folded iPhone Duo simulator** (iOS 27.1, Xcode 27.1), ScrollView > Content Inset Adjustment Behavior, full screen. Window 466×678 pt, safe area `{top 0, left 0, bottom 34, right 84}`; the 84 is the rail. | State | Result | |---|---| | vertical, never | ✅ Rows run under the rail: Item 0 row is 456 wide | | vertical, automatic | ✅ Rows stop at the rail: Item 0 row is `(5 99; 372 27)` and the controls row is 382 wide, while the container's background still runs under the rail | | vertical, always | ✅ Same as automatic, with no sideways offset | | horizontal, never | Items run under the home indicator | | horizontal, automatic | ✅ Items stop above the home indicator; `adjustedContentInset={0, 0, 0, 84}`, the rail along the scrollable axis only | | horizontal, always | ✅ Same, with no vertical offset | | vertical, automatic, scrolling | ✅ The scroll indicator stays at the device edge | ✅ **Layout identical to the JS version.** For vertical `never`, `automatic` and `always`, each from a fresh launch, the window rendered in-app (`drawViewHierarchyInRect:`) is pixel-identical between this commit and #59194, and every frame matches: | | never | automatic | always | |---|---|---|---| | `RCTEnhancedScrollView` frame | `(0 0; 466 678)` | `(0 0; 466 678)` | `(0 0; 466 678)` | | `adjustedContentInset` | `{0, 0, 0, 0}` | `{0, 0, 34, 0}` | `{0, 0, 34, 0}`, from `contentInset.right = -84` | | Content container frame | `(0 0; 466 1574)` | `(0 0; 466 1574)` | `(0 0; 466 1574)` | | Content container `LayoutMetrics.contentInsets.right` | 0 | 84 | 84 | ✅ **The padding no longer comes from JS.** Under `automatic`, on #59194 the content container's props carry the Yoga end padding of 84 that JS merges into `contentContainerStyle`. With this commit its props carry no padding, and the 84 comes from the native `safeAreaPadding` of `{0, 0, 0, 84}` in `ScrollContentViewState`, with the same `LayoutMetrics`. The JS inset subscription and re-render path are removed, so an inset change reaches layout through a state update; render counts were not measured. ✅ **Xcode's view debugger** (Debug > Attach to Process, View Debugging > Capture View Hierarchy) shows the class change and nothing else: - The content container is `RCTScrollContentViewComponentView`, a subclass of `RCTViewComponentView`, where #59194 shows a plain `RCTViewComponentView`. Filtering the navigator by `RCTScrollContent` finds it directly under `RCTEnhancedScrollView`, and finds nothing on #59194. - The Size inspector frames match the table above in both builds, and the 3D canvas shows the full-width container behind the inset rows in both. ``` never RCTScrollViewComponentView frame=(0 0; 466 678) RCTEnhancedScrollView frame=(0 0; 466 678) adjustedContentInset={0, 0, 0, 0} RCTScrollContentViewComponentView frame=(0 0; 466 1574) RCTViewComponentView frame=(0 0; 466 94) the controls row automatic RCTScrollViewComponentView frame=(0 0; 466 678) RCTEnhancedScrollView frame=(0 0; 466 678) adjustedContentInset={0, 0, 34, 0} RCTScrollContentViewComponentView frame=(0 0; 466 1574) RCTViewComponentView frame=(0 0; 382 94) the controls row, 84 clear of the rail ``` **Nesting in native tabs (react-native-screens):** RNTester's root hosted in a tab bar controller holding a navigation stack, with the first scroll view registered and flipped from `never` to `automatic` natively the way react-native-screens' native tabs do, and a horizontal `<ScrollView>` as the screen: - ✅ Prop unset: UIKit's insets are left alone, `adjustedContentInset={82, 0, 34, 84}`, and the cards rest below the navigation bar. - ✅ `automatic`: the container pads the top and bottom once and UIKit's cross-axis inset is removed, `adjustedContentInset={0, 0, 0, 84}`. - ✅ Switching between the values re-lays the content out in place.⚠️ Not run: Fantom, which targets Android, and an RTL layout on device; RTL is covered by the C++ cases.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
Follow-up to #59194.
<ScrollView contentInsetAdjustmentBehavior>keeps content clear of the safe area across the scrollable axis, such as the rail on iPhone Duo, by padding the content container. This moves that padding from JS into the content container's native component, so the safe area reaches the layout without a round trip through React.RCTScrollContentView, which<ScrollView>already renders as its content container on iOS, becomes a component of its own instead of an alias ofView. Its shadow node pads the Yoga style by the safe area in its state, on top of the padding the style resolves to on each edge, following the precedence Yoga gives the padding edges for the layout direction.'never'.<ScrollView>no longer observesexperimental_onSafeAreaInsetsChange, merges padding intocontentContainerStyleor re-renders when the insets change. The JS padding helper and its tests go with it.Notes:
RCTScrollContentView, and the shared view name mapping only loses the iOS alias.Changelog:
[INTERNAL] [CHANGED] - Pad the iOS ScrollView content container for the safe area natively instead of from JS
Test Plan:
✅ Run locally against the real headers with a gtest stand-in: padding on top of
paddingHorizontal, the logical edge over the physical one, a percentage on an inset edge, RTL with and without the left/right swap, and the column for a horizontal scroll view.✅ Flow, ESLint, Jest under
Libraries/Components, and clang-format.RNTester on the folded iPhone Duo simulator (iOS 27.1, Xcode 27.1), ScrollView > Content Inset Adjustment Behavior, full screen. Window 466×678 pt, safe area
{top 0, left 0, bottom 34, right 84}; the 84 is the rail.(5 99; 372 27)and the controls row is 382 wide, while the container's background still runs under the railadjustedContentInset={0, 0, 0, 84}, the rail along the scrollable axis only✅ Layout identical to the JS version. For vertical
never,automaticandalways, each from a fresh launch, the window rendered in-app (drawViewHierarchyInRect:) is pixel-identical between this commit and #59194, and every frame matches:RCTEnhancedScrollViewframe(0 0; 466 678)(0 0; 466 678)(0 0; 466 678)adjustedContentInset{0, 0, 0, 0}{0, 0, 34, 0}{0, 0, 34, 0}, fromcontentInset.right = -84(0 0; 466 1574)(0 0; 466 1574)(0 0; 466 1574)LayoutMetrics.contentInsets.right✅ The padding no longer comes from JS. Under
automatic, on #59194 the content container's props carry the Yoga end padding of 84 that JS merges intocontentContainerStyle. With this commit its props carry no padding, and the 84 comes from the nativesafeAreaPaddingof{0, 0, 0, 84}inScrollContentViewState, with the sameLayoutMetrics. The JS inset subscription and re-render path are removed, so an inset change reaches layout through a state update; render counts were not measured.✅ Xcode's view debugger (Debug > Attach to Process, View Debugging > Capture View Hierarchy) shows the class change and nothing else:
RCTScrollContentViewComponentView, a subclass ofRCTViewComponentView, where [ScrollView][iOS] Fix contentInsetAdjustmentBehavior for the horizontal safe area (iPhone Duo) #59194 shows a plainRCTViewComponentView. Filtering the navigator byRCTScrollContentfinds it directly underRCTEnhancedScrollView, and finds nothing on [ScrollView][iOS] Fix contentInsetAdjustmentBehavior for the horizontal safe area (iPhone Duo) #59194.Nesting in native tabs (react-native-screens): RNTester's root hosted in a tab bar controller holding a navigation stack, with the first scroll view registered and flipped from
nevertoautomaticnatively the way react-native-screens' native tabs do, and a horizontal<ScrollView>as the screen:adjustedContentInset={82, 0, 34, 84}, and the cards rest below the navigation bar.automatic: the container pads the top and bottom once and UIKit's cross-axis inset is removed,adjustedContentInset={0, 0, 0, 84}.