From 108e4c0b967f02418126f48eb42d5a184c4a3c5a Mon Sep 17 00:00:00 2001 From: Ishkirat-Singh Date: Wed, 11 Nov 2026 19:35:17 +0530 Subject: [PATCH] fix: seed horizontal window virtualizers from window.scrollX --- .../window-horizontal-initial-offset.md | 10 +++++++ packages/angular-virtual/src/index.ts | 24 ++++++++++------ packages/lit-virtual/src/index.ts | 6 +++- .../lit-virtual/tests/set-options.test.ts | 26 +++++++++++++++++ packages/react-virtual/src/index.tsx | 7 ++++- packages/react-virtual/tests/index.test.tsx | 28 +++++++++++++++++-- packages/solid-virtual/src/index.tsx | 6 +++- packages/svelte-virtual/src/index.ts | 7 ++++- packages/vue-virtual/src/index.ts | 26 +++++++++++------ packages/vue-virtual/tests/index.test.ts | 26 +++++++++++++++++ 10 files changed, 143 insertions(+), 23 deletions(-) create mode 100644 .changeset/window-horizontal-initial-offset.md diff --git a/.changeset/window-horizontal-initial-offset.md b/.changeset/window-horizontal-initial-offset.md new file mode 100644 index 000000000..074d55f92 --- /dev/null +++ b/.changeset/window-horizontal-initial-offset.md @@ -0,0 +1,10 @@ +--- +'@tanstack/react-virtual': patch +'@tanstack/vue-virtual': patch +'@tanstack/svelte-virtual': patch +'@tanstack/solid-virtual': patch +'@tanstack/lit-virtual': patch +'@tanstack/angular-virtual': patch +--- + +Seed a horizontal window virtualizer's default `initialOffset` from `window.scrollX` instead of `window.scrollY`, matching `observeWindowOffset` and the Marko adapter. A horizontal list mounted on a page that was scrolled down no longer renders the columns at the vertical scroll position until the first scroll event. diff --git a/packages/angular-virtual/src/index.ts b/packages/angular-virtual/src/index.ts index a841f213d..81a1f983e 100644 --- a/packages/angular-virtual/src/index.ts +++ b/packages/angular-virtual/src/index.ts @@ -216,12 +216,20 @@ export function injectWindowVirtualizer( | 'scrollToFn' >, ): AngularVirtualizer { - return injectVirtualizerBase(() => ({ - getScrollElement: () => (typeof document !== 'undefined' ? window : null), - observeElementRect: observeWindowRect, - observeElementOffset: observeWindowOffset, - scrollToFn: windowScroll, - initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0), - ...options(), - })) + return injectVirtualizerBase(() => { + const resolvedOptions = options() + return { + getScrollElement: () => (typeof document !== 'undefined' ? window : null), + observeElementRect: observeWindowRect, + observeElementOffset: observeWindowOffset, + scrollToFn: windowScroll, + initialOffset: () => + typeof document !== 'undefined' + ? resolvedOptions.horizontal + ? window.scrollX + : window.scrollY + : 0, + ...resolvedOptions, + } + }) } diff --git a/packages/lit-virtual/src/index.ts b/packages/lit-virtual/src/index.ts index 0b619c430..7136c396b 100644 --- a/packages/lit-virtual/src/index.ts +++ b/packages/lit-virtual/src/index.ts @@ -159,7 +159,11 @@ export class WindowVirtualizerController< observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => - typeof document !== 'undefined' ? window.scrollY : 0, + typeof document !== 'undefined' + ? options.horizontal + ? window.scrollX + : window.scrollY + : 0, ...options, }) } diff --git a/packages/lit-virtual/tests/set-options.test.ts b/packages/lit-virtual/tests/set-options.test.ts index 552e1deac..5fd421563 100644 --- a/packages/lit-virtual/tests/set-options.test.ts +++ b/packages/lit-virtual/tests/set-options.test.ts @@ -134,6 +134,32 @@ test('WindowVirtualizerController.setOptions keeps the window defaults', () => { controller.hostDisconnected() }) +test('WindowVirtualizerController seeds a horizontal list from window.scrollX', () => { + Object.defineProperties(window, { + scrollX: { configurable: true, value: 300 }, + scrollY: { configurable: true, value: 1000 }, + }) + const { host } = createHost() + const controller = new WindowVirtualizerController(host, { + count: 100, + estimateSize: () => 50, + horizontal: true, + }) + try { + controller.hostConnected() + + const virtualizer = controller.getVirtualizer() + expect(virtualizer.getVirtualItems()[0]?.index).toBe(5) + expect(virtualizer.scrollOffset).toBe(300) + } finally { + controller.hostDisconnected() + Object.defineProperties(window, { + scrollX: { configurable: true, value: 0 }, + scrollY: { configurable: true, value: 0 }, + }) + } +}) + @customElement('test-list-set-options') class ListSetOptions extends LitElement { @property({ type: Number }) diff --git a/packages/react-virtual/src/index.tsx b/packages/react-virtual/src/index.tsx index e4082c0f5..54a828a7c 100644 --- a/packages/react-virtual/src/index.tsx +++ b/packages/react-virtual/src/index.tsx @@ -314,7 +314,12 @@ export function useWindowVirtualizer( observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, - initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0), + initialOffset: () => + typeof document !== 'undefined' + ? options.horizontal + ? window.scrollX + : window.scrollY + : 0, ...options, }) } diff --git a/packages/react-virtual/tests/index.test.tsx b/packages/react-virtual/tests/index.test.tsx index d2a3caecd..71b790b7a 100644 --- a/packages/react-virtual/tests/index.test.tsx +++ b/packages/react-virtual/tests/index.test.tsx @@ -1,8 +1,8 @@ import { beforeEach, test, expect, vi } from 'vitest' import * as React from 'react' -import { act, render, screen } from '@testing-library/react' +import { act, render, renderHook, screen } from '@testing-library/react' -import { useVirtualizer, Range } from '../src/index' +import { useVirtualizer, useWindowVirtualizer, Range } from '../src/index' import type { ReactVirtualizer } from '../src/index' beforeEach(() => { @@ -386,3 +386,27 @@ test('directDomUpdates positions rows a child mounts together with the container transform: 'translate3d(0, 150px, 0)', }) }) + +test('useWindowVirtualizer seeds a horizontal list from window.scrollX', () => { + Object.defineProperties(window, { + scrollX: { configurable: true, value: 300 }, + scrollY: { configurable: true, value: 1000 }, + }) + try { + const { result } = renderHook(() => + useWindowVirtualizer({ + count: 100, + estimateSize: () => 50, + horizontal: true, + }), + ) + + expect(result.current.getVirtualItems()[0]?.index).toBe(5) + expect(result.current.scrollOffset).toBe(300) + } finally { + Object.defineProperties(window, { + scrollX: { configurable: true, value: 0 }, + scrollY: { configurable: true, value: 0 }, + }) + } +}) diff --git a/packages/solid-virtual/src/index.tsx b/packages/solid-virtual/src/index.tsx index 9f16672aa..bdf537b04 100644 --- a/packages/solid-virtual/src/index.tsx +++ b/packages/solid-virtual/src/index.tsx @@ -128,7 +128,11 @@ export function createWindowVirtualizer( observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => - typeof document !== 'undefined' ? window.scrollY : 0, + typeof document !== 'undefined' + ? options.horizontal + ? window.scrollX + : window.scrollY + : 0, }, options, ), diff --git a/packages/svelte-virtual/src/index.ts b/packages/svelte-virtual/src/index.ts index 49eb96cef..4cad45b43 100644 --- a/packages/svelte-virtual/src/index.ts +++ b/packages/svelte-virtual/src/index.ts @@ -103,7 +103,12 @@ export function createWindowVirtualizer( observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, - initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0), + initialOffset: () => + typeof document !== 'undefined' + ? options.horizontal + ? window.scrollX + : window.scrollY + : 0, ...options, }) } diff --git a/packages/vue-virtual/src/index.ts b/packages/vue-virtual/src/index.ts index 8fbd418cc..40f4d3e0f 100644 --- a/packages/vue-virtual/src/index.ts +++ b/packages/vue-virtual/src/index.ts @@ -100,14 +100,22 @@ export function useWindowVirtualizer( >, ): Ref> { return useVirtualizerBase( - computed(() => ({ - getScrollElement: () => (typeof document !== 'undefined' ? window : null), - observeElementRect: observeWindowRect, - observeElementOffset: observeWindowOffset, - scrollToFn: windowScroll, - initialOffset: () => - typeof document !== 'undefined' ? window.scrollY : 0, - ...unref(options), - })), + computed(() => { + const resolvedOptions = unref(options) + return { + getScrollElement: () => + typeof document !== 'undefined' ? window : null, + observeElementRect: observeWindowRect, + observeElementOffset: observeWindowOffset, + scrollToFn: windowScroll, + initialOffset: () => + typeof document !== 'undefined' + ? resolvedOptions.horizontal + ? window.scrollX + : window.scrollY + : 0, + ...resolvedOptions, + } + }), ) } diff --git a/packages/vue-virtual/tests/index.test.ts b/packages/vue-virtual/tests/index.test.ts index 3fb223527..01d369ee1 100644 --- a/packages/vue-virtual/tests/index.test.ts +++ b/packages/vue-virtual/tests/index.test.ts @@ -115,3 +115,29 @@ describe('scroll element lifecycle', () => { expect(cleanupOffset).toHaveBeenCalledTimes(1) }) }) + +test('useWindowVirtualizer seeds a horizontal list from window.scrollX', () => { + Object.defineProperties(window, { + scrollX: { configurable: true, value: 300 }, + scrollY: { configurable: true, value: 1000 }, + }) + const scope = effectScope() + try { + const virtualizer = scope.run(() => + useWindowVirtualizer({ + count: 100, + estimateSize: () => 50, + horizontal: true, + }), + )! + + expect(virtualizer.value.getVirtualItems()[0]?.index).toBe(5) + expect(virtualizer.value.scrollOffset).toBe(300) + } finally { + scope.stop() + Object.defineProperties(window, { + scrollX: { configurable: true, value: 0 }, + scrollY: { configurable: true, value: 0 }, + }) + } +})