From 99c4d846013d6428410aadb9e7e874159b15f5b6 Mon Sep 17 00:00:00 2001 From: Markus Obelitz Date: Sat, 3 Oct 2026 23:12:48 +0200 Subject: [PATCH] fix(layout): place the next grid row below the row's tallest item A stale minHeight left over from previous data could exclude the row's real tallest item from the tallest check. The uneven-row branch then reset the min heights but returned the shorter item, so the next row started too high and overlapped the taller items, and nothing corrected it afterwards. Fixes #2519 --- src/__tests__/GridLayoutManager.test.ts | 30 +++++++++++++++++++ .../layout-managers/GridLayoutManager.ts | 12 ++++++++ 2 files changed, 42 insertions(+) diff --git a/src/__tests__/GridLayoutManager.test.ts b/src/__tests__/GridLayoutManager.test.ts index 15daa1d4b..715639c6e 100644 --- a/src/__tests__/GridLayoutManager.test.ts +++ b/src/__tests__/GridLayoutManager.test.ts @@ -1,4 +1,6 @@ import { + createLayoutManager, + createMockLayoutInfo, createPopulatedLayoutManager, getAllLayouts, LayoutManagerType, @@ -109,5 +111,33 @@ describe("GridLayoutManager", () => { expect(updatedLayouts[0]).toEqual(initialLayouts[0]); expect(updatedLayouts[3].y).toBe(initialLayouts[0].height); }); + + it("should place the next row below the tallest item after data changes", () => { + const manager = createLayoutManager(LayoutManagerType.GRID, { + ...defaultParams, + windowSize: { width: 300, height: 900 }, + maxColumns: 3, + }); + const measure = (heights: number[]) => + manager.modifyLayout( + heights.map((height, index) => + createMockLayoutInfo(index, 100, height) + ), + heights.length + ); + + // The middle item is tallest, so its neighbours get its height as minHeight. + measure([408, 437, 408, 400, 400, 400]); + // New data at the same indices: tall, short, tall. The outer items now + // measure exactly their stale minHeight. + measure([437, 408, 437, 400, 400, 400]); + + const layouts = getAllLayouts(manager); + expect(layouts[3].y).toBe(437); + + // Measuring the same sizes again keeps it there. + measure([437, 408, 437, 400, 400, 400]); + expect(getAllLayouts(manager)[3].y).toBe(437); + }); }); }); diff --git a/src/recyclerview/layout-managers/GridLayoutManager.ts b/src/recyclerview/layout-managers/GridLayoutManager.ts index c9517568c..31fa26280 100644 --- a/src/recyclerview/layout-managers/GridLayoutManager.ts +++ b/src/recyclerview/layout-managers/GridLayoutManager.ts @@ -183,6 +183,18 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager { if (maxHeight - tallestItem.height > 1) { targetHeight = 0; this.requiresRepaint = true; + // A stale minHeight (e.g. after data changed) can hide the row's real + // tallest item from the check above. The next row has to start below + // it, or it overlaps the taller items in this row. + for ( + let j = startIndex; + j <= endIndex && j < this.layouts.length; + j++ + ) { + if (this.layouts[j].height > tallestItem.height) { + tallestItem = this.layouts[j]; + } + } } i = startIndex; while (i <= endIndex) {