Skip to content

Commit d97f58c

Browse files
amroaltahmeta-codesync[bot]
authored andcommitted
Fix viewability reporting for cross-orientation nested lists (#58412)
Summary: Pull Request resolved: #58412 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 e15b667 commit d97f58c

5 files changed

Lines changed: 601 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<<590f18ec6caea33fef591da5d56419f4>>
7+
* @generated SignedSource<<0c9222928e2bb42a859e43ec4d9736c7>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -1778,6 +1778,7 @@ declare function configureNext(
17781778
declare type ContentAvailable = 1 | null | void
17791779
declare type Context = {
17801780
readonly cellKey: string | undefined
1781+
readonly getCellVisibilityByKey?: (cellKey: string) => boolean | undefined
17811782
readonly horizontal: boolean | undefined
17821783
readonly getOutermostParentListRef: () => VirtualizedList_default
17831784
readonly getScrollMetrics: () => {
@@ -1792,6 +1793,7 @@ declare type Context = {
17921793
}
17931794
readonly registerAsNestedChild: ($$PARAM_0$$: {
17941795
cellKey: string
1796+
horizontal?: boolean
17951797
ref: VirtualizedList_default
17961798
}) => void
17971799
readonly unregisterAsNestedChild: ($$PARAM_0$$: {
@@ -5505,6 +5507,7 @@ declare class ViewabilityHelper_default {
55055507
first: number
55065508
last: number
55075509
},
5510+
suppressViewableItems?: boolean,
55085511
): void
55095512
recordInteraction(): void
55105513
resetViewableIndices(): void

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

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

@@ -96,9 +98,6 @@ class ViewabilityHelper {
9698
* Cleanup, e.g. on unmount. Clears any pending timers.
9799
*/
98100
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. */
102101
this._timers.forEach(clearTimeout);
103102
}
104103

@@ -197,17 +196,33 @@ class ViewabilityHelper {
197196
last: number,
198197
...
199198
},
199+
// Suppression bypasses the normal early returns so an empty result clears
200+
// items reported before an ancestor moved off screen.
201+
suppressViewableItems?: boolean,
200202
): void {
203+
const updateGeneration = this._updateGeneration + 1;
201204
const itemCount = props.getItemCount(props.data);
202205
if (
203-
(this._config.waitForInteraction && !this._hasInteracted) ||
204-
itemCount === 0 ||
205-
!listMetrics.getCellMetrics(0, props)
206+
suppressViewableItems !== true &&
207+
this._config.waitForInteraction &&
208+
!this._hasInteracted
206209
) {
210+
this._updateGeneration = updateGeneration;
211+
this._pendingSuppressedUpdate = false;
212+
this._viewableIndices = [];
213+
return;
214+
}
215+
if (
216+
suppressViewableItems !== true &&
217+
(itemCount === 0 || !listMetrics.getCellMetrics(0, props))
218+
) {
219+
this._updateGeneration = updateGeneration;
220+
this._pendingSuppressedUpdate = false;
221+
this._viewableIndices = [];
207222
return;
208223
}
209224
let viewableIndices: Array<number> = [];
210-
if (itemCount) {
225+
if (itemCount && suppressViewableItems !== true) {
211226
viewableIndices = this.computeViewableItems(
212227
props,
213228
scrollOffset,
@@ -218,35 +233,35 @@ class ViewabilityHelper {
218233
}
219234
if (
220235
this._viewableIndices.length === viewableIndices.length &&
221-
this._viewableIndices.every((v, ii) => v === viewableIndices[ii])
236+
this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) &&
237+
(suppressViewableItems !== true ||
238+
this._viewableItems.size === 0 ||
239+
this._pendingSuppressedUpdate)
222240
) {
223241
// We might get a lot of scroll events where visibility doesn't change and we don't want to do
224242
// extra work in those cases.
225243
return;
226244
}
227245
this._viewableIndices = viewableIndices;
246+
this._updateGeneration = updateGeneration;
228247
if (this._config.minimumViewTime) {
248+
this._pendingSuppressedUpdate = suppressViewableItems === true;
229249
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. */
233250
this._timers.delete(handle);
234-
// `onUpdate` replaces the array whenever the visible set changes.
235-
if (this._viewableIndices !== viewableIndices) {
251+
if (this._updateGeneration !== updateGeneration) {
236252
return;
237253
}
254+
this._pendingSuppressedUpdate = false;
238255
this._onUpdateSync(
239256
props,
240257
viewableIndices,
241258
onViewableItemsChanged,
242259
createViewToken,
243260
);
244261
}, 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. */
248262
this._timers.add(handle);
249263
} else {
264+
this._pendingSuppressedUpdate = false;
250265
this._onUpdateSync(
251266
props,
252267
viewableIndices,
@@ -261,6 +276,8 @@ class ViewabilityHelper {
261276
*/
262277
resetViewableIndices() {
263278
this._viewableIndices = [];
279+
this._pendingSuppressedUpdate = false;
280+
this._updateGeneration++;
264281
}
265282

266283
/**

0 commit comments

Comments
 (0)