diff --git a/.changeset/quiet-rows-wait.md b/.changeset/quiet-rows-wait.md new file mode 100644 index 000000000..14f9b9ce8 --- /dev/null +++ b/.changeset/quiet-rows-wait.md @@ -0,0 +1,5 @@ +--- +'@tanstack/virtual-core': patch +--- + +Reattach observers when a scroll element's window changes, so Solid lists created inside a suspended `` or with a signal ref render their rows after insertion. diff --git a/packages/solid-virtual/tests/suspense.test.tsx b/packages/solid-virtual/tests/suspense.test.tsx new file mode 100644 index 000000000..9df97a59b --- /dev/null +++ b/packages/solid-virtual/tests/suspense.test.tsx @@ -0,0 +1,65 @@ +import { beforeEach, expect, test } from 'vitest' +import { For, Suspense, createResource } from 'solid-js' +import { render } from 'solid-js/web' + +import { createVirtualizer } from '../src/index' + +beforeEach(() => { + Object.defineProperties(HTMLElement.prototype, { + clientHeight: { + configurable: true, + get: () => 200, + }, + }) +}) + +test('renders rows once a suspended list resolves', async () => { + let resolve!: (count: number) => void + const pending = new Promise((r) => (resolve = r)) + + function List() { + const [count] = createResource(() => pending) + let parentRef!: HTMLDivElement + + const virtualizer = createVirtualizer({ + get count() { + return count() ?? 0 + }, + getScrollElement: () => parentRef, + estimateSize: () => 50, + overscan: 0, + }) + + return ( +
+ + {(row) =>
Row {row.index}
} +
+
+ ) + } + + const container = document.createElement('div') + document.body.appendChild(container) + const dispose = render( + () => ( + + + + ), + container, + ) + + expect(container.textContent).toBe('Loading...') + + resolve(100) + await pending + await Promise.resolve() + + expect( + Array.from(container.firstElementChild!.children, (el) => el.textContent), + ).toEqual(['Row 0', 'Row 1', 'Row 2', 'Row 3']) + + dispose() + container.remove() +}) diff --git a/packages/virtual-core/src/index.ts b/packages/virtual-core/src/index.ts index b0ad7f222..9cb2304ff 100644 --- a/packages/virtual-core/src/index.ts +++ b/packages/virtual-core/src/index.ts @@ -1075,7 +1075,16 @@ export class Virtualizer< ? this.options.getScrollElement() : null - if (this.scrollElement !== scrollElement) { + // Solid can create the scroll element in a template document without a + // window while Suspense is pending (#1011). Once inserted, the element + // is the same but its window has changed, so the built-in observers + // need to be attached again. + if ( + this.scrollElement !== scrollElement || + (scrollElement && + 'ownerDocument' in scrollElement && + this.targetWindow !== scrollElement.ownerDocument.defaultView) + ) { this.cleanup() if (!scrollElement) { diff --git a/packages/virtual-core/tests/dom-observers.test.ts b/packages/virtual-core/tests/dom-observers.test.ts index 8056d57a9..11a36f200 100644 --- a/packages/virtual-core/tests/dom-observers.test.ts +++ b/packages/virtual-core/tests/dom-observers.test.ts @@ -251,6 +251,37 @@ test('cleanup resets the scroll flags on unmount', () => { expect(virtualizer.scrollDirection).toBe(null) }) +test('renders items after the scroll element is inserted from a template', () => { + // Solid clones nodes from a