@pyreon/virtual 0.11.5 → 0.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -19
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +2 -2
- package/package.json +16 -16
- package/src/index.ts +6 -9
- package/src/tests/virtual-comprehensive.test.tsx +57 -57
- package/src/tests/virtual.test.tsx +62 -62
- package/src/use-virtualizer.ts +6 -6
- package/src/use-window-virtualizer.ts +8 -8
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ bun add @pyreon/virtual @tanstack/virtual-core
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
import { signal } from
|
|
15
|
-
import { useVirtualizer } from
|
|
14
|
+
import { signal } from '@pyreon/reactivity'
|
|
15
|
+
import { useVirtualizer } from '@pyreon/virtual'
|
|
16
16
|
|
|
17
17
|
function VirtualList() {
|
|
18
18
|
const parentRef = signal<HTMLElement | null>(null)
|
|
@@ -48,20 +48,20 @@ function VirtualList() {
|
|
|
48
48
|
|
|
49
49
|
Create a reactive virtualizer for element-based scrolling. Options are passed as a function so reactive signals can be read inside, and the virtualizer updates automatically when they change.
|
|
50
50
|
|
|
51
|
-
| Parameter | Type
|
|
52
|
-
|
|
|
51
|
+
| Parameter | Type | Description |
|
|
52
|
+
| --------- | ----------------------------- | ------------------------------------- |
|
|
53
53
|
| `options` | `() => UseVirtualizerOptions` | Function returning virtualizer config |
|
|
54
54
|
|
|
55
55
|
Options extend `VirtualizerOptions` from `@tanstack/virtual-core` with `observeElementRect`, `observeElementOffset`, and `scrollToFn` pre-filled (overridable).
|
|
56
56
|
|
|
57
57
|
**Returns:** `UseVirtualizerResult` with:
|
|
58
58
|
|
|
59
|
-
| Property
|
|
60
|
-
|
|
|
61
|
-
| `instance`
|
|
62
|
-
| `virtualItems` | `Signal<VirtualItem[]>` | Currently visible virtual items
|
|
63
|
-
| `totalSize`
|
|
64
|
-
| `isScrolling`
|
|
59
|
+
| Property | Type | Description |
|
|
60
|
+
| -------------- | ----------------------- | -------------------------------------------- |
|
|
61
|
+
| `instance` | `Virtualizer` | The underlying TanStack Virtualizer instance |
|
|
62
|
+
| `virtualItems` | `Signal<VirtualItem[]>` | Currently visible virtual items |
|
|
63
|
+
| `totalSize` | `Signal<number>` | Total scrollable size in pixels |
|
|
64
|
+
| `isScrolling` | `Signal<boolean>` | Whether the user is currently scrolling |
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
67
|
const parentRef = signal<HTMLDivElement | null>(null)
|
|
@@ -82,8 +82,8 @@ instance.scrollToIndex(500)
|
|
|
82
82
|
|
|
83
83
|
Create a reactive virtualizer for window-based scrolling. The scroll element is automatically set to `window`. SSR-safe — checks for `window` and `document` availability.
|
|
84
84
|
|
|
85
|
-
| Parameter | Type
|
|
86
|
-
|
|
|
85
|
+
| Parameter | Type | Description |
|
|
86
|
+
| --------- | ----------------------------------- | -------------------------------------------------------- |
|
|
87
87
|
| `options` | `() => UseWindowVirtualizerOptions` | Function returning config (no `getScrollElement` needed) |
|
|
88
88
|
|
|
89
89
|
**Returns:** `UseWindowVirtualizerResult` — same shape as `UseVirtualizerResult` but typed with `Window` as the scroll element.
|
|
@@ -119,7 +119,7 @@ function WindowList() {
|
|
|
119
119
|
Use `measureElement` for variable-height items that are measured after render.
|
|
120
120
|
|
|
121
121
|
```tsx
|
|
122
|
-
import { measureElement } from
|
|
122
|
+
import { measureElement } from '@pyreon/virtual'
|
|
123
123
|
|
|
124
124
|
const { virtualItems, totalSize, instance } = useVirtualizer(() => ({
|
|
125
125
|
count: items.length,
|
|
@@ -130,11 +130,7 @@ const { virtualItems, totalSize, instance } = useVirtualizer(() => ({
|
|
|
130
130
|
|
|
131
131
|
// In render, set the ref on each item:
|
|
132
132
|
virtualItems().map((row) => (
|
|
133
|
-
<div
|
|
134
|
-
key={row.key}
|
|
135
|
-
ref={(el) => instance.measureElement(el)}
|
|
136
|
-
data-index={row.index}
|
|
137
|
-
>
|
|
133
|
+
<div key={row.key} ref={(el) => instance.measureElement(el)} data-index={row.index}>
|
|
138
134
|
{items[row.index]}
|
|
139
135
|
</div>
|
|
140
136
|
))
|
|
@@ -166,7 +162,7 @@ const { virtualItems } = useVirtualizer(() => ({
|
|
|
166
162
|
}))
|
|
167
163
|
|
|
168
164
|
// Updating filteredItems triggers recalculation
|
|
169
|
-
filteredItems.set(allItems.filter(i => i.includes(search())))
|
|
165
|
+
filteredItems.set(allItems.filter((i) => i.includes(search())))
|
|
170
166
|
```
|
|
171
167
|
|
|
172
168
|
## Re-exports from `@tanstack/virtual-core`
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["elementScroll","Virtualizer","observeWindowRect","observeWindowOffset","windowScroll","Virtualizer"],"sources":["../src/use-virtualizer.ts","../src/use-window-virtualizer.ts"],"sourcesContent":["import { onMount, onUnmount } from
|
|
1
|
+
{"version":3,"file":"index.js","names":["elementScroll","Virtualizer","observeWindowRect","observeWindowOffset","windowScroll","Virtualizer"],"sources":["../src/use-virtualizer.ts","../src/use-window-virtualizer.ts"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\nimport type { Signal } from '@pyreon/reactivity'\nimport { batch, effect, signal } from '@pyreon/reactivity'\nimport {\n elementScroll,\n observeElementOffset,\n observeElementRect,\n type VirtualItem,\n Virtualizer,\n type VirtualizerOptions,\n} from '@tanstack/virtual-core'\n\nexport type UseVirtualizerOptions<\n TScrollElement extends Element,\n TItemElement extends Element,\n> = () => Omit<\n VirtualizerOptions<TScrollElement, TItemElement>,\n 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'\n> &\n Partial<\n Pick<\n VirtualizerOptions<TScrollElement, TItemElement>,\n 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'\n >\n >\n\nexport interface UseVirtualizerResult<\n TScrollElement extends Element,\n TItemElement extends Element,\n> {\n /** The virtualizer instance — read to access all methods. */\n instance: Virtualizer<TScrollElement, TItemElement>\n /** Reactive signal of currently visible virtual items. */\n virtualItems: Signal<VirtualItem[]>\n /** Reactive signal of the total scrollable size in pixels. */\n totalSize: Signal<number>\n /** Reactive signal indicating whether the user is scrolling. */\n isScrolling: Signal<boolean>\n}\n\n/**\n * Create a reactive TanStack Virtual virtualizer for element-based scrolling.\n *\n * Options are passed as a function so reactive signals (e.g. count, estimateSize)\n * can be read inside, and the virtualizer updates automatically when they change.\n *\n * @example\n * const parentRef = signal<HTMLDivElement | null>(null)\n * const virtual = useVirtualizer(() => ({\n * count: 10000,\n * getScrollElement: () => parentRef(),\n * estimateSize: () => 35,\n * }))\n * // virtual.virtualItems() — array of visible VirtualItem\n * // virtual.totalSize() — total height/width for the inner container\n */\nexport function useVirtualizer<TScrollElement extends Element, TItemElement extends Element>(\n options: UseVirtualizerOptions<TScrollElement, TItemElement>,\n): UseVirtualizerResult<TScrollElement, TItemElement> {\n const resolvedOptions: VirtualizerOptions<TScrollElement, TItemElement> = {\n observeElementRect,\n observeElementOffset,\n scrollToFn: elementScroll,\n ...options(),\n }\n\n const virtualItems = signal<VirtualItem[]>([])\n const totalSize = signal(0)\n const isScrolling = signal(false)\n\n // Store latest user options so onChange always reads the freshest reference\n let latestUserOpts = options()\n\n const instance = new Virtualizer<TScrollElement, TItemElement>(resolvedOptions)\n\n // Track reactive options: when signals inside options() change, update the virtualizer.\n const effectCleanup = effect(() => {\n latestUserOpts = options()\n instance.setOptions({\n ...instance.options,\n ...latestUserOpts,\n onChange: (inst, sync) => {\n batch(() => {\n virtualItems.set(inst.getVirtualItems())\n totalSize.set(inst.getTotalSize())\n isScrolling.set(inst.isScrolling)\n })\n // Read latest opts to avoid stale closure\n latestUserOpts.onChange?.(inst, sync)\n },\n })\n\n // After updating options, recalculate and re-emit\n instance._willUpdate()\n batch(() => {\n virtualItems.set(instance.getVirtualItems())\n totalSize.set(instance.getTotalSize())\n })\n })\n\n // Lifecycle: mount observers, clean up on unmount.\n let mountCleanup: (() => void) | undefined\n onMount(() => {\n mountCleanup = instance._didMount()\n instance._willUpdate()\n batch(() => {\n virtualItems.set(instance.getVirtualItems())\n totalSize.set(instance.getTotalSize())\n })\n return undefined\n })\n\n onUnmount(() => {\n effectCleanup.dispose()\n mountCleanup?.()\n })\n\n return { instance, virtualItems, totalSize, isScrolling }\n}\n","import { onMount, onUnmount } from '@pyreon/core'\nimport type { Signal } from '@pyreon/reactivity'\nimport { batch, effect, signal } from '@pyreon/reactivity'\nimport {\n observeWindowOffset,\n observeWindowRect,\n type VirtualItem,\n Virtualizer,\n type VirtualizerOptions,\n windowScroll,\n} from '@tanstack/virtual-core'\n\nexport type UseWindowVirtualizerOptions<TItemElement extends Element> = () => Omit<\n VirtualizerOptions<Window, TItemElement>,\n 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' | 'getScrollElement'\n> &\n Partial<\n Pick<\n VirtualizerOptions<Window, TItemElement>,\n 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'\n >\n >\n\nexport interface UseWindowVirtualizerResult<TItemElement extends Element> {\n instance: Virtualizer<Window, TItemElement>\n virtualItems: Signal<VirtualItem[]>\n totalSize: Signal<number>\n isScrolling: Signal<boolean>\n}\n\n/**\n * Create a reactive TanStack Virtual virtualizer for window-based scrolling.\n *\n * @example\n * const virtual = useWindowVirtualizer(() => ({\n * count: 10000,\n * estimateSize: () => 35,\n * }))\n */\nexport function useWindowVirtualizer<TItemElement extends Element>(\n options: UseWindowVirtualizerOptions<TItemElement>,\n): UseWindowVirtualizerResult<TItemElement> {\n const virtualItems = signal<VirtualItem[]>([])\n const totalSize = signal(0)\n const isScrolling = signal(false)\n\n const resolvedOptions: VirtualizerOptions<Window, TItemElement> = {\n observeElementRect: observeWindowRect,\n observeElementOffset: observeWindowOffset,\n scrollToFn: windowScroll,\n initialOffset: typeof document !== 'undefined' ? window.scrollY : 0,\n getScrollElement: () => (typeof window !== 'undefined' ? window : (null as unknown as Window)),\n ...options(),\n }\n\n // Store latest user options so onChange always reads the freshest reference\n let latestUserOpts = options()\n\n const instance = new Virtualizer<Window, TItemElement>(resolvedOptions)\n\n const effectCleanup = effect(() => {\n latestUserOpts = options()\n instance.setOptions({\n ...instance.options,\n ...latestUserOpts,\n onChange: (inst, sync) => {\n batch(() => {\n virtualItems.set(inst.getVirtualItems())\n totalSize.set(inst.getTotalSize())\n isScrolling.set(inst.isScrolling)\n })\n // Read latest opts to avoid stale closure\n latestUserOpts.onChange?.(inst, sync)\n },\n })\n\n instance._willUpdate()\n batch(() => {\n virtualItems.set(instance.getVirtualItems())\n totalSize.set(instance.getTotalSize())\n })\n })\n\n let mountCleanup: (() => void) | undefined\n onMount(() => {\n mountCleanup = instance._didMount()\n instance._willUpdate()\n batch(() => {\n virtualItems.set(instance.getVirtualItems())\n totalSize.set(instance.getTotalSize())\n })\n return undefined\n })\n\n onUnmount(() => {\n effectCleanup.dispose()\n mountCleanup?.()\n })\n\n return { instance, virtualItems, totalSize, isScrolling }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,eACd,SACoD;CACpD,MAAM,kBAAoE;EACxE;EACA;EACA,YAAYA;EACZ,GAAG,SAAS;EACb;CAED,MAAM,eAAe,OAAsB,EAAE,CAAC;CAC9C,MAAM,YAAY,OAAO,EAAE;CAC3B,MAAM,cAAc,OAAO,MAAM;CAGjC,IAAI,iBAAiB,SAAS;CAE9B,MAAM,WAAW,IAAIC,cAA0C,gBAAgB;CAG/E,MAAM,gBAAgB,aAAa;AACjC,mBAAiB,SAAS;AAC1B,WAAS,WAAW;GAClB,GAAG,SAAS;GACZ,GAAG;GACH,WAAW,MAAM,SAAS;AACxB,gBAAY;AACV,kBAAa,IAAI,KAAK,iBAAiB,CAAC;AACxC,eAAU,IAAI,KAAK,cAAc,CAAC;AAClC,iBAAY,IAAI,KAAK,YAAY;MACjC;AAEF,mBAAe,WAAW,MAAM,KAAK;;GAExC,CAAC;AAGF,WAAS,aAAa;AACtB,cAAY;AACV,gBAAa,IAAI,SAAS,iBAAiB,CAAC;AAC5C,aAAU,IAAI,SAAS,cAAc,CAAC;IACtC;GACF;CAGF,IAAI;AACJ,eAAc;AACZ,iBAAe,SAAS,WAAW;AACnC,WAAS,aAAa;AACtB,cAAY;AACV,gBAAa,IAAI,SAAS,iBAAiB,CAAC;AAC5C,aAAU,IAAI,SAAS,cAAc,CAAC;IACtC;GAEF;AAEF,iBAAgB;AACd,gBAAc,SAAS;AACvB,kBAAgB;GAChB;AAEF,QAAO;EAAE;EAAU;EAAc;EAAW;EAAa;;;;;;;;;;;;;;AC9E3D,SAAgB,qBACd,SAC0C;CAC1C,MAAM,eAAe,OAAsB,EAAE,CAAC;CAC9C,MAAM,YAAY,OAAO,EAAE;CAC3B,MAAM,cAAc,OAAO,MAAM;CAEjC,MAAM,kBAA4D;EAChE,oBAAoBC;EACpB,sBAAsBC;EACtB,YAAYC;EACZ,eAAe,OAAO,aAAa,cAAc,OAAO,UAAU;EAClE,wBAAyB,OAAO,WAAW,cAAc,SAAU;EACnE,GAAG,SAAS;EACb;CAGD,IAAI,iBAAiB,SAAS;CAE9B,MAAM,WAAW,IAAIC,cAAkC,gBAAgB;CAEvE,MAAM,gBAAgB,aAAa;AACjC,mBAAiB,SAAS;AAC1B,WAAS,WAAW;GAClB,GAAG,SAAS;GACZ,GAAG;GACH,WAAW,MAAM,SAAS;AACxB,gBAAY;AACV,kBAAa,IAAI,KAAK,iBAAiB,CAAC;AACxC,eAAU,IAAI,KAAK,cAAc,CAAC;AAClC,iBAAY,IAAI,KAAK,YAAY;MACjC;AAEF,mBAAe,WAAW,MAAM,KAAK;;GAExC,CAAC;AAEF,WAAS,aAAa;AACtB,cAAY;AACV,gBAAa,IAAI,SAAS,iBAAiB,CAAC;AAC5C,aAAU,IAAI,SAAS,cAAc,CAAC;IACtC;GACF;CAEF,IAAI;AACJ,eAAc;AACZ,iBAAe,SAAS,WAAW;AACnC,WAAS,aAAa;AACtB,cAAY;AACV,gBAAa,IAAI,SAAS,iBAAiB,CAAC;AAC5C,aAAU,IAAI,SAAS,cAAc,CAAC;IACtC;GAEF;AAEF,iBAAgB;AACd,gBAAc,SAAS;AACvB,kBAAgB;GAChB;AAEF,QAAO;EAAE;EAAU;EAAc;EAAW;EAAa"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Range, Rect, ScrollToOptions, VirtualItem, VirtualItem as VirtualItem$1
|
|
|
2
2
|
import { Signal } from "@pyreon/reactivity";
|
|
3
3
|
|
|
4
4
|
//#region src/use-virtualizer.d.ts
|
|
5
|
-
type UseVirtualizerOptions<TScrollElement extends Element, TItemElement extends Element> = () => Omit<VirtualizerOptions$1<TScrollElement, TItemElement>,
|
|
5
|
+
type UseVirtualizerOptions<TScrollElement extends Element, TItemElement extends Element> = () => Omit<VirtualizerOptions$1<TScrollElement, TItemElement>, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'> & Partial<Pick<VirtualizerOptions$1<TScrollElement, TItemElement>, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'>>;
|
|
6
6
|
interface UseVirtualizerResult<TScrollElement extends Element, TItemElement extends Element> {
|
|
7
7
|
/** The virtualizer instance — read to access all methods. */
|
|
8
8
|
instance: Virtualizer$1<TScrollElement, TItemElement>;
|
|
@@ -32,7 +32,7 @@ interface UseVirtualizerResult<TScrollElement extends Element, TItemElement exte
|
|
|
32
32
|
declare function useVirtualizer<TScrollElement extends Element, TItemElement extends Element>(options: UseVirtualizerOptions<TScrollElement, TItemElement>): UseVirtualizerResult<TScrollElement, TItemElement>;
|
|
33
33
|
//#endregion
|
|
34
34
|
//#region src/use-window-virtualizer.d.ts
|
|
35
|
-
type UseWindowVirtualizerOptions<TItemElement extends Element> = () => Omit<VirtualizerOptions$1<Window, TItemElement>,
|
|
35
|
+
type UseWindowVirtualizerOptions<TItemElement extends Element> = () => Omit<VirtualizerOptions$1<Window, TItemElement>, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' | 'getScrollElement'> & Partial<Pick<VirtualizerOptions$1<Window, TItemElement>, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'>>;
|
|
36
36
|
interface UseWindowVirtualizerResult<TItemElement extends Element> {
|
|
37
37
|
instance: Virtualizer$1<Window, TItemElement>;
|
|
38
38
|
virtualItems: Signal<VirtualItem$1[]>;
|
package/package.json
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/virtual",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "Pyreon adapter for TanStack Virtual",
|
|
5
|
+
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/virtual#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"repository": {
|
|
7
11
|
"type": "git",
|
|
8
12
|
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
13
|
"directory": "packages/fundamentals/virtual"
|
|
10
14
|
},
|
|
11
|
-
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/virtual#readme",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
|
-
},
|
|
15
|
-
"publishConfig": {
|
|
16
|
-
"access": "public"
|
|
17
|
-
},
|
|
18
15
|
"files": [
|
|
19
16
|
"lib",
|
|
20
17
|
"src",
|
|
@@ -33,24 +30,27 @@
|
|
|
33
30
|
"types": "./lib/types/index.d.ts"
|
|
34
31
|
}
|
|
35
32
|
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "vl_rolldown_build",
|
|
38
38
|
"dev": "vl_rolldown_build-watch",
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
|
-
"lint": "
|
|
41
|
+
"lint": "oxlint ."
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@tanstack/virtual-core": "^3.0.0"
|
|
45
45
|
},
|
|
46
|
-
"peerDependencies": {
|
|
47
|
-
"@pyreon/core": "^0.11.5",
|
|
48
|
-
"@pyreon/reactivity": "^0.11.5"
|
|
49
|
-
},
|
|
50
46
|
"devDependencies": {
|
|
51
47
|
"@happy-dom/global-registrator": "^20.8.3",
|
|
52
|
-
"@pyreon/core": "^0.11.
|
|
53
|
-
"@pyreon/reactivity": "^0.11.
|
|
54
|
-
"@pyreon/runtime-dom": "^0.11.
|
|
48
|
+
"@pyreon/core": "^0.11.7",
|
|
49
|
+
"@pyreon/reactivity": "^0.11.7",
|
|
50
|
+
"@pyreon/runtime-dom": "^0.11.7"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@pyreon/core": "^0.11.7",
|
|
54
|
+
"@pyreon/reactivity": "^0.11.7"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type {
|
|
|
7
7
|
ScrollToOptions,
|
|
8
8
|
VirtualItem,
|
|
9
9
|
VirtualizerOptions,
|
|
10
|
-
} from
|
|
10
|
+
} from '@tanstack/virtual-core'
|
|
11
11
|
export {
|
|
12
12
|
defaultKeyExtractor,
|
|
13
13
|
defaultRangeExtractor,
|
|
@@ -19,17 +19,14 @@ export {
|
|
|
19
19
|
observeWindowRect,
|
|
20
20
|
Virtualizer,
|
|
21
21
|
windowScroll,
|
|
22
|
-
} from
|
|
22
|
+
} from '@tanstack/virtual-core'
|
|
23
23
|
|
|
24
24
|
// ─── Pyreon adapter ─────────────────────────────────────────────────────────────
|
|
25
25
|
|
|
26
|
-
export type {
|
|
27
|
-
|
|
28
|
-
UseVirtualizerResult,
|
|
29
|
-
} from "./use-virtualizer"
|
|
30
|
-
export { useVirtualizer } from "./use-virtualizer"
|
|
26
|
+
export type { UseVirtualizerOptions, UseVirtualizerResult } from './use-virtualizer'
|
|
27
|
+
export { useVirtualizer } from './use-virtualizer'
|
|
31
28
|
export type {
|
|
32
29
|
UseWindowVirtualizerOptions,
|
|
33
30
|
UseWindowVirtualizerResult,
|
|
34
|
-
} from
|
|
35
|
-
export { useWindowVirtualizer } from
|
|
31
|
+
} from './use-window-virtualizer'
|
|
32
|
+
export { useWindowVirtualizer } from './use-window-virtualizer'
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { mount } from
|
|
3
|
-
import { useVirtualizer, useWindowVirtualizer } from
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
3
|
+
import { useVirtualizer, useWindowVirtualizer } from '../index'
|
|
4
4
|
|
|
5
5
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
7
|
function mountWith<T>(fn: () => T): { result: T; unmount: () => void } {
|
|
8
8
|
let result: T | undefined
|
|
9
|
-
const el = document.createElement(
|
|
9
|
+
const el = document.createElement('div')
|
|
10
10
|
document.body.appendChild(el)
|
|
11
11
|
const Wrapper = () => {
|
|
12
12
|
result = fn()
|
|
@@ -23,19 +23,19 @@ function mountWith<T>(fn: () => T): { result: T; unmount: () => void } {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function createScrollContainer(height = 200): HTMLDivElement {
|
|
26
|
-
const container = document.createElement(
|
|
27
|
-
Object.defineProperty(container,
|
|
28
|
-
Object.defineProperty(container,
|
|
29
|
-
Object.defineProperty(container,
|
|
30
|
-
Object.defineProperty(container,
|
|
26
|
+
const container = document.createElement('div')
|
|
27
|
+
Object.defineProperty(container, 'offsetHeight', { value: height })
|
|
28
|
+
Object.defineProperty(container, 'offsetWidth', { value: 300 })
|
|
29
|
+
Object.defineProperty(container, 'scrollHeight', { value: 10000 })
|
|
30
|
+
Object.defineProperty(container, 'clientHeight', { value: height })
|
|
31
31
|
document.body.appendChild(container)
|
|
32
32
|
return container
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// ─── useVirtualizer — virtualItems signal ──────────────────────────────────
|
|
36
36
|
|
|
37
|
-
describe(
|
|
38
|
-
it(
|
|
37
|
+
describe('useVirtualizer — virtualItems signal', () => {
|
|
38
|
+
it('returns a signal that holds an array of VirtualItem', () => {
|
|
39
39
|
const container = createScrollContainer()
|
|
40
40
|
const { result: virt, unmount } = mountWith(() =>
|
|
41
41
|
useVirtualizer(() => ({
|
|
@@ -52,7 +52,7 @@ describe("useVirtualizer — virtualItems signal", () => {
|
|
|
52
52
|
container.remove()
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
it(
|
|
55
|
+
it('virtual items have index, start, end, size, key properties', () => {
|
|
56
56
|
const container = createScrollContainer()
|
|
57
57
|
const { result: virt, unmount } = mountWith(() =>
|
|
58
58
|
useVirtualizer(() => ({
|
|
@@ -65,16 +65,16 @@ describe("useVirtualizer — virtualItems signal", () => {
|
|
|
65
65
|
const items = virt.virtualItems()
|
|
66
66
|
expect(items.length).toBeGreaterThan(0)
|
|
67
67
|
const first = items[0]!
|
|
68
|
-
expect(typeof first.index).toBe(
|
|
69
|
-
expect(typeof first.start).toBe(
|
|
70
|
-
expect(typeof first.end).toBe(
|
|
71
|
-
expect(typeof first.size).toBe(
|
|
68
|
+
expect(typeof first.index).toBe('number')
|
|
69
|
+
expect(typeof first.start).toBe('number')
|
|
70
|
+
expect(typeof first.end).toBe('number')
|
|
71
|
+
expect(typeof first.size).toBe('number')
|
|
72
72
|
expect(first.key).toBeDefined()
|
|
73
73
|
unmount()
|
|
74
74
|
container.remove()
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
it(
|
|
77
|
+
it('first item starts at index 0', () => {
|
|
78
78
|
const container = createScrollContainer()
|
|
79
79
|
const { result: virt, unmount } = mountWith(() =>
|
|
80
80
|
useVirtualizer(() => ({
|
|
@@ -92,8 +92,8 @@ describe("useVirtualizer — virtualItems signal", () => {
|
|
|
92
92
|
|
|
93
93
|
// ─── useVirtualizer — totalSize signal ─────────────────────────────────────
|
|
94
94
|
|
|
95
|
-
describe(
|
|
96
|
-
it(
|
|
95
|
+
describe('useVirtualizer — totalSize signal', () => {
|
|
96
|
+
it('returns count * estimateSize for uniform items', () => {
|
|
97
97
|
const container = createScrollContainer()
|
|
98
98
|
const { result: virt, unmount } = mountWith(() =>
|
|
99
99
|
useVirtualizer(() => ({
|
|
@@ -108,7 +108,7 @@ describe("useVirtualizer — totalSize signal", () => {
|
|
|
108
108
|
container.remove()
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
-
it(
|
|
111
|
+
it('includes padding in totalSize', () => {
|
|
112
112
|
const container = createScrollContainer()
|
|
113
113
|
const { result: virt, unmount } = mountWith(() =>
|
|
114
114
|
useVirtualizer(() => ({
|
|
@@ -125,7 +125,7 @@ describe("useVirtualizer — totalSize signal", () => {
|
|
|
125
125
|
container.remove()
|
|
126
126
|
})
|
|
127
127
|
|
|
128
|
-
it(
|
|
128
|
+
it('includes gaps in totalSize', () => {
|
|
129
129
|
const container = createScrollContainer()
|
|
130
130
|
const { result: virt, unmount } = mountWith(() =>
|
|
131
131
|
useVirtualizer(() => ({
|
|
@@ -141,7 +141,7 @@ describe("useVirtualizer — totalSize signal", () => {
|
|
|
141
141
|
container.remove()
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
-
it(
|
|
144
|
+
it('returns 0 when count is 0', () => {
|
|
145
145
|
const container = createScrollContainer()
|
|
146
146
|
const { result: virt, unmount } = mountWith(() =>
|
|
147
147
|
useVirtualizer(() => ({
|
|
@@ -159,8 +159,8 @@ describe("useVirtualizer — totalSize signal", () => {
|
|
|
159
159
|
|
|
160
160
|
// ─── useVirtualizer — count changes update virtualItems ────────────────────
|
|
161
161
|
|
|
162
|
-
describe(
|
|
163
|
-
it(
|
|
162
|
+
describe('useVirtualizer — count changes update virtualItems', () => {
|
|
163
|
+
it('increasing count increases totalSize', () => {
|
|
164
164
|
const container = createScrollContainer()
|
|
165
165
|
const count = signal(100)
|
|
166
166
|
const { result: virt, unmount } = mountWith(() =>
|
|
@@ -182,7 +182,7 @@ describe("useVirtualizer — count changes update virtualItems", () => {
|
|
|
182
182
|
container.remove()
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
it(
|
|
185
|
+
it('setting count to 0 empties virtualItems', () => {
|
|
186
186
|
const container = createScrollContainer()
|
|
187
187
|
const count = signal(100)
|
|
188
188
|
const { result: virt, unmount } = mountWith(() =>
|
|
@@ -202,7 +202,7 @@ describe("useVirtualizer — count changes update virtualItems", () => {
|
|
|
202
202
|
container.remove()
|
|
203
203
|
})
|
|
204
204
|
|
|
205
|
-
it(
|
|
205
|
+
it('growing count from 0 creates virtualItems', () => {
|
|
206
206
|
const container = createScrollContainer()
|
|
207
207
|
const count = signal(0)
|
|
208
208
|
const { result: virt, unmount } = mountWith(() =>
|
|
@@ -225,8 +225,8 @@ describe("useVirtualizer — count changes update virtualItems", () => {
|
|
|
225
225
|
|
|
226
226
|
// ─── useVirtualizer — estimateSize callback ────────────────────────────────
|
|
227
227
|
|
|
228
|
-
describe(
|
|
229
|
-
it(
|
|
228
|
+
describe('useVirtualizer — estimateSize callback', () => {
|
|
229
|
+
it('uses the provided estimateSize for totalSize calculation', () => {
|
|
230
230
|
const container = createScrollContainer()
|
|
231
231
|
const { result: virt, unmount } = mountWith(() =>
|
|
232
232
|
useVirtualizer(() => ({
|
|
@@ -241,7 +241,7 @@ describe("useVirtualizer — estimateSize callback", () => {
|
|
|
241
241
|
container.remove()
|
|
242
242
|
})
|
|
243
243
|
|
|
244
|
-
it(
|
|
244
|
+
it('reactive estimateSize updates totalSize after measure()', () => {
|
|
245
245
|
const container = createScrollContainer()
|
|
246
246
|
const itemSize = signal(50)
|
|
247
247
|
const { result: virt, unmount } = mountWith(() =>
|
|
@@ -261,7 +261,7 @@ describe("useVirtualizer — estimateSize callback", () => {
|
|
|
261
261
|
container.remove()
|
|
262
262
|
})
|
|
263
263
|
|
|
264
|
-
it(
|
|
264
|
+
it('small estimateSize produces more visible items', () => {
|
|
265
265
|
const container = createScrollContainer(200)
|
|
266
266
|
const { result: small, unmount: u1 } = mountWith(() =>
|
|
267
267
|
useVirtualizer(() => ({
|
|
@@ -291,8 +291,8 @@ describe("useVirtualizer — estimateSize callback", () => {
|
|
|
291
291
|
|
|
292
292
|
// ─── useVirtualizer — additional options ───────────────────────────────────
|
|
293
293
|
|
|
294
|
-
describe(
|
|
295
|
-
it(
|
|
294
|
+
describe('useVirtualizer — additional options', () => {
|
|
295
|
+
it('overscan controls extra items rendered', () => {
|
|
296
296
|
const container = createScrollContainer(200)
|
|
297
297
|
const { result: noOverscan, unmount: u1 } = mountWith(() =>
|
|
298
298
|
useVirtualizer(() => ({
|
|
@@ -320,7 +320,7 @@ describe("useVirtualizer — additional options", () => {
|
|
|
320
320
|
container.remove()
|
|
321
321
|
})
|
|
322
322
|
|
|
323
|
-
it(
|
|
323
|
+
it('horizontal mode works', () => {
|
|
324
324
|
const container = createScrollContainer()
|
|
325
325
|
const { result: virt, unmount } = mountWith(() =>
|
|
326
326
|
useVirtualizer(() => ({
|
|
@@ -337,7 +337,7 @@ describe("useVirtualizer — additional options", () => {
|
|
|
337
337
|
container.remove()
|
|
338
338
|
})
|
|
339
339
|
|
|
340
|
-
it(
|
|
340
|
+
it('enabled: false produces empty output', () => {
|
|
341
341
|
const container = createScrollContainer()
|
|
342
342
|
const { result: virt, unmount } = mountWith(() =>
|
|
343
343
|
useVirtualizer(() => ({
|
|
@@ -354,7 +354,7 @@ describe("useVirtualizer — additional options", () => {
|
|
|
354
354
|
container.remove()
|
|
355
355
|
})
|
|
356
356
|
|
|
357
|
-
it(
|
|
357
|
+
it('isScrolling starts as false', () => {
|
|
358
358
|
const container = createScrollContainer()
|
|
359
359
|
const { result: virt, unmount } = mountWith(() =>
|
|
360
360
|
useVirtualizer(() => ({
|
|
@@ -369,7 +369,7 @@ describe("useVirtualizer — additional options", () => {
|
|
|
369
369
|
container.remove()
|
|
370
370
|
})
|
|
371
371
|
|
|
372
|
-
it(
|
|
372
|
+
it('exposes instance with scrollToIndex and scrollToOffset', () => {
|
|
373
373
|
const container = createScrollContainer()
|
|
374
374
|
const { result: virt, unmount } = mountWith(() =>
|
|
375
375
|
useVirtualizer(() => ({
|
|
@@ -379,9 +379,9 @@ describe("useVirtualizer — additional options", () => {
|
|
|
379
379
|
})),
|
|
380
380
|
)
|
|
381
381
|
|
|
382
|
-
expect(typeof virt.instance.scrollToIndex).toBe(
|
|
383
|
-
expect(typeof virt.instance.scrollToOffset).toBe(
|
|
384
|
-
expect(typeof virt.instance.measureElement).toBe(
|
|
382
|
+
expect(typeof virt.instance.scrollToIndex).toBe('function')
|
|
383
|
+
expect(typeof virt.instance.scrollToOffset).toBe('function')
|
|
384
|
+
expect(typeof virt.instance.measureElement).toBe('function')
|
|
385
385
|
unmount()
|
|
386
386
|
container.remove()
|
|
387
387
|
})
|
|
@@ -389,26 +389,26 @@ describe("useVirtualizer — additional options", () => {
|
|
|
389
389
|
|
|
390
390
|
// ─── useWindowVirtualizer ──────────────────────────────────────────────────
|
|
391
391
|
|
|
392
|
-
describe(
|
|
392
|
+
describe('useWindowVirtualizer — comprehensive', () => {
|
|
393
393
|
beforeEach(() => {
|
|
394
|
-
Object.defineProperty(window,
|
|
394
|
+
Object.defineProperty(window, 'innerHeight', {
|
|
395
395
|
value: 768,
|
|
396
396
|
writable: true,
|
|
397
397
|
configurable: true,
|
|
398
398
|
})
|
|
399
|
-
Object.defineProperty(window,
|
|
399
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
400
400
|
value: 1024,
|
|
401
401
|
writable: true,
|
|
402
402
|
configurable: true,
|
|
403
403
|
})
|
|
404
|
-
Object.defineProperty(window,
|
|
404
|
+
Object.defineProperty(window, 'scrollY', {
|
|
405
405
|
value: 0,
|
|
406
406
|
writable: true,
|
|
407
407
|
configurable: true,
|
|
408
408
|
})
|
|
409
409
|
})
|
|
410
410
|
|
|
411
|
-
it(
|
|
411
|
+
it('returns virtualItems, totalSize, isScrolling signals', () => {
|
|
412
412
|
const { result: virt, unmount } = mountWith(() =>
|
|
413
413
|
useWindowVirtualizer(() => ({
|
|
414
414
|
count: 1000,
|
|
@@ -416,16 +416,16 @@ describe("useWindowVirtualizer — comprehensive", () => {
|
|
|
416
416
|
})),
|
|
417
417
|
)
|
|
418
418
|
|
|
419
|
-
expect(typeof virt.virtualItems).toBe(
|
|
420
|
-
expect(typeof virt.totalSize).toBe(
|
|
421
|
-
expect(typeof virt.isScrolling).toBe(
|
|
419
|
+
expect(typeof virt.virtualItems).toBe('function')
|
|
420
|
+
expect(typeof virt.totalSize).toBe('function')
|
|
421
|
+
expect(typeof virt.isScrolling).toBe('function')
|
|
422
422
|
expect(Array.isArray(virt.virtualItems())).toBe(true)
|
|
423
|
-
expect(typeof virt.totalSize()).toBe(
|
|
424
|
-
expect(typeof virt.isScrolling()).toBe(
|
|
423
|
+
expect(typeof virt.totalSize()).toBe('number')
|
|
424
|
+
expect(typeof virt.isScrolling()).toBe('boolean')
|
|
425
425
|
unmount()
|
|
426
426
|
})
|
|
427
427
|
|
|
428
|
-
it(
|
|
428
|
+
it('totalSize equals count * estimateSize', () => {
|
|
429
429
|
const { result: virt, unmount } = mountWith(() =>
|
|
430
430
|
useWindowVirtualizer(() => ({
|
|
431
431
|
count: 100,
|
|
@@ -437,7 +437,7 @@ describe("useWindowVirtualizer — comprehensive", () => {
|
|
|
437
437
|
unmount()
|
|
438
438
|
})
|
|
439
439
|
|
|
440
|
-
it(
|
|
440
|
+
it('reactive count updates totalSize', () => {
|
|
441
441
|
const count = signal(100)
|
|
442
442
|
const { result: virt, unmount } = mountWith(() =>
|
|
443
443
|
useWindowVirtualizer(() => ({
|
|
@@ -456,7 +456,7 @@ describe("useWindowVirtualizer — comprehensive", () => {
|
|
|
456
456
|
unmount()
|
|
457
457
|
})
|
|
458
458
|
|
|
459
|
-
it(
|
|
459
|
+
it('reactive estimateSize updates totalSize after measure()', () => {
|
|
460
460
|
const size = signal(50)
|
|
461
461
|
const { result: virt, unmount } = mountWith(() =>
|
|
462
462
|
useWindowVirtualizer(() => ({
|
|
@@ -473,7 +473,7 @@ describe("useWindowVirtualizer — comprehensive", () => {
|
|
|
473
473
|
unmount()
|
|
474
474
|
})
|
|
475
475
|
|
|
476
|
-
it(
|
|
476
|
+
it('SSR-safe — handles missing document/window', () => {
|
|
477
477
|
const origDoc = globalThis.document
|
|
478
478
|
const origWin = globalThis.window
|
|
479
479
|
try {
|
|
@@ -494,7 +494,7 @@ describe("useWindowVirtualizer — comprehensive", () => {
|
|
|
494
494
|
}
|
|
495
495
|
})
|
|
496
496
|
|
|
497
|
-
it(
|
|
497
|
+
it('gap affects totalSize', () => {
|
|
498
498
|
const { result: virt, unmount } = mountWith(() =>
|
|
499
499
|
useWindowVirtualizer(() => ({
|
|
500
500
|
count: 10,
|
|
@@ -507,7 +507,7 @@ describe("useWindowVirtualizer — comprehensive", () => {
|
|
|
507
507
|
unmount()
|
|
508
508
|
})
|
|
509
509
|
|
|
510
|
-
it(
|
|
510
|
+
it('enabled: false produces empty output', () => {
|
|
511
511
|
const { result: virt, unmount } = mountWith(() =>
|
|
512
512
|
useWindowVirtualizer(() => ({
|
|
513
513
|
count: 100,
|
|
@@ -521,7 +521,7 @@ describe("useWindowVirtualizer — comprehensive", () => {
|
|
|
521
521
|
unmount()
|
|
522
522
|
})
|
|
523
523
|
|
|
524
|
-
it(
|
|
524
|
+
it('onChange callback is forwarded', () => {
|
|
525
525
|
const spy = vi.fn()
|
|
526
526
|
const { result: virt, unmount } = mountWith(() =>
|
|
527
527
|
useWindowVirtualizer(() => ({
|
|
@@ -540,7 +540,7 @@ describe("useWindowVirtualizer — comprehensive", () => {
|
|
|
540
540
|
unmount()
|
|
541
541
|
})
|
|
542
542
|
|
|
543
|
-
it(
|
|
543
|
+
it('overscan controls extra rendered items', () => {
|
|
544
544
|
const { result: small, unmount: u1 } = mountWith(() =>
|
|
545
545
|
useWindowVirtualizer(() => ({
|
|
546
546
|
count: 1000,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { mount } from
|
|
3
|
-
import { useVirtualizer, useWindowVirtualizer } from
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
3
|
+
import { useVirtualizer, useWindowVirtualizer } from '../index'
|
|
4
4
|
|
|
5
5
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
7
|
function mountWith<T>(fn: () => T): { result: T; unmount: () => void } {
|
|
8
8
|
let result: T | undefined
|
|
9
|
-
const el = document.createElement(
|
|
9
|
+
const el = document.createElement('div')
|
|
10
10
|
document.body.appendChild(el)
|
|
11
11
|
const Wrapper = () => {
|
|
12
12
|
result = fn()
|
|
@@ -24,20 +24,20 @@ function mountWith<T>(fn: () => T): { result: T; unmount: () => void } {
|
|
|
24
24
|
|
|
25
25
|
/** Create a mock scroll container with a known size. */
|
|
26
26
|
function createScrollContainer(height = 200): HTMLDivElement {
|
|
27
|
-
const container = document.createElement(
|
|
27
|
+
const container = document.createElement('div')
|
|
28
28
|
// happy-dom doesn't have real layout, but we can set properties
|
|
29
|
-
Object.defineProperty(container,
|
|
30
|
-
Object.defineProperty(container,
|
|
31
|
-
Object.defineProperty(container,
|
|
32
|
-
Object.defineProperty(container,
|
|
29
|
+
Object.defineProperty(container, 'offsetHeight', { value: height })
|
|
30
|
+
Object.defineProperty(container, 'offsetWidth', { value: 300 })
|
|
31
|
+
Object.defineProperty(container, 'scrollHeight', { value: 10000 })
|
|
32
|
+
Object.defineProperty(container, 'clientHeight', { value: height })
|
|
33
33
|
document.body.appendChild(container)
|
|
34
34
|
return container
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// ─── useVirtualizer ──────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
-
describe(
|
|
40
|
-
it(
|
|
39
|
+
describe('useVirtualizer', () => {
|
|
40
|
+
it('creates a virtualizer with virtual items', () => {
|
|
41
41
|
const container = createScrollContainer()
|
|
42
42
|
const { result: virt, unmount } = mountWith(() =>
|
|
43
43
|
useVirtualizer(() => ({
|
|
@@ -54,7 +54,7 @@ describe("useVirtualizer", () => {
|
|
|
54
54
|
container.remove()
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
it(
|
|
57
|
+
it('returns correct total size', () => {
|
|
58
58
|
const container = createScrollContainer()
|
|
59
59
|
const { result: virt, unmount } = mountWith(() =>
|
|
60
60
|
useVirtualizer(() => ({
|
|
@@ -70,7 +70,7 @@ describe("useVirtualizer", () => {
|
|
|
70
70
|
container.remove()
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
-
it(
|
|
73
|
+
it('reactive count — updates when count signal changes', () => {
|
|
74
74
|
const container = createScrollContainer()
|
|
75
75
|
const count = signal(100)
|
|
76
76
|
const { result: virt, unmount } = mountWith(() =>
|
|
@@ -89,7 +89,7 @@ describe("useVirtualizer", () => {
|
|
|
89
89
|
container.remove()
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
it(
|
|
92
|
+
it('reactive estimateSize — updates total size', () => {
|
|
93
93
|
const container = createScrollContainer()
|
|
94
94
|
const itemSize = signal(50)
|
|
95
95
|
const { result: virt, unmount } = mountWith(() =>
|
|
@@ -110,7 +110,7 @@ describe("useVirtualizer", () => {
|
|
|
110
110
|
container.remove()
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
it(
|
|
113
|
+
it('exposes the virtualizer instance', () => {
|
|
114
114
|
const container = createScrollContainer()
|
|
115
115
|
const { result: virt, unmount } = mountWith(() =>
|
|
116
116
|
useVirtualizer(() => ({
|
|
@@ -121,14 +121,14 @@ describe("useVirtualizer", () => {
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
expect(virt.instance).toBeDefined()
|
|
124
|
-
expect(typeof virt.instance.scrollToIndex).toBe(
|
|
125
|
-
expect(typeof virt.instance.scrollToOffset).toBe(
|
|
126
|
-
expect(typeof virt.instance.measureElement).toBe(
|
|
124
|
+
expect(typeof virt.instance.scrollToIndex).toBe('function')
|
|
125
|
+
expect(typeof virt.instance.scrollToOffset).toBe('function')
|
|
126
|
+
expect(typeof virt.instance.measureElement).toBe('function')
|
|
127
127
|
unmount()
|
|
128
128
|
container.remove()
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
it(
|
|
131
|
+
it('virtual items have correct structure', () => {
|
|
132
132
|
const container = createScrollContainer()
|
|
133
133
|
const { result: virt, unmount } = mountWith(() =>
|
|
134
134
|
useVirtualizer(() => ({
|
|
@@ -141,17 +141,17 @@ describe("useVirtualizer", () => {
|
|
|
141
141
|
const items = virt.virtualItems()
|
|
142
142
|
if (items.length > 0) {
|
|
143
143
|
const item = items[0]!
|
|
144
|
-
expect(typeof item.index).toBe(
|
|
145
|
-
expect(typeof item.start).toBe(
|
|
146
|
-
expect(typeof item.end).toBe(
|
|
147
|
-
expect(typeof item.size).toBe(
|
|
144
|
+
expect(typeof item.index).toBe('number')
|
|
145
|
+
expect(typeof item.start).toBe('number')
|
|
146
|
+
expect(typeof item.end).toBe('number')
|
|
147
|
+
expect(typeof item.size).toBe('number')
|
|
148
148
|
expect(typeof item.key).toBeDefined()
|
|
149
149
|
}
|
|
150
150
|
unmount()
|
|
151
151
|
container.remove()
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
-
it(
|
|
154
|
+
it('overscan controls extra items rendered', () => {
|
|
155
155
|
const container = createScrollContainer(200)
|
|
156
156
|
const { result: small, unmount: unmount1 } = mountWith(() =>
|
|
157
157
|
useVirtualizer(() => ({
|
|
@@ -178,7 +178,7 @@ describe("useVirtualizer", () => {
|
|
|
178
178
|
container.remove()
|
|
179
179
|
})
|
|
180
180
|
|
|
181
|
-
it(
|
|
181
|
+
it('gap option affects total size', () => {
|
|
182
182
|
const container = createScrollContainer()
|
|
183
183
|
const { result: noGap, unmount: unmount1 } = mountWith(() =>
|
|
184
184
|
useVirtualizer(() => ({
|
|
@@ -206,7 +206,7 @@ describe("useVirtualizer", () => {
|
|
|
206
206
|
container.remove()
|
|
207
207
|
})
|
|
208
208
|
|
|
209
|
-
it(
|
|
209
|
+
it('horizontal mode works', () => {
|
|
210
210
|
const container = createScrollContainer()
|
|
211
211
|
const { result: virt, unmount } = mountWith(() =>
|
|
212
212
|
useVirtualizer(() => ({
|
|
@@ -223,7 +223,7 @@ describe("useVirtualizer", () => {
|
|
|
223
223
|
container.remove()
|
|
224
224
|
})
|
|
225
225
|
|
|
226
|
-
it(
|
|
226
|
+
it('padding affects total size', () => {
|
|
227
227
|
const container = createScrollContainer()
|
|
228
228
|
const { result: virt, unmount } = mountWith(() =>
|
|
229
229
|
useVirtualizer(() => ({
|
|
@@ -241,7 +241,7 @@ describe("useVirtualizer", () => {
|
|
|
241
241
|
container.remove()
|
|
242
242
|
})
|
|
243
243
|
|
|
244
|
-
it(
|
|
244
|
+
it('isScrolling starts as false', () => {
|
|
245
245
|
const container = createScrollContainer()
|
|
246
246
|
const { result: virt, unmount } = mountWith(() =>
|
|
247
247
|
useVirtualizer(() => ({
|
|
@@ -256,7 +256,7 @@ describe("useVirtualizer", () => {
|
|
|
256
256
|
container.remove()
|
|
257
257
|
})
|
|
258
258
|
|
|
259
|
-
it(
|
|
259
|
+
it('enabled: false produces empty virtual items', () => {
|
|
260
260
|
const container = createScrollContainer()
|
|
261
261
|
const { result: virt, unmount } = mountWith(() =>
|
|
262
262
|
useVirtualizer(() => ({
|
|
@@ -273,7 +273,7 @@ describe("useVirtualizer", () => {
|
|
|
273
273
|
container.remove()
|
|
274
274
|
})
|
|
275
275
|
|
|
276
|
-
it(
|
|
276
|
+
it('onChange callback updates signals when triggered', () => {
|
|
277
277
|
const container = createScrollContainer()
|
|
278
278
|
const onChangeSpy = vi.fn()
|
|
279
279
|
const { result: virt, unmount } = mountWith(() =>
|
|
@@ -293,15 +293,15 @@ describe("useVirtualizer", () => {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
expect(virt.virtualItems()).toBeDefined()
|
|
296
|
-
expect(typeof virt.totalSize()).toBe(
|
|
297
|
-
expect(typeof virt.isScrolling()).toBe(
|
|
296
|
+
expect(typeof virt.totalSize()).toBe('number')
|
|
297
|
+
expect(typeof virt.isScrolling()).toBe('boolean')
|
|
298
298
|
// The user's onChange should have been forwarded
|
|
299
299
|
expect(onChangeSpy).toHaveBeenCalled()
|
|
300
300
|
unmount()
|
|
301
301
|
container.remove()
|
|
302
302
|
})
|
|
303
303
|
|
|
304
|
-
it(
|
|
304
|
+
it('onChange callback works without user-provided onChange', () => {
|
|
305
305
|
const container = createScrollContainer()
|
|
306
306
|
const { result: virt, unmount } = mountWith(() =>
|
|
307
307
|
useVirtualizer(() => ({
|
|
@@ -326,27 +326,27 @@ describe("useVirtualizer", () => {
|
|
|
326
326
|
|
|
327
327
|
// ─── useWindowVirtualizer ─────────────────────────────────────────────────────
|
|
328
328
|
|
|
329
|
-
describe(
|
|
329
|
+
describe('useWindowVirtualizer', () => {
|
|
330
330
|
beforeEach(() => {
|
|
331
331
|
// Ensure window has layout-like properties for happy-dom
|
|
332
|
-
Object.defineProperty(window,
|
|
332
|
+
Object.defineProperty(window, 'innerHeight', {
|
|
333
333
|
value: 768,
|
|
334
334
|
writable: true,
|
|
335
335
|
configurable: true,
|
|
336
336
|
})
|
|
337
|
-
Object.defineProperty(window,
|
|
337
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
338
338
|
value: 1024,
|
|
339
339
|
writable: true,
|
|
340
340
|
configurable: true,
|
|
341
341
|
})
|
|
342
|
-
Object.defineProperty(window,
|
|
342
|
+
Object.defineProperty(window, 'scrollY', {
|
|
343
343
|
value: 0,
|
|
344
344
|
writable: true,
|
|
345
345
|
configurable: true,
|
|
346
346
|
})
|
|
347
347
|
})
|
|
348
348
|
|
|
349
|
-
it(
|
|
349
|
+
it('creates an instance and returns virtualItems, totalSize, isScrolling signals', () => {
|
|
350
350
|
const { result: virt, unmount } = mountWith(() =>
|
|
351
351
|
useWindowVirtualizer(() => ({
|
|
352
352
|
count: 1000,
|
|
@@ -357,12 +357,12 @@ describe("useWindowVirtualizer", () => {
|
|
|
357
357
|
expect(virt.instance).toBeDefined()
|
|
358
358
|
expect(virt.virtualItems()).toBeDefined()
|
|
359
359
|
expect(Array.isArray(virt.virtualItems())).toBe(true)
|
|
360
|
-
expect(typeof virt.totalSize()).toBe(
|
|
360
|
+
expect(typeof virt.totalSize()).toBe('number')
|
|
361
361
|
expect(virt.isScrolling()).toBe(false)
|
|
362
362
|
unmount()
|
|
363
363
|
})
|
|
364
364
|
|
|
365
|
-
it(
|
|
365
|
+
it('returns correct total size', () => {
|
|
366
366
|
const { result: virt, unmount } = mountWith(() =>
|
|
367
367
|
useWindowVirtualizer(() => ({
|
|
368
368
|
count: 100,
|
|
@@ -374,7 +374,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
374
374
|
unmount()
|
|
375
375
|
})
|
|
376
376
|
|
|
377
|
-
it(
|
|
377
|
+
it('reactive count — updates when count signal changes', () => {
|
|
378
378
|
const count = signal(100)
|
|
379
379
|
const { result: virt, unmount } = mountWith(() =>
|
|
380
380
|
useWindowVirtualizer(() => ({
|
|
@@ -390,7 +390,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
390
390
|
unmount()
|
|
391
391
|
})
|
|
392
392
|
|
|
393
|
-
it(
|
|
393
|
+
it('reactive estimateSize — updates total size after measure()', () => {
|
|
394
394
|
const itemSize = signal(50)
|
|
395
395
|
const { result: virt, unmount } = mountWith(() =>
|
|
396
396
|
useWindowVirtualizer(() => ({
|
|
@@ -407,7 +407,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
407
407
|
unmount()
|
|
408
408
|
})
|
|
409
409
|
|
|
410
|
-
it(
|
|
410
|
+
it('exposes instance methods (scrollToIndex, scrollToOffset, measureElement)', () => {
|
|
411
411
|
const { result: virt, unmount } = mountWith(() =>
|
|
412
412
|
useWindowVirtualizer(() => ({
|
|
413
413
|
count: 50,
|
|
@@ -415,13 +415,13 @@ describe("useWindowVirtualizer", () => {
|
|
|
415
415
|
})),
|
|
416
416
|
)
|
|
417
417
|
|
|
418
|
-
expect(typeof virt.instance.scrollToIndex).toBe(
|
|
419
|
-
expect(typeof virt.instance.scrollToOffset).toBe(
|
|
420
|
-
expect(typeof virt.instance.measureElement).toBe(
|
|
418
|
+
expect(typeof virt.instance.scrollToIndex).toBe('function')
|
|
419
|
+
expect(typeof virt.instance.scrollToOffset).toBe('function')
|
|
420
|
+
expect(typeof virt.instance.measureElement).toBe('function')
|
|
421
421
|
unmount()
|
|
422
422
|
})
|
|
423
423
|
|
|
424
|
-
it(
|
|
424
|
+
it('virtual items have correct structure', () => {
|
|
425
425
|
const { result: virt, unmount } = mountWith(() =>
|
|
426
426
|
useWindowVirtualizer(() => ({
|
|
427
427
|
count: 1000,
|
|
@@ -432,16 +432,16 @@ describe("useWindowVirtualizer", () => {
|
|
|
432
432
|
const items = virt.virtualItems()
|
|
433
433
|
if (items.length > 0) {
|
|
434
434
|
const item = items[0]!
|
|
435
|
-
expect(typeof item.index).toBe(
|
|
436
|
-
expect(typeof item.start).toBe(
|
|
437
|
-
expect(typeof item.end).toBe(
|
|
438
|
-
expect(typeof item.size).toBe(
|
|
435
|
+
expect(typeof item.index).toBe('number')
|
|
436
|
+
expect(typeof item.start).toBe('number')
|
|
437
|
+
expect(typeof item.end).toBe('number')
|
|
438
|
+
expect(typeof item.size).toBe('number')
|
|
439
439
|
expect(typeof item.key).toBeDefined()
|
|
440
440
|
}
|
|
441
441
|
unmount()
|
|
442
442
|
})
|
|
443
443
|
|
|
444
|
-
it(
|
|
444
|
+
it('gap option affects total size', () => {
|
|
445
445
|
const { result: noGap, unmount: unmount1 } = mountWith(() =>
|
|
446
446
|
useWindowVirtualizer(() => ({
|
|
447
447
|
count: 10,
|
|
@@ -465,7 +465,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
465
465
|
unmount2()
|
|
466
466
|
})
|
|
467
467
|
|
|
468
|
-
it(
|
|
468
|
+
it('padding affects total size', () => {
|
|
469
469
|
const { result: virt, unmount } = mountWith(() =>
|
|
470
470
|
useWindowVirtualizer(() => ({
|
|
471
471
|
count: 10,
|
|
@@ -480,7 +480,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
480
480
|
unmount()
|
|
481
481
|
})
|
|
482
482
|
|
|
483
|
-
it(
|
|
483
|
+
it('horizontal mode works', () => {
|
|
484
484
|
const { result: virt, unmount } = mountWith(() =>
|
|
485
485
|
useWindowVirtualizer(() => ({
|
|
486
486
|
count: 100,
|
|
@@ -494,7 +494,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
494
494
|
unmount()
|
|
495
495
|
})
|
|
496
496
|
|
|
497
|
-
it(
|
|
497
|
+
it('enabled: false produces empty virtual items', () => {
|
|
498
498
|
const { result: virt, unmount } = mountWith(() =>
|
|
499
499
|
useWindowVirtualizer(() => ({
|
|
500
500
|
count: 100,
|
|
@@ -508,7 +508,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
508
508
|
unmount()
|
|
509
509
|
})
|
|
510
510
|
|
|
511
|
-
it(
|
|
511
|
+
it('calls user-provided onChange callback', () => {
|
|
512
512
|
const onChangeSpy = vi.fn()
|
|
513
513
|
const count = signal(10)
|
|
514
514
|
const { result: virt, unmount } = mountWith(() =>
|
|
@@ -527,7 +527,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
527
527
|
unmount()
|
|
528
528
|
})
|
|
529
529
|
|
|
530
|
-
it(
|
|
530
|
+
it('overscan controls extra items rendered', () => {
|
|
531
531
|
const { result: small, unmount: unmount1 } = mountWith(() =>
|
|
532
532
|
useWindowVirtualizer(() => ({
|
|
533
533
|
count: 1000,
|
|
@@ -549,7 +549,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
549
549
|
unmount2()
|
|
550
550
|
})
|
|
551
551
|
|
|
552
|
-
it(
|
|
552
|
+
it('onChange callback updates signals when triggered directly', () => {
|
|
553
553
|
const onChangeSpy = vi.fn()
|
|
554
554
|
const { result: virt, unmount } = mountWith(() =>
|
|
555
555
|
useWindowVirtualizer(() => ({
|
|
@@ -566,13 +566,13 @@ describe("useWindowVirtualizer", () => {
|
|
|
566
566
|
}
|
|
567
567
|
|
|
568
568
|
expect(virt.virtualItems()).toBeDefined()
|
|
569
|
-
expect(typeof virt.totalSize()).toBe(
|
|
570
|
-
expect(typeof virt.isScrolling()).toBe(
|
|
569
|
+
expect(typeof virt.totalSize()).toBe('number')
|
|
570
|
+
expect(typeof virt.isScrolling()).toBe('boolean')
|
|
571
571
|
expect(onChangeSpy).toHaveBeenCalled()
|
|
572
572
|
unmount()
|
|
573
573
|
})
|
|
574
574
|
|
|
575
|
-
it(
|
|
575
|
+
it('onChange works without user-provided onChange', () => {
|
|
576
576
|
const { result: virt, unmount } = mountWith(() =>
|
|
577
577
|
useWindowVirtualizer(() => ({
|
|
578
578
|
count: 100,
|
|
@@ -589,7 +589,7 @@ describe("useWindowVirtualizer", () => {
|
|
|
589
589
|
unmount()
|
|
590
590
|
})
|
|
591
591
|
|
|
592
|
-
it(
|
|
592
|
+
it('handles missing document/window gracefully', () => {
|
|
593
593
|
const origDoc = globalThis.document
|
|
594
594
|
const origWin = globalThis.window
|
|
595
595
|
try {
|
package/src/use-virtualizer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { onMount, onUnmount } from
|
|
2
|
-
import type { Signal } from
|
|
3
|
-
import { batch, effect, signal } from
|
|
1
|
+
import { onMount, onUnmount } from '@pyreon/core'
|
|
2
|
+
import type { Signal } from '@pyreon/reactivity'
|
|
3
|
+
import { batch, effect, signal } from '@pyreon/reactivity'
|
|
4
4
|
import {
|
|
5
5
|
elementScroll,
|
|
6
6
|
observeElementOffset,
|
|
@@ -8,19 +8,19 @@ import {
|
|
|
8
8
|
type VirtualItem,
|
|
9
9
|
Virtualizer,
|
|
10
10
|
type VirtualizerOptions,
|
|
11
|
-
} from
|
|
11
|
+
} from '@tanstack/virtual-core'
|
|
12
12
|
|
|
13
13
|
export type UseVirtualizerOptions<
|
|
14
14
|
TScrollElement extends Element,
|
|
15
15
|
TItemElement extends Element,
|
|
16
16
|
> = () => Omit<
|
|
17
17
|
VirtualizerOptions<TScrollElement, TItemElement>,
|
|
18
|
-
|
|
18
|
+
'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
|
|
19
19
|
> &
|
|
20
20
|
Partial<
|
|
21
21
|
Pick<
|
|
22
22
|
VirtualizerOptions<TScrollElement, TItemElement>,
|
|
23
|
-
|
|
23
|
+
'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
|
|
24
24
|
>
|
|
25
25
|
>
|
|
26
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { onMount, onUnmount } from
|
|
2
|
-
import type { Signal } from
|
|
3
|
-
import { batch, effect, signal } from
|
|
1
|
+
import { onMount, onUnmount } from '@pyreon/core'
|
|
2
|
+
import type { Signal } from '@pyreon/reactivity'
|
|
3
|
+
import { batch, effect, signal } from '@pyreon/reactivity'
|
|
4
4
|
import {
|
|
5
5
|
observeWindowOffset,
|
|
6
6
|
observeWindowRect,
|
|
@@ -8,16 +8,16 @@ import {
|
|
|
8
8
|
Virtualizer,
|
|
9
9
|
type VirtualizerOptions,
|
|
10
10
|
windowScroll,
|
|
11
|
-
} from
|
|
11
|
+
} from '@tanstack/virtual-core'
|
|
12
12
|
|
|
13
13
|
export type UseWindowVirtualizerOptions<TItemElement extends Element> = () => Omit<
|
|
14
14
|
VirtualizerOptions<Window, TItemElement>,
|
|
15
|
-
|
|
15
|
+
'observeElementRect' | 'observeElementOffset' | 'scrollToFn' | 'getScrollElement'
|
|
16
16
|
> &
|
|
17
17
|
Partial<
|
|
18
18
|
Pick<
|
|
19
19
|
VirtualizerOptions<Window, TItemElement>,
|
|
20
|
-
|
|
20
|
+
'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
|
|
21
21
|
>
|
|
22
22
|
>
|
|
23
23
|
|
|
@@ -48,8 +48,8 @@ export function useWindowVirtualizer<TItemElement extends Element>(
|
|
|
48
48
|
observeElementRect: observeWindowRect,
|
|
49
49
|
observeElementOffset: observeWindowOffset,
|
|
50
50
|
scrollToFn: windowScroll,
|
|
51
|
-
initialOffset: typeof document !==
|
|
52
|
-
getScrollElement: () => (typeof window !==
|
|
51
|
+
initialOffset: typeof document !== 'undefined' ? window.scrollY : 0,
|
|
52
|
+
getScrollElement: () => (typeof window !== 'undefined' ? window : (null as unknown as Window)),
|
|
53
53
|
...options(),
|
|
54
54
|
}
|
|
55
55
|
|