Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/quiet-rows-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/virtual-core': patch
---

Reattach observers when a scroll element's window changes, so Solid lists created inside a suspended `<Suspense>` or with a signal ref render their rows after insertion.
65 changes: 65 additions & 0 deletions packages/solid-virtual/tests/suspense.test.tsx
Original file line number Diff line number Diff line change
@@ -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<number>((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 (
<div ref={parentRef}>
<For each={virtualizer.getVirtualItems()}>
{(row) => <div>Row {row.index}</div>}
</For>
</div>
)
}

const container = document.createElement('div')
document.body.appendChild(container)
const dispose = render(
() => (
<Suspense fallback="Loading...">
<List />
</Suspense>
),
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()
})
11 changes: 10 additions & 1 deletion packages/virtual-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
this.cleanup()

if (!scrollElement) {
Expand Down
31 changes: 31 additions & 0 deletions packages/virtual-core/tests/dom-observers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <template>; a suspended <Suspense> keeps them
// in the template's document, which has no window (#1011).
const template = document.createElement('template')
template.innerHTML = '<div></div>'
const el = template.content.firstElementChild!.cloneNode() as HTMLDivElement
Object.defineProperties(el, {
clientWidth: { value: 400 },
clientHeight: { value: 600 },
})

const virtualizer = new Virtualizer({
count: 100,
estimateSize: () => 50,
getScrollElement: () => el,
scrollToFn: elementScroll,
observeElementRect,
observeElementOffset,
})

virtualizer._willUpdate()
expect(virtualizer.getVirtualItems()).toHaveLength(0)

document.body.appendChild(el)
virtualizer._willUpdate()

expect(virtualizer.getVirtualItems()).toHaveLength(13)
virtualizer._didMount()()
el.remove()
})

test('observeElementOffset: cleanup drops the queued isScrolling reset', () => {
vi.useFakeTimers()
try {
Expand Down