From 54c772023b6b8c668fc3303e7cc1f67e26529d5a Mon Sep 17 00:00:00 2001 From: pinpong Date: Sun, 11 Oct 2026 18:00:55 +0700 Subject: [PATCH] Fix onViewableItemsChanged not firing after StrictMode/Activity remount --- .../Lists/ViewabilityHelper.js | 1 + .../Lists/__tests__/ViewabilityHelper-test.js | 82 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/packages/virtualized-lists/Lists/ViewabilityHelper.js b/packages/virtualized-lists/Lists/ViewabilityHelper.js index 5fc5addc11b8..35b929e5851f 100644 --- a/packages/virtualized-lists/Lists/ViewabilityHelper.js +++ b/packages/virtualized-lists/Lists/ViewabilityHelper.js @@ -99,6 +99,7 @@ class ViewabilityHelper { */ dispose() { this._timers.forEach(clearTimeout); + this.resetViewableIndices(); } /** diff --git a/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js b/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js index fb2c672826e4..a0e9051464ec 100644 --- a/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js +++ b/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js @@ -767,6 +767,88 @@ describe('onUpdate', function () { }); }); + it('minimumViewTime reports items again after dispose', function () { + const helper = new ViewabilityHelper({ + minimumViewTime: 350, + viewAreaCoveragePercentThreshold: 0, + }); + rowFrames = { + a: {y: 0, height: 200}, + b: {y: 200, height: 200}, + }; + data = [{key: 'a'}, {key: 'b'}]; + const onViewableItemsChanged = jest.fn(); + helper.onUpdate( + props, + 0, + 200, + // $FlowFixMe[incompatible-type] - Invalid `ListMetricsAggregator`. + {getCellMetrics}, + createViewToken, + onViewableItemsChanged, + ); + // Cancels the pending callback, e.g. when the list gets hidden. + helper.dispose(); + jest.runAllTimers(); + expect(onViewableItemsChanged).not.toBeCalled(); + + helper.onUpdate( + props, + 0, + 200, + // $FlowFixMe[incompatible-type] - Invalid `ListMetricsAggregator`. + {getCellMetrics}, + createViewToken, + onViewableItemsChanged, + ); + jest.runAllTimers(); + + expect(onViewableItemsChanged.mock.calls.length).toBe(1); + expect(onViewableItemsChanged.mock.calls[0][0]).toEqual({ + changed: [{isViewable: true, key: 'a'}], + viewabilityConfig: { + minimumViewTime: 350, + viewAreaCoveragePercentThreshold: 0, + }, + viewableItems: [{isViewable: true, key: 'a'}], + }); + }); + + it('does not report already viewable items again after dispose', function () { + const helper = new ViewabilityHelper({ + viewAreaCoveragePercentThreshold: 0, + }); + rowFrames = { + a: {y: 0, height: 200}, + b: {y: 200, height: 200}, + }; + data = [{key: 'a'}, {key: 'b'}]; + const onViewableItemsChanged = jest.fn(); + helper.onUpdate( + props, + 0, + 200, + // $FlowFixMe[incompatible-type] - Invalid `ListMetricsAggregator`. + {getCellMetrics}, + createViewToken, + onViewableItemsChanged, + ); + expect(onViewableItemsChanged.mock.calls.length).toBe(1); + + helper.dispose(); + helper.onUpdate( + props, + 0, + 200, + // $FlowFixMe[incompatible-type] - Invalid `ListMetricsAggregator`. + {getCellMetrics}, + createViewToken, + onViewableItemsChanged, + ); + + expect(onViewableItemsChanged.mock.calls.length).toBe(1); + }); + it('waitForInteraction blocks callback until interaction', function () { const helper = new ViewabilityHelper({ waitForInteraction: true,