Skip to content

Commit 7dd3c0f

Browse files
amroaltahfacebook-github-bot
authored andcommitted
Fix viewability reporting for cross-orientation nested lists (#58412)
Summary: A horizontal `VirtualizedList` nested inside a vertical list could report its items through `onViewableItemsChanged` while the parent row was off screen. The existing same-orientation nesting path translates parent scroll metrics, but cross-orientation children have independent scroll axes and therefore need the visibility of their containing row as a separate signal. Expose a semantic cell-visibility query through `VirtualizedListContext` and register cross-orientation children with the key of their containing cell. Parents evaluate those cells after scroll and layout changes, but invoke children only when the ancestor suppression state transitions. Known data rows without layout metrics fail closed, eliminating transient mount reports, while unsupported structural cells preserve the previous behavior. Explicit suppression now clears previously published viewability tokens even when current cell metrics are unavailable. It also invalidates stale `minimumViewTime` work. Suppression propagates through deeper cross-orientation nesting, and parent registration is reconciled when orientation or context changes so unmount always cleans up the exact collection that was registered. The implementation is kept in sync across the main, Windows, and macOS variants. The generated public API snapshot is updated accordingly. Fixes #57797 Fixes #57778 Changelog: [General][Fixed] - Do not report viewable items for a cross-orientation nested list while its parent row is off screen Differential Revision: D117727511
1 parent b830082 commit 7dd3c0f

5 files changed

Lines changed: 444 additions & 37 deletions

File tree

‎packages/react-native/ReactNativeApi.d.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<cf6a759b9ae1a69151a61e1babd1abd3>>
7+
* @generated SignedSource<<263d6fb9708d4d343663fecd91a48a8a>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -1776,6 +1776,7 @@ declare function configureNext(
17761776
declare type ContentAvailable = 1 | null | void
17771777
declare type Context = {
17781778
readonly cellKey: string | undefined
1779+
readonly getCellVisibilityByKey?: (cellKey: string) => boolean | undefined
17791780
readonly horizontal: boolean | undefined
17801781
readonly getOutermostParentListRef: () => VirtualizedList_default
17811782
readonly getScrollMetrics: () => {
@@ -1790,6 +1791,7 @@ declare type Context = {
17901791
}
17911792
readonly registerAsNestedChild: ($$PARAM_0$$: {
17921793
cellKey: string
1794+
horizontal?: boolean
17931795
ref: VirtualizedList_default
17941796
}) => void
17951797
readonly unregisterAsNestedChild: ($$PARAM_0$$: {
@@ -5490,6 +5492,7 @@ declare class ViewabilityHelper_default {
54905492
first: number
54915493
last: number
54925494
},
5495+
suppressViewableItems?: boolean,
54935496
): void
54945497
recordInteraction(): void
54955498
resetViewableIndices(): void

‎packages/virtualized-lists/Lists/ViewabilityHelper.js‎

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ export type ViewabilityConfig = Readonly<{
8282
class ViewabilityHelper {
8383
_config: ViewabilityConfig;
8484
_hasInteracted: boolean = false;
85-
_timers: Set<number> = new Set();
85+
_timers: Set<TimeoutID> = new Set();
86+
_updateGeneration: number = 0;
8687
_viewableIndices: Array<number> = [];
8788
_viewableItems: Map<string, ViewToken> = new Map();
8889

@@ -96,9 +97,6 @@ class ViewabilityHelper {
9697
* Cleanup, e.g. on unmount. Clears any pending timers.
9798
*/
9899
dispose() {
99-
/* $FlowFixMe[incompatible-type] (>=0.63.0 site=react_native_fb) This
100-
* comment suppresses an error found when Flow v0.63 was deployed. To see
101-
* the error delete this comment and run Flow. */
102100
this._timers.forEach(clearTimeout);
103101
}
104102

@@ -197,17 +195,29 @@ class ViewabilityHelper {
197195
last: number,
198196
...
199197
},
198+
// Suppression bypasses the normal early returns so an empty result clears
199+
// items reported before an ancestor moved off screen.
200+
suppressViewableItems?: boolean,
200201
): void {
202+
const updateGeneration = this._updateGeneration + 1;
201203
const itemCount = props.getItemCount(props.data);
202204
if (
203-
(this._config.waitForInteraction && !this._hasInteracted) ||
204-
itemCount === 0 ||
205-
!listMetrics.getCellMetrics(0, props)
205+
suppressViewableItems !== true &&
206+
this._config.waitForInteraction &&
207+
!this._hasInteracted
206208
) {
209+
this._updateGeneration = updateGeneration;
210+
return;
211+
}
212+
if (
213+
suppressViewableItems !== true &&
214+
(itemCount === 0 || !listMetrics.getCellMetrics(0, props))
215+
) {
216+
this._updateGeneration = updateGeneration;
207217
return;
208218
}
209219
let viewableIndices: Array<number> = [];
210-
if (itemCount) {
220+
if (itemCount && suppressViewableItems !== true) {
211221
viewableIndices = this.computeViewableItems(
212222
props,
213223
scrollOffset,
@@ -218,21 +228,19 @@ class ViewabilityHelper {
218228
}
219229
if (
220230
this._viewableIndices.length === viewableIndices.length &&
221-
this._viewableIndices.every((v, ii) => v === viewableIndices[ii])
231+
this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) &&
232+
(suppressViewableItems !== true || this._viewableItems.size === 0)
222233
) {
223234
// We might get a lot of scroll events where visibility doesn't change and we don't want to do
224235
// extra work in those cases.
225236
return;
226237
}
227238
this._viewableIndices = viewableIndices;
239+
this._updateGeneration = updateGeneration;
228240
if (this._config.minimumViewTime) {
229241
const handle: TimeoutID = setTimeout(() => {
230-
/* $FlowFixMe[incompatible-type] (>=0.63.0 site=react_native_fb) This
231-
* comment suppresses an error found when Flow v0.63 was deployed. To
232-
* see the error delete this comment and run Flow. */
233242
this._timers.delete(handle);
234-
// `onUpdate` replaces the array whenever the visible set changes.
235-
if (this._viewableIndices !== viewableIndices) {
243+
if (this._updateGeneration !== updateGeneration) {
236244
return;
237245
}
238246
this._onUpdateSync(
@@ -242,9 +250,6 @@ class ViewabilityHelper {
242250
createViewToken,
243251
);
244252
}, this._config.minimumViewTime);
245-
/* $FlowFixMe[incompatible-type] (>=0.63.0 site=react_native_fb) This
246-
* comment suppresses an error found when Flow v0.63 was deployed. To see
247-
* the error delete this comment and run Flow. */
248253
this._timers.add(handle);
249254
} else {
250255
this._onUpdateSync(
@@ -261,6 +266,7 @@ class ViewabilityHelper {
261266
*/
262267
resetViewableIndices() {
263268
this._viewableIndices = [];
269+
this._updateGeneration++;
264270
}
265271

266272
/**

0 commit comments

Comments
 (0)