Skip to content

Implement fadingEdgeLength for ScrollView on iOS - #59003

Open
YevheniiKotyrlo wants to merge 2 commits into
react:mainfrom
YevheniiKotyrlo:fix/ios-scrollview-fading-edge
Open

YevheniiKotyrlo wants to merge 2 commits into
react:mainfrom
YevheniiKotyrlo:fix/ios-scrollview-fading-edge

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

ScrollView declares fadingEdgeLength on its cross-platform props, but only Android implements it. On iOS the value never reaches native code — the iOS view config does not list it and the cxx HostPlatformScrollViewProps has no field for it — so a scroller whose overflow cue is its fading edge shows none, usually with its scroll indicator turned off beside it.

This implements it on iOS. RCTScrollViewComponentView masks the inner UIScrollView's layer with a CAGradientLayer in which each edge fades over its own length at full strength, which is what Android draws for {start, end} (AOSP scales the fade by fadeHeight × strength, and the strength is value / max(start, end)); a number fades both edges. The mask is re-framed to the scroll view's bounds on scroll so it stays fixed to the viewport, mirrors with the scroll view in RTL so start and end stay logical, and is removed for a zero pair. It sits on the scroll view rather than the component view because RCTViewComponentView owns the component view's layer.mask for clipping rounded corners.

The iOS view config now forwards fadingEdgeLength and horizontal — the mask follows the declared axis, as Android does, rather than the contentSize heuristic, which would orient it wrongly while the content fits — and the prop moves to the shared props: from ScrollViewPropsAndroid in Flow, the TypeScript types and the API snapshot, and from Android's HostPlatformScrollViewProps to BaseScrollViewProps in C++, where both platforms parse it. RCTEnhancedScrollView's safe-area padding (#59528) reads the same horizontal, which until now is false for every iOS scroller; #59194 adds horizontal to the iOS view config too. horizontal now resets to false when the prop is removed, its declared default, as #59125 did for six other props; it reset to true, which nothing acted on until iOS received the prop. The RNTester example is shared now and shows 0, 40 and {start: 0, end: 80} side by side, each on a horizontal and a vertical scroller.

Changelog:

[IOS] [ADDED] - ScrollView and the lists built on it support fadingEdgeLength on iOS

Test Plan:

  • RCTScrollViewComponentViewTests, eight new cases: each edge's own length, a number on both edges, a horizontal scroll view, lengths that would overlap, zero removing the mask, the mask following the content offset, a resized scroll view, and props parsed from a raw update that removes horizontal. No Podfile enables React-RCTFabric's Tests spec, so I ran the file in RNTester's RNTesterUnitTests on an iPhone 12 Pro Max, iOS 27.2, Xcode 27.2: 9 tests, 0 failures. Never building the mask fails the seven mask cases; never re-framing it on scroll fails the content-offset case; with horizontal resetting to true, the parsed case's mask stays along the width.
  • ScrollViewTest, three new cases that test_react_common_cxx runs: a removed horizontal resetting to its default, and fadingEdgeLength taking a number or a {start, end} pair and resetting to its default. On main the horizontal case fails, reset.horizontal being true against the default false; with this change the scroll view's tests pass with clang 18, 12 of 12.
  • RNTester › ScrollView › fadingEdgeLength on an iPhone 16 simulator, iOS 18.0, before and after: the pair. The 0 row is pixel-identical, and the {start: 0, end: 80} row changes only at its end edges.
  • On an iPad Pro (12.9-inch, 4th generation), iPadOS 27.0.1, with the change applied to 0.86: horizontal and vertical scrollers given per-edge lengths fade exactly those edges, in LTR and in RTL.
  • yarn flow-check, ESLint and Prettier on the changed JavaScript, scripts/clang-format.js on the changed C++ and Objective-C++, yarn test-typescript-legacy, yarn build-types (snapshot regenerated), yarn test-generated-typescript and yarn cxx-api-validate (Doxygen 1.16.1, Linux; the three Frameworks snapshots regenerated) pass, and ReactAndroid's rrc_scrollview builds for arm64-v8a with -Werror.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 12, 2026
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — iOS ignores fadingEdgeLength: every scroller keeps a hard edge, whatever the value.

FIXED — 40 fades both edges of the horizontal and the vertical scroller, {start: 0, end: 80} fades only their end edges, and 0 is unchanged.

before — main at 1c6e0c7738 after — with this PR

iPhone 16 simulator, iOS 18.0, RNTester › ScrollView › fadingEdgeLength, one session; before carries only this PR's RNTester example, so the native change is the one variable. The 0 row is identical to the byte; at the {start: 0, end: 80} row the largest per-channel difference is 176 (horizontal) and 148 (vertical) at the end edges and 1 at the start edges.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 12, 2026 04:52
@github-actions

Copy link
Copy Markdown

Warning

JavaScript API change detected

This PR commits an update to ReactNativeApi.d.ts, indicating a change to React Native's public JavaScript API.

  • Please include a clear changelog message.
  • This change will be subject to additional review.

This change was flagged as: POTENTIALLY_BREAKING

@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 12, 2026
ScrollView declares fadingEdgeLength on its cross-platform props, but only
Android implements it: the iOS view config does not list it and iOS's props
have no field for it, so iOS drops the value.

RCTScrollViewComponentView masks the inner UIScrollView's layer with a
CAGradientLayer in which each edge fades over its own length at full
strength, which is what Android draws for {start, end}. The iOS view config
forwards fadingEdgeLength and horizontal, so the mask follows the declared
axis. The prop moves to BaseScrollViewProps, where both platforms parse it,
and to the shared props in Flow, the TypeScript types and the API snapshot.

horizontal now resets to false when the prop is removed, as its declared
default is. It reset to true, so a ScrollView whose horizontal prop was
removed kept its horizontal axis on iOS.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/ios-scrollview-fading-edge branch from f9b6791 to 35d0736 Compare November 11, 2026 14:18
ScrollViewTest checks that a removed horizontal resets to its declared
default and that fadingEdgeLength takes a number or a {start, end} pair
and resets to its default, so test_react_common_cxx runs both on every
pull request.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant