Fix SectionList.scrollToLocation no-op for itemIndex: 0#57594
Conversation
scrollToLocation flattened {sectionIndex, itemIndex} to a VirtualizedList
index without accounting for the current section's header cell, so
itemIndex: 0 resolved to the section header instead of the first row.
With sticky headers (iOS default) that header is already pinned to the
top, so the scroll was a visually imperceptible no-op and, because the
header cell is usually already measured, onScrollToIndexFailed never
fired -- exactly the report in react#50143.
Seed the flat index with `itemIndex + 1` so itemIndex: 0 targets the
first item (the row after the header), matching the documented "item at
the specified sectionIndex and itemIndex". Apply the sticky-header height
compensation for itemIndex >= 0 (was > 0) so the first row is not
obscured by the pinned header. Out-of-range indices are now forwarded to
scrollToIndex so its range validation / onScrollToIndexFailed path runs.
Updates the existing scrollToLocation expectations to the corrected
mapping and adds regression tests for react#50143 (sticky itemIndex: 0,
later-section itemIndex: 0, and out-of-range forwarding).
Fixes react#50143
|
Hi @KAMRONBEK! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
Summary
SectionList.scrollToLocation({ sectionIndex, itemIndex: 0 })silently doesnothing: no scroll happens, no error is logged, and
onScrollToIndexFailednever fires.
itemIndex: 1(and higher) works. This is most visible on iOS,where
stickySectionHeadersEnableddefaults totrue. Fixes #50143.Root cause
VirtualizedSectionListflattens all sections into a singleVirtualizedList.Every section contributes a header cell and a footer cell around its items, so
the flat-index layout for a section starting at flat offset
baseis:(This layout is authoritatively defined by
_subExtractor, which subtracts 1for the header when resolving an item's relative index.)
Before this change,
scrollToLocationmapped{ sectionIndex, itemIndex }to aflat index without accounting for the current section's header:
So the mapping was off by one:
itemIndex: 0resolved tobase→ the section header, not the first row.itemIndex: 1resolved tobase + 1→ the first row (relative item 0).itemIndex: 0therefore targeted the header. With sticky headers enabled theheader is already pinned at the top of the viewport, so scrolling to its offset
lands where the viewport already is — a visually imperceptible no-op. And because
that header cell is typically already measured,
scrollToIndexdoes not take itsonScrollToIndexFailedbranch — hence "no scroll, no error, no callback", exactlyas reported. The sticky-header compensation block made the same off-by-one
assumption and was additionally gated on
itemIndex > 0, so it skippeditemIndex: 0entirely.The public API documents
scrollToLocationas scrolling to "the item at thespecified sectionIndex and itemIndex" (
SectionList.d.ts), soitemIndex: 0should target the first item, not the header.
Root cause:
packages/virtualized-lists/Lists/VirtualizedSectionList.js,scrollToLocation(thelet index = params.itemIndexseed and the sticky block).The fix
+ 1when seeding the flat index soitemIndex: 0maps to the first row(
base + 1) rather than the section header.itemIndex >= 0(was> 0)so
itemIndex: 0also lands below the pinned header; the header's flat indexis correspondingly
index - params.itemIndex - 1.Out-of-range indices are now forwarded to
VirtualizedList.scrollToIndexinsteadof silently resolving to a header/footer cell, so its existing range validation
and
onScrollToIndexFailedpath run as documented.Behavior-change note (for reviewers)
This corrects a long-standing off-by-one dating to commit
8a82503b5(2019),which locked
{sectionIndex: 0, itemIndex: 0} -> index: 0into a regression testwith a
// prevents #18098comment. That change's actual goal (per itsdescription) was to stop
viewOffsetfrom being overridden — theindex: 0value was incidental, not the point of the test. #18098 was about
scrollToLocationoverscrolling past content on the last section; the
viewOffset-preservationguarantee that fixed it is retained here, and only the index/header mapping is
corrected. Existing
scrollToLocationtest expectations were updated to thecorrected semantics.
Changelog:
[General] [Fixed] -
SectionList.scrollToLocationnow scrolls toitemIndex: 0(the first row of a section) instead of silently no-op'ing on the sticky section
header.
Test Plan
Added/updated tests in
packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js:scrollToLocationexpectations to the corrected mapping(indices +1;
itemIndex: 0no longer resolves to the header).itemIndex: 0with sticky headers →index: 1with the header height added toviewOffset(regression test for SectionList.scrollToLocation not scrolling to section with itemIndex: 0 #50143).itemIndex: 0in a later section → that section's first row(
{sectionIndex: 1, itemIndex: 0} -> index: 6), not its header.itemIndexis still forwarded toscrollToIndex(soonScrollToIndexFailedcan fire) rather than being swallowed.Ran the suite locally (Node 24):
Confirmed the tests catch the bug: reverting the source change makes the 7
scrollToLocationexpectations fail; restoring it makes them pass.