From 8180d13802a865262621db85de3ae44f9b45b32e Mon Sep 17 00:00:00 2001 From: yj-z <1368334952@qq.com> Date: Fri, 24 Jul 2026 12:04:05 +0800 Subject: [PATCH 1/4] fix(cli): collapse bottom-stuck virtualized list viewport --- .../shared/VirtualizedList.test.tsx | 38 +++++++++++++++ .../ui/components/shared/VirtualizedList.tsx | 47 +++++++++++++++++-- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx index af1ec8df589..e46607efd9f 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx @@ -92,6 +92,44 @@ describe('', () => { expect(listRef!.getScrollIndex()).toBe(19); }); + it('collapses bottom-stuck viewport to measured rows when estimates are too tall', async () => { + const tallEstimate = () => 4; + + const { lastFrame, rerender } = render( + + data={makeItems(20)} + renderItem={renderItem} + estimatedItemHeight={tallEstimate} + keyExtractor={keyExtractor} + initialScrollIndex={SCROLL_TO_ITEM_END} + containerHeight={20} + width={40} + showScrollbar={false} + />, + ); + + for (let i = 0; i < 10; i++) { + rerender( + + data={makeItems(20)} + renderItem={renderItem} + estimatedItemHeight={tallEstimate} + keyExtractor={keyExtractor} + initialScrollIndex={SCROLL_TO_ITEM_END} + containerHeight={20} + width={40} + showScrollbar={false} + />, + ); + await act(async () => {}); + } + + const frame = lastFrame() ?? ''; + expect(frame).toContain('item-0'); + expect(frame).toContain('item-19'); + expect(frame.endsWith('item-19')).toBe(true); + }); + it('targetScrollIndex anchors to that index on first usable render', () => { type RefShape = VirtualizedListRef; let listRef: RefShape | null = null; diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.tsx index da391e375cc..1a5c825654a 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.tsx @@ -490,9 +490,28 @@ function VirtualizedList( endIndexOffsetRaw >= offsets.length ? data.length - 1 : Math.min(data.length - 1, endIndexOffsetRaw); + const renderStartIndex = (() => { + if (!isStickingToBottom || renderStatic === true || endIndex < 0) { + return startIndex; + } + + let heightFromEnd = 0; + for (let i = endIndex; i >= 0; i--) { + const item = data[i]; + const key = item ? keyExtractor(item, i) : ''; + const raw = heights[key] ?? estimatedItemHeight(i); + const height = Number.isFinite(raw) && raw > 0 ? raw : 0; + heightFromEnd += height; + if (heightFromEnd >= viewHeightForEndIndex) { + return Math.min(startIndex, Math.max(0, i - 1)); + } + } + + return 0; + })(); const topSpacerHeight = - renderStatic === true ? 0 : (offsets[startIndex] ?? 0); + renderStatic === true ? 0 : (offsets[renderStartIndex] ?? 0); const bottomSpacerHeight = renderStatic ? 0 : totalHeight - (offsets[endIndex + 1] ?? totalHeight); @@ -512,8 +531,24 @@ function VirtualizedList( ); } - const renderRangeStart = renderStatic ? 0 : startIndex; + const renderRangeStart = renderStatic ? 0 : renderStartIndex; const renderRangeEnd = renderStatic ? data.length - 1 : endIndex; + const measuredRenderedHeight = (() => { + if (!isStickingToBottom || renderStatic === true || renderRangeEnd < 0) { + return undefined; + } + + let total = 0; + for (let i = renderRangeStart; i <= renderRangeEnd; i++) { + const item = data[i]; + if (!item) return undefined; + const measured = heights[keyExtractor(item, i)]; + if (measured === undefined) return undefined; + total += measured; + } + + return total; + })(); const renderedItems = useMemo(() => { if (!isReady) { @@ -883,7 +918,13 @@ function VirtualizedList( // the full `containerHeight`, so scrolling is unaffected. const rootHeight = props.containerHeight !== undefined - ? Math.min(props.containerHeight, totalHeight) + ? Math.min( + props.containerHeight, + measuredRenderedHeight !== undefined && + measuredRenderedHeight < props.containerHeight + ? measuredRenderedHeight + : totalHeight, + ) : '100%'; return ( From 30c5a9959e708afdc4b5ad33c7a34ead18e753bd Mon Sep 17 00:00:00 2001 From: yj-z <1368334952@qq.com> Date: Fri, 24 Jul 2026 16:56:47 +0800 Subject: [PATCH 2/4] fix(cli): address virtualized list review feedback --- .../components/shared/VirtualizedList.test.tsx | 2 +- .../ui/components/shared/VirtualizedList.tsx | 17 ++++------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx index e46607efd9f..fb33319be4a 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx @@ -108,7 +108,7 @@ describe('', () => { />, ); - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 2; i++) { rerender( data={makeItems(20)} diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.tsx index 1a5c825654a..145e037d0b2 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.tsx @@ -495,19 +495,10 @@ function VirtualizedList( return startIndex; } - let heightFromEnd = 0; - for (let i = endIndex; i >= 0; i--) { - const item = data[i]; - const key = item ? keyExtractor(item, i) : ''; - const raw = heights[key] ?? estimatedItemHeight(i); - const height = Number.isFinite(raw) && raw > 0 ? raw : 0; - heightFromEnd += height; - if (heightFromEnd >= viewHeightForEndIndex) { - return Math.min(startIndex, Math.max(0, i - 1)); - } - } - - return 0; + const targetOffset = + (offsets[endIndex + 1] ?? totalHeight) - viewHeightForEndIndex; + const backStart = findLastLE(offsets, targetOffset); + return Math.min(startIndex, Math.max(0, backStart)); })(); const topSpacerHeight = From 018707edcc17bac69e75fc95487365a09ae90201 Mon Sep 17 00:00:00 2001 From: yj-z <1368334952@qq.com> Date: Fri, 24 Jul 2026 20:41:33 +0800 Subject: [PATCH 3/4] test(cli): cover short virtualized list collapse --- .../shared/VirtualizedList.test.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx index fb33319be4a..ecfc19359ee 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx @@ -130,6 +130,44 @@ describe('', () => { expect(frame.endsWith('item-19')).toBe(true); }); + it('collapses a short bottom-stuck list below the container height', async () => { + const { lastFrame, rerender } = render( + + data={makeItems(5)} + renderItem={renderItem} + estimatedItemHeight={estimatedItemHeight} + keyExtractor={keyExtractor} + initialScrollIndex={SCROLL_TO_ITEM_END} + containerHeight={20} + width={40} + showScrollbar={false} + />, + ); + + rerender( + + data={makeItems(5)} + renderItem={renderItem} + estimatedItemHeight={estimatedItemHeight} + keyExtractor={keyExtractor} + initialScrollIndex={SCROLL_TO_ITEM_END} + containerHeight={20} + width={40} + showScrollbar={false} + />, + ); + await act(async () => {}); + + const frame = lastFrame() ?? ''; + expect(frame.split('\n')).toEqual([ + 'item-0', + 'item-1', + 'item-2', + 'item-3', + 'item-4', + ]); + }); + it('targetScrollIndex anchors to that index on first usable render', () => { type RefShape = VirtualizedListRef; let listRef: RefShape | null = null; From aeba8847de5cb11eb45dfa49bbf39460eab3e58e Mon Sep 17 00:00:00 2001 From: yj-z <1368334952@qq.com> Date: Sun, 26 Jul 2026 00:22:24 +0800 Subject: [PATCH 4/4] test(cli): keep virtualized list coverage only --- .../ui/components/shared/VirtualizedList.tsx | 38 ++----------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.tsx index 145e037d0b2..da391e375cc 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.tsx @@ -490,19 +490,9 @@ function VirtualizedList( endIndexOffsetRaw >= offsets.length ? data.length - 1 : Math.min(data.length - 1, endIndexOffsetRaw); - const renderStartIndex = (() => { - if (!isStickingToBottom || renderStatic === true || endIndex < 0) { - return startIndex; - } - - const targetOffset = - (offsets[endIndex + 1] ?? totalHeight) - viewHeightForEndIndex; - const backStart = findLastLE(offsets, targetOffset); - return Math.min(startIndex, Math.max(0, backStart)); - })(); const topSpacerHeight = - renderStatic === true ? 0 : (offsets[renderStartIndex] ?? 0); + renderStatic === true ? 0 : (offsets[startIndex] ?? 0); const bottomSpacerHeight = renderStatic ? 0 : totalHeight - (offsets[endIndex + 1] ?? totalHeight); @@ -522,24 +512,8 @@ function VirtualizedList( ); } - const renderRangeStart = renderStatic ? 0 : renderStartIndex; + const renderRangeStart = renderStatic ? 0 : startIndex; const renderRangeEnd = renderStatic ? data.length - 1 : endIndex; - const measuredRenderedHeight = (() => { - if (!isStickingToBottom || renderStatic === true || renderRangeEnd < 0) { - return undefined; - } - - let total = 0; - for (let i = renderRangeStart; i <= renderRangeEnd; i++) { - const item = data[i]; - if (!item) return undefined; - const measured = heights[keyExtractor(item, i)]; - if (measured === undefined) return undefined; - total += measured; - } - - return total; - })(); const renderedItems = useMemo(() => { if (!isReady) { @@ -909,13 +883,7 @@ function VirtualizedList( // the full `containerHeight`, so scrolling is unaffected. const rootHeight = props.containerHeight !== undefined - ? Math.min( - props.containerHeight, - measuredRenderedHeight !== undefined && - measuredRenderedHeight < props.containerHeight - ? measuredRenderedHeight - : totalHeight, - ) + ? Math.min(props.containerHeight, totalHeight) : '100%'; return (