Skip to content

Fix zIndex being ignored on Android ScrollView/FlatList with a RefreshControl - #58104

Open
kulkarni-rohan wants to merge 1 commit into
react:mainfrom
kulkarni-rohan:fix/31083-zindex-refresh-control
Open

Fix zIndex being ignored on Android ScrollView/FlatList with a RefreshControl#58104
kulkarni-rohan wants to merge 1 commit into
react:mainfrom
kulkarni-rohan:fix/31083-zindex-refresh-control

Conversation

@kulkarni-rohan

Copy link
Copy Markdown
Contributor

Summary:

Fixes #31083.

On Android, a ScrollView that has a RefreshControl is not one view. ScrollView.render() wraps it
in an AndroidSwipeRefreshLayout and splits the user's style across the two nodes with
splitLayoutProps() — layout props to the wrapper, painting props to the scroll view
(Libraries/Components/ScrollView/ScrollView.js:1939-1948).

splitLayoutProps routes position, top, left, right, bottom and transform to the outer
node, but never listed zIndex. So zIndex fell through to the inner NativeScrollView — which
is an only child of the wrapper, and therefore can never be reordered against anything. The user's
zIndex silently stops working the moment onRefresh is added, because onRefresh is exactly what
makes VirtualizedList synthesize a RefreshControl
(packages/virtualized-lists/Lists/VirtualizedList.js:1308-1317).

That is why removing a single onRefresh prop fixes the stacking, and why the workaround in the issue
thread — wrapping the list in <View style={{zIndex: 1}}> — works: it reintroduces a node that the
parent can order.

On Android z-order is not a view property. BaseViewManager.setZIndex is an explicit no-op
("Z-order is managed at the C++ layer in Fabric"); ConcreteViewShadowNode copies zIndex into
orderIndex_, and sliceChildShadowNodeViewPairs stable-sorts a parent's children by it to assign
mount indices. The parent does the ordering, reading each child's own zIndex — so the prop has to
be on the wrapper. Correspondingly, sliceChildShadowNodeViewPairs returns early for a single child,
which is why the current placement is inert rather than merely wrong.

This is a regression, not a long-standing gap: the wrapper split and this omission both arrived in
d9a8ac5 (#24411), first shipping in 0.60.0.

Prior art for this exact shape of fix: #26611 added transform to this same outer list for the same
reason, and #24411 itself created the list. Both landed.

Both consumers already apply the split correctly and need no change — ScrollView.js:1939 and
Libraries/Animated/components/AnimatedScrollView.js:94. splitLayoutProps is internal (not exported
from index.js, absent from ReactNativeApi.d.ts) and the diff adds only a case label, so no
generated artifact changes and no yarn build-types run is needed.

Scoped deliberately to zIndex. elevation is routed to inner too and has related symptoms, but
moving it is not equivalent — it would put the elevation on a background-less wrapper and change the
drop shadow — so it belongs in a separate change.

Note that because Android z-order is implemented by reordering mount indices, this also affects touch
and accessibility order, not just paint order — which is the correct, consistent behaviour, and
matches what a list without onRefresh already does.

Changelog:

[ANDROID] [FIXED] - Fix zIndex being ignored on a ScrollView/FlatList that has a RefreshControl (e.g. when onRefresh is set)

Test Plan:

Added Libraries/Components/ScrollView/__tests__/ScrollView-refreshControl-test.js, which renders a
real ScrollView with a RefreshControl on Android and asserts where the style lands — this pins the
actual reported behaviour, not just the helper:

PASS packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-refreshControl-test.js
  ScrollView with a RefreshControl on Android
    ✓ applies zIndex to the wrapper, not to the inner ScrollView
    ✓ keeps non-layout style on the inner ScrollView

Verified it is a real regression guard — with the one-line fix reverted, it fails:

● applies zIndex to the wrapper, not to the inner ScrollView
    Expected: 7
    Received: undefined

The rendered trees, before and after:

BEFORE                                    AFTER
AndroidSwipeRefreshLayout                 AndroidSwipeRefreshLayout
  style: [..., {flexGrow, flexShrink}]      style: [..., {flexGrow, flexShrink, zIndex: 7}]
  └─ RCTScrollView                          └─ RCTScrollView
       style: [..., {zIndex: 7}]                 style: [...]              <- no zIndex

Also extended Libraries/StyleSheet/__tests__/splitLayoutProps-itest.js with a unit case covering the
routing directly.

Static checks, against main @ ab2ea64:

$ eslint --max-warnings 0 <the three files>
(clean, exit 0)

$ prettier --check <the three files>
All matched files use Prettier code style!

$ flow focus-check <the three files>
Found 0 errors

$ flow full-check
Found 0 errors

Flow 0.329.0 matching the .flowconfig pin; ESLint 8.57.0 and Prettier 3.9.4, both the versions
declared in package.json.

Not run locally: the Fantom itest (splitLayoutProps-itest.js) — the Fantom tester needs a JDK
plus cmake/ninja, which I don't have set up. Its assertions were verified by exercising the
transpiled module directly, and toEqual is used widely in existing itests. Also no on-device run;
the before/after above is from the rendered element tree, not a device.

…hControl

On Android a ScrollView with a RefreshControl is wrapped in an
AndroidSwipeRefreshLayout, and its style is split across the two nodes by
splitLayoutProps(). zIndex was not in the outer list, so it landed on the inner
NativeScrollView -- an only child, which can never be reordered -- and z-ordering
silently stopped working as soon as onRefresh was passed.

Route zIndex to the outer node alongside position, so the wrapper (the node the
parent actually orders) carries it.

Fixes react#31083
@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 Aug 24, 2026
@facebook-github-tools facebook-github-tools Bot added p: Facebook Partner: Facebook Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Aug 24, 2026
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. p: Facebook Partner: Facebook Partner 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.

Flatlist zIndex not working when using onRefresh prop

1 participant