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. horizontal now resets to false when the prop is removed, as its declared default is; it reset to true, and the mask is the only code that reads it. 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. React-RCTFabric's Tests spec passes on an iOS 18.0 simulator, 62 tests with 0 failures. No Podfile enables that spec, and RCTParagraphComponentViewTests.mm does not compile under CocoaPods because no pod packages react/renderer/element, so I ran it with :testspecs => ['Tests'] and that file excluded. 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.
  • 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.
  • flow full-check, ESLint and Prettier on the changed files, node scripts/clang-format.js --check, 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.

ScrollView declares fadingEdgeLength on its cross-platform props, but only
Android implements it. The iOS view config does not list it and the cxx
HostPlatformScrollViewProps has 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, and the prop moves to the shared props in Flow, the TypeScript types
and the API snapshot.
@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
fadingEdgeLength is a shared ScrollView prop in Flow and TypeScript, but
the C++ props declared it twice: on Android's HostPlatformScrollViewProps
and on a cxx HostPlatformScrollViewProps written only to carry it. It now
lives in BaseScrollViewProps, where both platforms parse it, and the cxx
props are an alias of the base again. Android's diff keeps reporting it
among the base props.

horizontal now resets to false when the prop is removed, as its declared
default is. It reset to true, which only iOS reads, so a ScrollView whose
horizontal prop was removed faded along its width.

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