Skip to content

Exclude content behind a pinned sticky header from viewability - #59200

Open
ramya-2196 wants to merge 2 commits into
react:mainfrom
ramya-2196:export-D126079197
Open

ramya-2196 wants to merge 2 commits into
react:mainfrom
ramya-2196:export-D126079197

Conversation

@ramya-2196

Copy link
Copy Markdown
Contributor

Summary:

What

onViewableItemsChanged no longer reports items hidden behind a pinned sticky header, and reports the pinned header itself while it is on screen. This applies to FlatList and VirtualizedList with stickyHeaderIndices (including a sticky ListHeaderComponent) and to SectionList with sticky section headers. JS only; no new props or public API.

  • VirtualizeUtils.js: new findPinnedStickyHeader finds the pinned header and how much of the viewport it covers.
  • ViewabilityHelper.js: computeViewableItems and onUpdate take an optional pinned header. Content behind it does not count as visible, and the header is checked where it is drawn.
  • VirtualizedList.js: works out the pinned header from the sticky header layouts and passes it on. Same change in VirtualizedList.macos.js and VirtualizedList.windows.js.

Why

Fixes the other half of #37439. A pinned sticky header covers the start of the viewport, but viewability treated the whole viewport as visible:

  • items fully or partly behind the header were reported as viewable
  • the pinned header itself dropped out as soon as its original position scrolled away, even though it is still on screen

Example: a 120px sticky ListHeaderComponent over 80px rows, scrolled to 400. Only items 5-10 are visible, but item 4, which is completely behind the header, was reported too.

How

findPinnedStickyHeader follows the positioning in ScrollViewStickyHeader: the pinned header is the last sticky header whose start the scroll offset has reached, and it covers its full length until the next sticky header pushes it out. The helper does not allocate per header.

ViewabilityHelper treats the viewport as starting where the pinned header ends. The view-area percentage still uses the whole viewport, since the header is part of it. When the pinned header is a list item, it is skipped in the normal scan and checked at its drawn position.

The viewport is unchanged in cases where we cannot tell where the header is drawn:

  • horizontal lists (ScrollView does not support sticky headers there)
  • headers that stick to the bottom (invertStickyHeaders, which defaults to inverted)
  • stickyHeaderHiddenOnScroll
  • a custom StickyHeaderComponent
  • lists nested in a list with the same orientation

For Reviewers

  • This builds on the previous diff, which records the real position of sticky header cells. The next header's position decides when the pinned one is pushed out.
  • The pinned header is now reported as viewable for as long as it is pinned. That is a visible change for apps that track section headers.
  • Inverted lists keep today's behaviour. Sticky headers there are drawn with a different formula that has open issues of its own ([SectionList][inverted] SectionList headers are sticky-at-the-top-footers if the list is inverted #18945), so it seemed wrong to build viewability on it.
  • ListHeaderComponent is assumed to start at offset 0. Its own layout is measured inside the sticky wrapper, so its reported position is not reliable. Content container padding only delays pinning, and nothing else is under the header in that range.

Changelog:
[General][Fixed] - Fix onViewableItemsChanged reporting items hidden behind a pinned sticky header, and missing the pinned header itself, in FlatList and SectionList

Differential Revision: D126079197

Summary:

### What

`VirtualizedList` now records the position of a sticky header cell from its sticky wrapper, instead of from the cell's own layout. This affects `FlatList` with `stickyHeaderIndices` and `SectionList` with sticky section headers. JS only; no new props or public API.

- `VirtualizedList.js`: sticky cells get an `onLayout` callback, and that layout takes priority over the cell's own `onCellLayout` measurement.
- `VirtualizedListCellRenderer.js`: adds the optional `onLayout` prop type.
- `VirtualizedList.macos.js`, `VirtualizedList.windows.js`, `VirtualizedListCellRenderer.macos.js`: the same change.

### Why

Fixes part of react#37439. With sticky headers, `onViewableItemsChanged` never reports the next sticky header or any item after it, even when they are fully on screen.

`ScrollView` wraps every sticky child in `ScrollViewStickyHeader`. The cell's inner `View` therefore measures its layout relative to that wrapper, so a sticky cell's offset is always about 0 instead of its position in the list. Viewability scans cells in order and stops at the first cell that is not visible after a visible one. A sticky header stored at offset 0 looks scrolled off the top, so the scan stops there and drops it and everything after it.

Example: groups of one 50px header and five 80px rows, scrolled to y=254 with header A pinned. Header B (at 450) and B1-B4 are on screen, but only `A4, A5` are reported.

### How

`ScrollViewStickyHeader` already calls the child element's `onLayout` with the wrapper's layout, which is relative to the list content. Since cells moved their layout callback to `onCellLayout`, nothing received it. `VirtualizedList` now passes `onLayout` to sticky cells and records that layout.

The cell's own measurement is still recorded until the wrapper reports, so a custom `StickyHeaderComponent` that does not forward `onLayout` behaves exactly as before. Once the wrapper has reported, later inner measurements for that cell are ignored. The flag is cleared when the cell unmounts or stops being sticky.

Alternative considered: changing `ScrollViewStickyHeader` to forward `onCellLayout` as well. Rejected because it would teach `ScrollView` about a list-specific prop.

### For Reviewers

- This does not fix the other half of react#37439: items hidden behind the pinned header are still reported as viewable. That needs a separate change in `ViewabilityHelper`.
- Sticky cell offsets also feed render windowing, `scrollToIndex`, and fill-rate tracking, so those now use the correct positions too.

Changelog:
[General][Fixed] - Fix `onViewableItemsChanged` skipping the next sticky header and the items after it in `FlatList` and `SectionList`

Differential Revision: D126073361
Summary:
### What

`onViewableItemsChanged` no longer reports items hidden behind a pinned sticky header, and reports the pinned header itself while it is on screen. This applies to `FlatList` and `VirtualizedList` with `stickyHeaderIndices` (including a sticky `ListHeaderComponent`) and to `SectionList` with sticky section headers. JS only; no new props or public API.

- `VirtualizeUtils.js`: new `findPinnedStickyHeader` finds the pinned header and how much of the viewport it covers.
- `ViewabilityHelper.js`: `computeViewableItems` and `onUpdate` take an optional pinned header. Content behind it does not count as visible, and the header is checked where it is drawn.
- `VirtualizedList.js`: works out the pinned header from the sticky header layouts and passes it on. Same change in `VirtualizedList.macos.js` and `VirtualizedList.windows.js`.

### Why

Fixes the other half of react#37439. A pinned sticky header covers the start of the viewport, but viewability treated the whole viewport as visible:

- items fully or partly behind the header were reported as viewable
- the pinned header itself dropped out as soon as its original position scrolled away, even though it is still on screen

Example: a 120px sticky `ListHeaderComponent` over 80px rows, scrolled to 400. Only items 5-10 are visible, but item 4, which is completely behind the header, was reported too.

### How

`findPinnedStickyHeader` follows the positioning in `ScrollViewStickyHeader`: the pinned header is the last sticky header whose start the scroll offset has reached, and it covers its full length until the next sticky header pushes it out. The helper does not allocate per header.

`ViewabilityHelper` treats the viewport as starting where the pinned header ends. The view-area percentage still uses the whole viewport, since the header is part of it. When the pinned header is a list item, it is skipped in the normal scan and checked at its drawn position.

The viewport is unchanged in cases where we cannot tell where the header is drawn:

- horizontal lists (`ScrollView` does not support sticky headers there)
- headers that stick to the bottom (`invertStickyHeaders`, which defaults to `inverted`)
- `stickyHeaderHiddenOnScroll`
- a custom `StickyHeaderComponent`
- lists nested in a list with the same orientation

### For Reviewers

- This builds on the previous diff, which records the real position of sticky header cells. The next header's position decides when the pinned one is pushed out.
- The pinned header is now reported as viewable for as long as it is pinned. That is a visible change for apps that track section headers.
- Inverted lists keep today's behaviour. Sticky headers there are drawn with a different formula that has open issues of its own (react#18945), so it seemed wrong to build viewability on it.
- `ListHeaderComponent` is assumed to start at offset 0. Its own layout is measured inside the sticky wrapper, so its reported position is not reliable. Content container padding only delays pinning, and nothing else is under the header in that range.

Changelog:
[General][Fixed] - Fix `onViewableItemsChanged` reporting items hidden behind a pinned sticky header, and missing the pinned header itself, in `FlatList` and `SectionList`

Differential Revision: D126079197
@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 19, 2026
@meta-codesync

meta-codesync Bot commented Oct 19, 2026

Copy link
Copy Markdown

@ramya-2196 has exported this pull request. If you are a Meta employee, you can view the originating Diff in D126079197.

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. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant