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 (