diff --git a/__tests__/core/calculateItemsInView.test.ts b/__tests__/core/calculateItemsInView.test.ts index e55f6e84..8e8bdc11 100644 --- a/__tests__/core/calculateItemsInView.test.ts +++ b/__tests__/core/calculateItemsInView.test.ts @@ -326,6 +326,22 @@ describe("calculateItemsInView", () => { } }); + it("applies a deferred item-size MVCP correction in the next pass that repositions items", () => { + setupFixedSizeItems(30, 10); + const deferredPass = mock(() => {}); + mockState.scheduledWork.frame(deferredPass, "mvcpRecalculate"); + const prepareMVCPSpy = spyOn(mvcpModule, "prepareMVCP").mockImplementation(() => undefined); + try { + // A scroll-driven pass runs before the deferred frame. + calculateItemsInView(mockCtx, { scrollVelocity: 0 }); + + expect(prepareMVCPSpy).toHaveBeenCalledWith(mockCtx, undefined, "item-size"); + expect(mockState.scheduledWork.has("mvcpRecalculate")).toBe(false); + } finally { + prepareMVCPSpy.mockRestore(); + } + }); + it("limits cold assignments to the current buffer and header-corrected animated target union", () => { setupFixedSizeItems(120, 64); mockState.props.estimatedItemSize = 64; diff --git a/src/core/calculateItemsInView.ts b/src/core/calculateItemsInView.ts index e2e311c8..31c59964 100644 --- a/src/core/calculateItemsInView.ts +++ b/src/core/calculateItemsInView.ts @@ -345,7 +345,8 @@ export function calculateItemsInView( const stickyHeaderIndicesArr = state.props.stickyHeaderIndicesArr || []; const stickyHeaderIndicesSet = state.props.stickyHeaderIndicesSet || new Set(); const drawDistance = getEffectiveDrawDistance(ctx, params.drawDistanceMode); - const { dataChanged, doMVCP, forceFullItemPositions, mvcpAdjustmentSource } = params; + const { dataChanged, forceFullItemPositions } = params; + let { doMVCP, mvcpAdjustmentSource } = params; const bootstrapInitialScrollState = state.initialScrollSession?.kind === "bootstrap" ? state.initialScrollSession.bootstrap : undefined; const suppressInitialScrollSideEffects = !!bootstrapInitialScrollState; @@ -503,6 +504,13 @@ export function calculateItemsInView( } } + // A deferred item-size correction must land in any pass that repositions items, or they move without it. + if (!doMVCP && state.scheduledWork.has("mvcpRecalculate")) { + state.scheduledWork.cancel("mvcpRecalculate"); + doMVCP = true; + mvcpAdjustmentSource = "item-size"; + } + ////// Update item positions and do MVCP // Handle maintainVisibleContentPosition adjustment early const checkMVCP =