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
5 changes: 5 additions & 0 deletions .changeset/quiet-tables-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-virtual': patch
---

Detach scroll observers when the scroll element becomes null.
4 changes: 3 additions & 1 deletion packages/vue-virtual/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
],
"scripts": {
"clean": "premove ./dist ./coverage",
"test:eslint": "eslint ./src",
"test:eslint": "eslint ./src ./tests",
"test:types": "tsc",
"test:build": "publint --strict",
"test:lib": "vitest",
"test:lib:dev": "pnpm run test:lib --watch",
"build": "vite build"
},
"type": "module",
Expand Down
6 changes: 2 additions & 4 deletions packages/vue-virtual/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ function useVirtualizerBase<

watch(
() => unref(options).getScrollElement(),
(el) => {
if (el) {
virtualizer._willUpdate()
}
() => {
virtualizer._willUpdate()
},
{
immediate: true,
Expand Down
117 changes: 117 additions & 0 deletions packages/vue-virtual/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { effectScope, nextTick, shallowRef } from 'vue'
import { useVirtualizer, useWindowVirtualizer } from '../src'
import type { EffectScope } from 'vue'

describe('scroll element lifecycle', () => {
let scope: EffectScope

beforeEach(() => {
scope = effectScope()
})

afterEach(() => {
scope.stop()
})

function setup<T extends HTMLElement | Window>(initialElement: T | null) {
const scrollElement = shallowRef(initialElement)
const cleanupRect = vi.fn()
const cleanupOffset = vi.fn()
const options = {
count: 10,
estimateSize: () => 50,
getScrollElement: () => scrollElement.value,
observeElementRect: vi.fn(() => cleanupRect),
observeElementOffset: vi.fn(() => cleanupOffset),
scrollToFn: vi.fn(),
}

return { scrollElement, cleanupRect, cleanupOffset, options }
}

test('attaches observers when an initially null element becomes available', async () => {
const { scrollElement, options } = setup<HTMLElement>(null)
const virtualizer = scope.run(() => useVirtualizer(options))!

expect(virtualizer.value.scrollElement).toBeNull()
expect(options.observeElementRect).not.toHaveBeenCalled()
expect(options.observeElementOffset).not.toHaveBeenCalled()

scrollElement.value = document.createElement('div')
await nextTick()

expect(virtualizer.value.scrollElement).toBe(scrollElement.value)
expect(options.observeElementRect).toHaveBeenCalledTimes(1)
expect(options.observeElementOffset).toHaveBeenCalledTimes(1)
})

test('cleans up observers when the element becomes null and reattaches later', async () => {
const element = document.createElement('div')
const { scrollElement, cleanupRect, cleanupOffset, options } =
setup(element)
const virtualizer = scope.run(() => useVirtualizer(options))!

expect(virtualizer.value.scrollElement).toBe(element)

scrollElement.value = null
await nextTick()

expect(cleanupRect).toHaveBeenCalledTimes(1)
expect(cleanupOffset).toHaveBeenCalledTimes(1)
expect(virtualizer.value.scrollElement).toBeNull()
expect(virtualizer.value.targetWindow).toBeNull()

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
scrollElement.value = element
await nextTick()

expect(virtualizer.value.scrollElement).toBe(element)
expect(options.observeElementRect).toHaveBeenCalledTimes(2)
expect(options.observeElementOffset).toHaveBeenCalledTimes(2)

scope.stop()
expect(cleanupRect).toHaveBeenCalledTimes(2)
expect(cleanupOffset).toHaveBeenCalledTimes(2)
expect(virtualizer.value.scrollElement).toBeNull()
})

test('cleans up observers on element replacement and scope disposal', async () => {
const { scrollElement, cleanupRect, cleanupOffset, options } = setup(
document.createElement('div'),
)
const virtualizer = scope.run(() => useVirtualizer(options))!

scrollElement.value = document.createElement('div')
await nextTick()

expect(cleanupRect).toHaveBeenCalledTimes(1)
expect(cleanupOffset).toHaveBeenCalledTimes(1)
expect(virtualizer.value.scrollElement).toBe(scrollElement.value)
expect(options.observeElementRect).toHaveBeenCalledTimes(2)
expect(options.observeElementOffset).toHaveBeenCalledTimes(2)

scope.stop()
expect(cleanupRect).toHaveBeenCalledTimes(2)
expect(cleanupOffset).toHaveBeenCalledTimes(2)
expect(virtualizer.value.scrollElement).toBeNull()
})

test('cleans up window observers when the window getter becomes null', async () => {
const { scrollElement, cleanupRect, cleanupOffset, options } =
setup<Window>(window)
const virtualizer = scope.run(() => useWindowVirtualizer(options))!

expect(virtualizer.value.scrollElement).toBe(window)

scrollElement.value = null
await nextTick()

expect(cleanupRect).toHaveBeenCalledTimes(1)
expect(cleanupOffset).toHaveBeenCalledTimes(1)
expect(virtualizer.value.scrollElement).toBeNull()

scope.stop()
expect(cleanupRect).toHaveBeenCalledTimes(1)
expect(cleanupOffset).toHaveBeenCalledTimes(1)
})
})
2 changes: 1 addition & 1 deletion packages/vue-virtual/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "vite.config.ts"]
"include": ["src", "tests", "vite.config.ts"]
}
7 changes: 7 additions & 0 deletions packages/vue-virtual/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'
import vue from '@vitejs/plugin-vue'
import packageJson from './package.json'

const config = defineConfig({
plugins: [vue()],
test: {
name: packageJson.name,
dir: './tests',
watch: false,
environment: 'jsdom',
},
})

export default mergeConfig(
Expand Down
Loading