Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@opentui/core": "0.4.3",
"@opentui/keymap": "0.4.3",
"@opentui/solid": "0.4.3",
"@tanstack/solid-virtual": "3.13.28",
"@tanstack/solid-virtual": "3.13.32",
"@shikijs/stream": "4.2.0",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
Expand Down Expand Up @@ -152,10 +152,9 @@
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch"
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
}
}
59 changes: 43 additions & 16 deletions packages/app/test-browser/solid-virtual.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import { expect, test } from "bun:test"
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
import { createVirtualizer, defaultRangeExtractor, Virtualizer } from "@tanstack/solid-virtual"
import { createRoot, createSignal } from "solid-js"
import { filterVirtualIndexes } from "@/pages/session/timeline/virtual-items"

test("end anchoring survives consecutive resizes when the first scroll write is clamped", () => {
const writes: { offset: number; adjustments?: number }[] = []
const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>({
count: 5,
estimateSize: () => 50,
initialOffset: 50,
initialRect: { width: 400, height: 200 },
anchorTo: "end",
scrollEndThreshold: 1,
getScrollElement: () => null,
scrollToFn: (offset, options) => writes.push({ offset, adjustments: options.adjustments }),
observeElementRect: () => {},
observeElementOffset: () => {},
})

virtualizer.getTotalSize()
virtualizer.resizeItem(4, 120)
expect(writes).toEqual([{ offset: 50, adjustments: 70 }])
writes.length = 0

virtualizer.resizeItem(4, 200)
expect(writes).toEqual([{ offset: 120, adjustments: 80 }])
})

test("reactive count updates preserve measured row sizes", () => {
createRoot((dispose) => {
const [count, setCount] = createSignal(2)
Expand Down Expand Up @@ -42,23 +66,26 @@ test("initial rect projects rows before a scroll element connects", () => {
})
})

test("logical scroll offset includes pending measurement adjustments", () => {
createRoot((dispose) => {
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
count: 2,
getScrollElement: () => null,
estimateSize: () => 60,
initialOffset: 100,
initialRect: { width: 800, height: 60 },
})
test("clamps oversized offsets with scroll margin and padding changes", () => {
const options = (paddingEnd: number) => ({
count: 20,
estimateSize: () => 60,
initialOffset: Number.MAX_SAFE_INTEGER,
initialRect: { width: 800, height: 600 },
scrollMargin: 64,
paddingEnd,
overscan: 1,
getScrollElement: () => null,
scrollToFn: () => {},
observeElementRect: () => {},
observeElementOffset: () => {},
})
const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>(options(64))

virtualizer.getTotalSize()
virtualizer.resizeItem(0, 100)
expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])

expect(virtualizer.scrollOffset).toBe(100)
expect(virtualizer.getLogicalScrollOffset()).toBe(140)
dispose()
})
virtualizer.setOptions(options(600))
expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([18, 19])
})

test("stale pinned indexes do not produce missing virtual items after count shrinks", () => {
Expand Down
45 changes: 0 additions & 45 deletions patches/@tanstack%2Fsolid-virtual@3.13.28.patch

This file was deleted.

105 changes: 0 additions & 105 deletions patches/@tanstack%2Fvirtual-core@3.17.0.patch

This file was deleted.

108 changes: 108 additions & 0 deletions patches/@tanstack%2Fvirtual-core@3.17.3.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
index 52ae6ca12f8d1c650ee7f1bd55573ee7d4f8b65f..830f646f5bd80f4e00f442291ad6e4c6c508630d 100644
--- a/dist/cjs/index.cjs
+++ b/dist/cjs/index.cjs
@@ -716,17 +716,20 @@ class Virtualizer {
this.getMeasurements(),
this.getSize(),
this.getScrollOffset(),
- this.options.lanes
+ this.options.lanes,
+ this.options.paddingEnd
],
- (measurements, outerSize, scrollOffset, lanes) => {
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
if (measurements.length === 0 || outerSize === 0) {
this.range = null;
return null;
}
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
this.range = calculateRangeImpl(
measurements,
outerSize,
- scrollOffset,
+ effectiveScrollOffset,
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
diff --git a/dist/cjs/index.d.cts b/dist/cjs/index.d.cts
index c61ee17752565253f795c7fc7d57e86237ecbb52..430c622a18c69407df9f1ca82ee9440e9be287b7 100644
--- a/dist/cjs/index.d.cts
+++ b/dist/cjs/index.d.cts
@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
startIndex: number;
endIndex: number;
} | null;
- updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
+ updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
};
getVirtualIndexes: {
(): number[];
diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts
index b03abab604eb6578f6f56ff92c489259cfaf8f19..940310f9215ba5bf666c5b71e92dddbbbc85635d 100644
--- a/dist/esm/index.d.ts
+++ b/dist/esm/index.d.ts
@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
startIndex: number;
endIndex: number;
} | null;
- updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
+ updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
};
getVirtualIndexes: {
(): number[];
diff --git a/dist/esm/index.js b/dist/esm/index.js
index 3032c0ca457582be3f47923cba1f7d92c848745c..6e52124048fb3bc7b7ee37e763354b33593a6285 100644
--- a/dist/esm/index.js
+++ b/dist/esm/index.js
@@ -714,17 +714,20 @@ class Virtualizer {
this.getMeasurements(),
this.getSize(),
this.getScrollOffset(),
- this.options.lanes
+ this.options.lanes,
+ this.options.paddingEnd
],
- (measurements, outerSize, scrollOffset, lanes) => {
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
if (measurements.length === 0 || outerSize === 0) {
this.range = null;
return null;
}
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
this.range = calculateRangeImpl(
measurements,
outerSize,
- scrollOffset,
+ effectiveScrollOffset,
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
diff --git a/src/index.ts b/src/index.ts
index 7ad79aacd591c3a4f7855c91b2727a59a9579bca..201406779ee85d2f2e5b01a8ea284aa9a5f694e2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1367,16 +1367,19 @@ export class Virtualizer<
this.getSize(),
this.getScrollOffset(),
this.options.lanes,
+ this.options.paddingEnd,
],
- (measurements, outerSize, scrollOffset, lanes) => {
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
if (measurements.length === 0 || outerSize === 0) {
this.range = null
return null
}
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0)
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset)
this.range = calculateRangeImpl(
measurements,
outerSize,
- scrollOffset,
+ effectiveScrollOffset,
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
Loading