From 65d055b100cbf65f8b011a281c8af143d5557429 Mon Sep 17 00:00:00 2001 From: clean99 <53188310+clean99@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:53:37 +0800 Subject: [PATCH] fix(svelte-virtual): preserve onChange across partial option updates --- .changeset/tidy-scroll-callbacks.md | 5 + packages/svelte-virtual/package.json | 1 + packages/svelte-virtual/src/index.ts | 4 +- packages/svelte-virtual/tests/index.test.ts | 118 ++++++++++++++++++++ packages/svelte-virtual/tsconfig.json | 1 + packages/svelte-virtual/vite.config.ts | 4 + 6 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-scroll-callbacks.md create mode 100644 packages/svelte-virtual/tests/index.test.ts diff --git a/.changeset/tidy-scroll-callbacks.md b/.changeset/tidy-scroll-callbacks.md new file mode 100644 index 000000000..239885dcd --- /dev/null +++ b/.changeset/tidy-scroll-callbacks.md @@ -0,0 +1,5 @@ +--- +'@tanstack/svelte-virtual': patch +--- + +Preserve the `onChange` callback when partial `setOptions` updates omit it. diff --git a/packages/svelte-virtual/package.json b/packages/svelte-virtual/package.json index 821427816..a690f45a8 100644 --- a/packages/svelte-virtual/package.json +++ b/packages/svelte-virtual/package.json @@ -28,6 +28,7 @@ "test:eslint": "eslint ./src", "test:types": "tsc", "test:build": "publint --strict", + "test:lib": "vitest", "build": "svelte-package --input ./src --output ./dist" }, "type": "module", diff --git a/packages/svelte-virtual/src/index.ts b/packages/svelte-virtual/src/index.ts index 1607d8c2c..18b1a62f0 100644 --- a/packages/svelte-virtual/src/index.ts +++ b/packages/svelte-virtual/src/index.ts @@ -30,6 +30,7 @@ function createVirtualizerBase< ): Readable> { const virtualizer = new Virtualizer(initialOptions) const originalSetOptions = virtualizer.setOptions + let onChange = initialOptions.onChange // eslint-disable-next-line prefer-const let virtualizerWritable: Writable> @@ -39,9 +40,10 @@ function createVirtualizerBase< ) => { const resolvedOptions = { ...virtualizer.options, + onChange, ...options, - onChange: options.onChange, } + onChange = resolvedOptions.onChange originalSetOptions({ ...resolvedOptions, onChange: ( diff --git a/packages/svelte-virtual/tests/index.test.ts b/packages/svelte-virtual/tests/index.test.ts new file mode 100644 index 000000000..91ebc4259 --- /dev/null +++ b/packages/svelte-virtual/tests/index.test.ts @@ -0,0 +1,118 @@ +import { get } from 'svelte/store' +import { describe, expect, test, vi } from 'vitest' +import { createVirtualizer, createWindowVirtualizer } from '../src' +import type { SvelteVirtualizer } from '../src' + +type TestVirtualizer = + | SvelteVirtualizer + | SvelteVirtualizer + +describe.each([ + ['createVirtualizer', createVirtualizer], + ['createWindowVirtualizer', createWindowVirtualizer], +] as const)('%s', (_name, create) => { + test('preserves onChange across partial option updates', () => { + const onChange = vi.fn() + const subscriber = vi.fn() + const store = create({ + count: 100, + estimateSize: () => 40, + getScrollElement: () => null, + onChange, + }) + const unsubscribe = store.subscribe(subscriber) + const virtualizer = get(store) + + try { + for (const options of [{ count: 110 }, { overscan: 2 }, {}]) { + subscriber.mockClear() + virtualizer.setOptions(options) + expect(subscriber).toHaveBeenCalledOnce() + expect(virtualizer.options.count).toBe(110) + expect(virtualizer.getTotalSize()).toBe(4400) + + onChange.mockClear() + subscriber.mockClear() + virtualizer.measure() + + expect(onChange).toHaveBeenCalledExactlyOnceWith(virtualizer, false) + expect(subscriber).toHaveBeenCalledExactlyOnceWith(virtualizer) + } + } finally { + unsubscribe() + } + }) + + test('replaces onChange without retaining the previous wrapper', () => { + const onChange = vi.fn() + const nextOnChange = vi.fn() + const store = create({ + count: 100, + estimateSize: () => 40, + getScrollElement: () => null, + onChange, + }) + const unsubscribe = store.subscribe(() => {}) + const virtualizer = get(store) + + try { + virtualizer.setOptions({ onChange: nextOnChange }) + virtualizer.setOptions({ count: 110 }) + onChange.mockClear() + nextOnChange.mockClear() + virtualizer.measure() + + expect(onChange).not.toHaveBeenCalled() + expect(nextOnChange).toHaveBeenCalledExactlyOnceWith(virtualizer, false) + } finally { + unsubscribe() + } + }) + + test('can clear onChange without stopping store updates', () => { + const onChange = vi.fn() + const subscriber = vi.fn() + const store = create({ + count: 100, + estimateSize: () => 40, + getScrollElement: () => null, + onChange, + }) + const unsubscribe = store.subscribe(subscriber) + const virtualizer = get(store) + + try { + virtualizer.setOptions({ onChange: undefined }) + virtualizer.setOptions({ count: 110 }) + onChange.mockClear() + subscriber.mockClear() + virtualizer.measure() + + expect(onChange).not.toHaveBeenCalled() + expect(subscriber).toHaveBeenCalledExactlyOnceWith(virtualizer) + } finally { + unsubscribe() + } + }) + + test('updates the store when no onChange was supplied', () => { + const subscriber = vi.fn() + const store = create({ + count: 100, + estimateSize: () => 40, + getScrollElement: () => null, + }) + const unsubscribe = store.subscribe(subscriber) + const virtualizer = get(store) + + try { + virtualizer.setOptions({ count: 110 }) + subscriber.mockClear() + virtualizer.measure() + + expect(subscriber).toHaveBeenCalledExactlyOnceWith(virtualizer) + } finally { + unsubscribe() + } + }) +}) diff --git a/packages/svelte-virtual/tsconfig.json b/packages/svelte-virtual/tsconfig.json index 83321eef4..24b25061a 100644 --- a/packages/svelte-virtual/tsconfig.json +++ b/packages/svelte-virtual/tsconfig.json @@ -4,6 +4,7 @@ "src/**/*.js", "src/**/*.ts", "src/**/*.svelte", + "tests/**/*.ts", "svelte.config.js", "vite.config.ts" ] diff --git a/packages/svelte-virtual/vite.config.ts b/packages/svelte-virtual/vite.config.ts index 3d696d6ec..32f16fb77 100644 --- a/packages/svelte-virtual/vite.config.ts +++ b/packages/svelte-virtual/vite.config.ts @@ -3,4 +3,8 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ plugins: [svelte()], + test: { + dir: './tests', + watch: false, + }, })