@v-c/virtual-list 0.0.1

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Filler.cjs +1 -0
  3. package/dist/Filler.d.ts +33 -0
  4. package/dist/Filler.js +57 -0
  5. package/dist/Item.cjs +1 -0
  6. package/dist/Item.d.ts +18 -0
  7. package/dist/Item.js +27 -0
  8. package/dist/List.cjs +1 -0
  9. package/dist/List.d.ts +108 -0
  10. package/dist/List.js +276 -0
  11. package/dist/ScrollBar.cjs +1 -0
  12. package/dist/ScrollBar.d.ts +116 -0
  13. package/dist/ScrollBar.js +193 -0
  14. package/dist/hooks/useDiffItem.cjs +1 -0
  15. package/dist/hooks/useDiffItem.d.ts +2 -0
  16. package/dist/hooks/useDiffItem.js +21 -0
  17. package/dist/hooks/useFrameWheel.cjs +1 -0
  18. package/dist/hooks/useFrameWheel.d.ts +11 -0
  19. package/dist/hooks/useFrameWheel.js +52 -0
  20. package/dist/hooks/useGetSize.cjs +1 -0
  21. package/dist/hooks/useGetSize.d.ts +7 -0
  22. package/dist/hooks/useGetSize.js +24 -0
  23. package/dist/hooks/useHeights.cjs +1 -0
  24. package/dist/hooks/useHeights.d.ts +9 -0
  25. package/dist/hooks/useHeights.js +43 -0
  26. package/dist/hooks/useMobileTouchMove.cjs +1 -0
  27. package/dist/hooks/useMobileTouchMove.d.ts +2 -0
  28. package/dist/hooks/useMobileTouchMove.js +44 -0
  29. package/dist/hooks/useOriginScroll.cjs +1 -0
  30. package/dist/hooks/useOriginScroll.d.ts +2 -0
  31. package/dist/hooks/useOriginScroll.js +17 -0
  32. package/dist/hooks/useScrollDrag.cjs +1 -0
  33. package/dist/hooks/useScrollDrag.d.ts +3 -0
  34. package/dist/hooks/useScrollDrag.js +51 -0
  35. package/dist/index.cjs +1 -0
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.js +4 -0
  38. package/dist/interface.cjs +1 -0
  39. package/dist/interface.d.ts +27 -0
  40. package/dist/interface.js +1 -0
  41. package/dist/utils/CacheMap.cjs +1 -0
  42. package/dist/utils/CacheMap.d.ts +16 -0
  43. package/dist/utils/CacheMap.js +29 -0
  44. package/dist/utils/isFirefox.cjs +1 -0
  45. package/dist/utils/isFirefox.d.ts +2 -0
  46. package/dist/utils/isFirefox.js +4 -0
  47. package/dist/utils/scrollbarUtil.cjs +1 -0
  48. package/dist/utils/scrollbarUtil.d.ts +1 -0
  49. package/dist/utils/scrollbarUtil.js +7 -0
  50. package/docs/basic.vue +175 -0
  51. package/docs/height.vue +48 -0
  52. package/docs/nest.vue +60 -0
  53. package/docs/no-virtual.vue +127 -0
  54. package/docs/switch.vue +101 -0
  55. package/docs/virtual-list.stories.vue +31 -0
  56. package/package.json +38 -0
  57. package/src/Filler.tsx +72 -0
  58. package/src/Item.tsx +34 -0
  59. package/src/List.tsx +577 -0
  60. package/src/ScrollBar.tsx +298 -0
  61. package/src/__tests__/List.test.ts +59 -0
  62. package/src/hooks/useDiffItem.ts +27 -0
  63. package/src/hooks/useFrameWheel.ts +141 -0
  64. package/src/hooks/useGetSize.ts +44 -0
  65. package/src/hooks/useHeights.ts +106 -0
  66. package/src/hooks/useMobileTouchMove.ts +131 -0
  67. package/src/hooks/useOriginScroll.ts +47 -0
  68. package/src/hooks/useScrollDrag.ts +123 -0
  69. package/src/index.ts +5 -0
  70. package/src/interface.ts +32 -0
  71. package/src/utils/CacheMap.ts +42 -0
  72. package/src/utils/isFirefox.ts +3 -0
  73. package/src/utils/scrollbarUtil.ts +10 -0
  74. package/vite.config.ts +18 -0
  75. package/vitest.config.ts +11 -0
package/src/Filler.tsx ADDED
@@ -0,0 +1,72 @@
1
+ import ResizeObserver from '@v-c/resize-observer'
2
+ import { type CSSProperties, defineComponent, type PropType, type VNode } from 'vue'
3
+
4
+ export interface InnerProps {
5
+ role?: string
6
+ id?: string
7
+ }
8
+
9
+ export default defineComponent({
10
+ name: 'Filler',
11
+ props: {
12
+ prefixCls: String,
13
+ /** Virtual filler height. Should be `count * itemMinHeight` */
14
+ height: Number,
15
+ /** Set offset of visible items. Should be the top of start item position */
16
+ offsetY: Number,
17
+ offsetX: Number,
18
+ scrollWidth: Number,
19
+ onInnerResize: Function as PropType<() => void>,
20
+ innerProps: Object as PropType<InnerProps>,
21
+ rtl: Boolean,
22
+ extra: Object as PropType<VNode>,
23
+ },
24
+ setup(props, { slots }) {
25
+ return () => {
26
+ let outerStyle: CSSProperties = {}
27
+ let innerStyle: CSSProperties = {
28
+ display: 'flex',
29
+ flexDirection: 'column',
30
+ }
31
+
32
+ if (props.offsetY !== undefined) {
33
+ outerStyle = {
34
+ height: `${props.height}px`,
35
+ position: 'relative',
36
+ overflow: 'hidden',
37
+ }
38
+
39
+ innerStyle = {
40
+ ...innerStyle,
41
+ transform: `translateY(${props.offsetY}px)`,
42
+ [props.rtl ? 'marginRight' : 'marginLeft']: `-${props.offsetX || 0}px`,
43
+ position: 'absolute',
44
+ left: 0,
45
+ right: 0,
46
+ top: 0,
47
+ }
48
+ }
49
+
50
+ return (
51
+ <div style={outerStyle}>
52
+ <ResizeObserver
53
+ onResize={({ offsetHeight }) => {
54
+ if (offsetHeight && props.onInnerResize) {
55
+ props.onInnerResize()
56
+ }
57
+ }}
58
+ >
59
+ <div
60
+ style={innerStyle}
61
+ class={props.prefixCls ? `${props.prefixCls}-holder-inner` : undefined}
62
+ {...props.innerProps}
63
+ >
64
+ {slots.default?.()}
65
+ {props.extra}
66
+ </div>
67
+ </ResizeObserver>
68
+ </div>
69
+ )
70
+ }
71
+ },
72
+ })
package/src/Item.tsx ADDED
@@ -0,0 +1,34 @@
1
+ import { filterEmpty } from '@v-c/util/dist/props-util'
2
+ import { cloneVNode, defineComponent, type PropType, shallowRef } from 'vue'
3
+
4
+ export interface ItemProps {
5
+ setRef: (element: HTMLElement | null) => void
6
+ }
7
+
8
+ export default defineComponent({
9
+ name: 'Item',
10
+ props: {
11
+ setRef: {
12
+ type: Function as PropType<(element: HTMLElement | null) => void>,
13
+ required: true,
14
+ },
15
+ },
16
+ setup(props, { slots }) {
17
+ // Store the ref callback to avoid recreating it on each render
18
+ const currentElement = shallowRef<HTMLElement | null>(null)
19
+ const refFunc = (node: any) => {
20
+ if (currentElement.value !== node) {
21
+ currentElement.value = node
22
+ props.setRef(node)
23
+ }
24
+ }
25
+
26
+ return () => {
27
+ const child = filterEmpty(slots.default?.() ?? [])[0]
28
+ if (!child)
29
+ return null
30
+
31
+ return cloneVNode(child, { ref: refFunc })
32
+ }
33
+ },
34
+ })
package/src/List.tsx ADDED
@@ -0,0 +1,577 @@
1
+ import type { Key } from '@v-c/util/dist/type'
2
+
3
+ import type { ExtraRenderInfo } from './interface'
4
+ import ResizeObserver from '@v-c/resize-observer'
5
+ import {
6
+ computed,
7
+ type CSSProperties,
8
+ defineComponent,
9
+ type PropType,
10
+ ref,
11
+ shallowRef,
12
+ type VNode,
13
+ watch,
14
+ } from 'vue'
15
+ import Filler, { type InnerProps } from './Filler'
16
+ import useDiffItem from './hooks/useDiffItem'
17
+ import useFrameWheel from './hooks/useFrameWheel'
18
+ import { useGetSize } from './hooks/useGetSize'
19
+ import useHeights from './hooks/useHeights'
20
+ import useMobileTouchMove from './hooks/useMobileTouchMove'
21
+ import useScrollDrag from './hooks/useScrollDrag'
22
+ import Item from './Item'
23
+ import ScrollBar, { type ScrollBarRef } from './ScrollBar'
24
+ import { getSpinSize } from './utils/scrollbarUtil'
25
+
26
+ const EMPTY_DATA: any[] = []
27
+
28
+ const ScrollStyle: CSSProperties = {
29
+ overflowY: 'auto',
30
+ overflowAnchor: 'none',
31
+ }
32
+
33
+ export interface ScrollInfo {
34
+ x: number
35
+ y: number
36
+ }
37
+
38
+ export interface ListRef {
39
+ nativeElement?: HTMLDivElement
40
+ scrollTo: (arg?: number | ScrollConfig) => void
41
+ getScrollInfo: () => ScrollInfo
42
+ }
43
+
44
+ export interface ScrollPos {
45
+ left?: number
46
+ top?: number
47
+ }
48
+
49
+ export interface ScrollTarget {
50
+ index?: number
51
+ key?: Key
52
+ align?: 'top' | 'bottom' | 'auto'
53
+ offset?: number
54
+ }
55
+
56
+ export type ScrollConfig = ScrollTarget | ScrollPos
57
+
58
+ export interface ListProps<T = any> {
59
+ prefixCls?: string
60
+ data?: T[]
61
+ height?: number
62
+ itemHeight?: number
63
+ fullHeight?: boolean
64
+ itemKey: Key | ((item: T) => Key)
65
+ component?: string
66
+ virtual?: boolean
67
+ onScroll?: (e: Event) => void
68
+ onVirtualScroll?: (info: ScrollInfo) => void
69
+ onVisibleChange?: (visibleList: T[], fullList: T[]) => void
70
+ innerProps?: InnerProps
71
+ extraRender?: (info: ExtraRenderInfo) => VNode
72
+ }
73
+
74
+ export default defineComponent({
75
+ name: 'VirtualList',
76
+ props: {
77
+ prefixCls: { type: String, default: 'vc-virtual-list' },
78
+ data: { type: Array as PropType<any[]> },
79
+ height: Number,
80
+ itemHeight: Number,
81
+ fullHeight: { type: Boolean, default: true },
82
+ itemKey: { type: [String, Number, Function] as PropType<Key | ((item: any) => Key)>, required: true },
83
+ component: { type: String, default: 'div' },
84
+ virtual: { type: Boolean, default: true },
85
+ onScroll: Function as PropType<(e: Event) => void>,
86
+ onVirtualScroll: Function as PropType<(info: ScrollInfo) => void>,
87
+ onVisibleChange: Function as PropType<(visibleList: any[], fullList: any[]) => void>,
88
+ innerProps: Object as PropType<InnerProps>,
89
+ extraRender: Function as PropType<(info: ExtraRenderInfo) => VNode>,
90
+ },
91
+ setup(props, { expose, attrs, slots }) {
92
+ // =============================== Item Key ===============================
93
+ const getKey = (item: any): Key => {
94
+ if (typeof props.itemKey === 'function') {
95
+ return props.itemKey(item)
96
+ }
97
+ return item?.[props.itemKey as string]
98
+ }
99
+
100
+ // ================================ Height ================================
101
+ const [setInstanceRef, collectHeight, heights, heightUpdatedMark] = useHeights(
102
+ getKey,
103
+ undefined,
104
+ undefined,
105
+ )
106
+
107
+ // ================================= MISC =================================
108
+ const mergedData = computed(() => props.data || EMPTY_DATA)
109
+
110
+ const useVirtual = computed(
111
+ () => !!(props.virtual !== false && props.height && props.itemHeight),
112
+ )
113
+
114
+ const containerHeight = computed(() =>
115
+ Object.values(heights.maps).reduce((total: number, curr: number) => total + curr, 0),
116
+ )
117
+
118
+ const inVirtual = computed(() => {
119
+ const data = mergedData.value
120
+ return (
121
+ useVirtual.value
122
+ && data
123
+ && Math.max(props.itemHeight! * data.length, containerHeight.value) > props.height!
124
+ )
125
+ })
126
+
127
+ const componentRef = ref<HTMLDivElement>()
128
+ const fillerInnerRef = ref<HTMLDivElement>()
129
+ const containerRef = ref<HTMLDivElement>()
130
+ const verticalScrollBarRef = shallowRef<ScrollBarRef>()
131
+
132
+ const offsetTop = ref(0)
133
+ const offsetLeft = ref(0)
134
+ const scrollMoving = ref(false)
135
+
136
+ // ScrollBar related
137
+ const verticalScrollBarSpinSize = ref(0)
138
+ const scrollWidth = ref(0)
139
+
140
+ // ================================ Scroll ================================
141
+ function syncScrollTop(newTop: number | ((prev: number) => number)) {
142
+ let value: number
143
+ if (typeof newTop === 'function') {
144
+ value = newTop(offsetTop.value)
145
+ }
146
+ else {
147
+ value = newTop
148
+ }
149
+
150
+ const maxScrollHeight = scrollHeight!.value! - props.height!
151
+ const alignedTop = Math.max(0, Math.min(value, maxScrollHeight || 0))
152
+
153
+ if (componentRef.value) {
154
+ componentRef.value.scrollTop = alignedTop
155
+ }
156
+ offsetTop.value = alignedTop
157
+ }
158
+
159
+ // ================================ Range ================================
160
+ useDiffItem(mergedData, getKey)
161
+
162
+ // ========================== Visible Calculation =========================
163
+ const scrollHeight = ref(0)
164
+ const start = ref(0)
165
+ const end = ref(0)
166
+ const fillerOffset = ref<number | undefined>(undefined)
167
+
168
+ watch(
169
+ [
170
+ () => inVirtual.value,
171
+ () => useVirtual.value,
172
+ () => offsetTop.value,
173
+ () => mergedData.value,
174
+ () => heightUpdatedMark.value,
175
+ () => props.height,
176
+ ],
177
+ () => {
178
+ if (!useVirtual.value) {
179
+ scrollHeight.value = 0
180
+ start.value = 0
181
+ end.value = mergedData.value.length - 1
182
+ fillerOffset.value = undefined
183
+ return
184
+ }
185
+
186
+ if (!inVirtual.value) {
187
+ scrollHeight.value = fillerInnerRef.value?.offsetHeight || 0
188
+ start.value = 0
189
+ end.value = mergedData.value.length - 1
190
+ fillerOffset.value = undefined
191
+ return
192
+ }
193
+
194
+ let itemTop = 0
195
+ let startIndex: number | undefined
196
+ let startOffset: number | undefined
197
+ let endIndex: number | undefined
198
+
199
+ const dataLen = mergedData.value.length
200
+ const data = mergedData.value
201
+
202
+ for (let i = 0; i < dataLen; i += 1) {
203
+ const item = data[i]
204
+ const key = getKey(item)
205
+
206
+ const cacheHeight = heights.get(key)
207
+ const currentItemBottom = itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight)
208
+
209
+ if (currentItemBottom >= offsetTop.value && startIndex === undefined) {
210
+ startIndex = i
211
+ startOffset = itemTop
212
+ }
213
+
214
+ if (currentItemBottom > offsetTop.value + props.height! && endIndex === undefined) {
215
+ endIndex = i
216
+ }
217
+
218
+ itemTop = currentItemBottom
219
+ }
220
+
221
+ if (startIndex === undefined) {
222
+ startIndex = 0
223
+ startOffset = 0
224
+ endIndex = Math.ceil(props.height! / props.itemHeight!)
225
+ }
226
+ if (endIndex === undefined) {
227
+ endIndex = mergedData.value.length - 1
228
+ }
229
+
230
+ endIndex = Math.min(endIndex + 1, mergedData.value.length - 1)
231
+
232
+ scrollHeight.value = itemTop
233
+ start.value = startIndex
234
+ end.value = endIndex
235
+ fillerOffset.value = startOffset
236
+ },
237
+ { immediate: true },
238
+ )
239
+
240
+ // Sync scroll top when height changes
241
+ watch(
242
+ () => scrollHeight.value,
243
+ () => {
244
+ const changedRecord = heights.getRecord()
245
+ if (changedRecord.size === 1) {
246
+ const recordKey = Array.from(changedRecord.keys())[0]
247
+ const prevCacheHeight = changedRecord.get(recordKey)
248
+
249
+ const startItem = mergedData.value[start.value]
250
+ if (startItem && prevCacheHeight === undefined) {
251
+ const startIndexKey = getKey(startItem)
252
+ if (startIndexKey === recordKey) {
253
+ const realStartHeight = heights.get(recordKey)
254
+ const diffHeight = realStartHeight - props.itemHeight!
255
+ syncScrollTop(ori => ori + diffHeight)
256
+ }
257
+ }
258
+ }
259
+
260
+ heights.resetRecord()
261
+ },
262
+ )
263
+
264
+ // ================================= Size =================================
265
+ const size = ref({ width: 0, height: props.height || 0 })
266
+
267
+ const onHolderResize = (sizeInfo: { offsetWidth: number, offsetHeight: number }) => {
268
+ size.value = {
269
+ width: sizeInfo.offsetWidth,
270
+ height: sizeInfo.offsetHeight,
271
+ }
272
+ }
273
+
274
+ // =============================== Scroll ===============================
275
+ const getVirtualScrollInfo = () => ({
276
+ x: offsetLeft.value,
277
+ y: offsetTop.value,
278
+ })
279
+
280
+ const lastVirtualScrollInfo = ref(getVirtualScrollInfo())
281
+
282
+ const triggerScroll = (params?: { x?: number, y?: number }) => {
283
+ if (props.onVirtualScroll) {
284
+ const nextInfo = { ...getVirtualScrollInfo(), ...params }
285
+
286
+ if (
287
+ lastVirtualScrollInfo.value.x !== nextInfo.x
288
+ || lastVirtualScrollInfo.value.y !== nextInfo.y
289
+ ) {
290
+ props.onVirtualScroll(nextInfo)
291
+ lastVirtualScrollInfo.value = nextInfo
292
+ }
293
+ }
294
+ }
295
+
296
+ // ========================== Scroll Position ===========================
297
+ const isScrollAtTop = computed(() => offsetTop.value === 0)
298
+ const isScrollAtBottom = computed(() => offsetTop.value + props.height! >= scrollHeight.value)
299
+ const isScrollAtLeft = computed(() => offsetLeft.value === 0)
300
+ const isScrollAtRight = computed(() => offsetLeft.value + size.value.width >= scrollWidth.value)
301
+
302
+ // ========================== Wheel & Touch =========================
303
+ const delayHideScrollBar = () => {
304
+ verticalScrollBarRef.value?.delayHidden()
305
+ }
306
+
307
+ const [onWheel] = useFrameWheel(
308
+ inVirtual,
309
+ isScrollAtTop,
310
+ isScrollAtBottom,
311
+ isScrollAtLeft,
312
+ isScrollAtRight,
313
+ false, // horizontalScroll
314
+ (offsetY, isHorizontal) => {
315
+ if (isHorizontal) {
316
+ // Not implemented yet
317
+ }
318
+ else {
319
+ syncScrollTop(top => top + offsetY)
320
+ }
321
+ },
322
+ )
323
+
324
+ useMobileTouchMove(
325
+ inVirtual,
326
+ componentRef,
327
+ (isHorizontal, offset, _smoothOffset, _e) => {
328
+ if (isHorizontal) {
329
+ // Not implemented yet
330
+ return false
331
+ }
332
+ else {
333
+ syncScrollTop(top => top + offset)
334
+ return true
335
+ }
336
+ },
337
+ )
338
+
339
+ useScrollDrag(
340
+ inVirtual,
341
+ componentRef,
342
+ (offset) => {
343
+ syncScrollTop(top => top + offset)
344
+ },
345
+ )
346
+
347
+ // ========================== ScrollBar =========================
348
+ const onScrollBar = (newScrollOffset: number, horizontal?: boolean) => {
349
+ const newOffset = newScrollOffset
350
+ if (horizontal) {
351
+ // Not implemented yet
352
+ }
353
+ else {
354
+ syncScrollTop(newOffset)
355
+ }
356
+ }
357
+
358
+ const onScrollbarStartMove = () => {
359
+ scrollMoving.value = true
360
+ }
361
+
362
+ const onScrollbarStopMove = () => {
363
+ scrollMoving.value = false
364
+ }
365
+
366
+ // Calculate ScrollBar spin size
367
+ watch(
368
+ [() => props.height, () => scrollHeight.value, () => inVirtual.value, () => size.value.height],
369
+ () => {
370
+ if (inVirtual.value && props.height && scrollHeight.value) {
371
+ // First parameter is container size, second is scroll range
372
+ verticalScrollBarSpinSize.value = getSpinSize(size.value.height, scrollHeight.value)
373
+ }
374
+ },
375
+ { immediate: true },
376
+ )
377
+
378
+ function onFallbackScroll(e: Event) {
379
+ const target = e.currentTarget as HTMLDivElement
380
+ const newScrollTop = target.scrollTop
381
+ if (newScrollTop !== offsetTop.value) {
382
+ syncScrollTop(newScrollTop)
383
+ }
384
+
385
+ props.onScroll?.(e)
386
+ triggerScroll()
387
+ }
388
+
389
+ // ================================= Ref ==================================
390
+ const scrollTo = (config?: number | ScrollConfig) => {
391
+ if (config === null || config === undefined) {
392
+ return
393
+ }
394
+
395
+ if (typeof config === 'number') {
396
+ syncScrollTop(config)
397
+ }
398
+ else if (config && typeof config === 'object') {
399
+ let scrollTop: number | undefined
400
+
401
+ if ('left' in config) {
402
+ offsetLeft.value = config.left || 0
403
+ }
404
+
405
+ if ('top' in config) {
406
+ scrollTop = config.top
407
+ }
408
+ else if ('index' in config) {
409
+ const index = config.index || 0
410
+ const item = mergedData.value[index]
411
+ if (item) {
412
+ let itemTop = 0
413
+ for (let i = 0; i < index; i += 1) {
414
+ const key = getKey(mergedData.value[i])
415
+ const cacheHeight = heights.get(key)
416
+ itemTop += cacheHeight === undefined ? props.itemHeight! : cacheHeight
417
+ }
418
+ scrollTop = itemTop
419
+ }
420
+ }
421
+ else if ('key' in config) {
422
+ const index = mergedData.value.findIndex(item => getKey(item) === config.key)
423
+ if (index >= 0) {
424
+ let itemTop = 0
425
+ for (let i = 0; i < index; i += 1) {
426
+ const key = getKey(mergedData.value[i])
427
+ const cacheHeight = heights.get(key)
428
+ itemTop += cacheHeight === undefined ? props.itemHeight! : cacheHeight
429
+ }
430
+ scrollTop = itemTop
431
+ }
432
+ }
433
+
434
+ if (scrollTop !== undefined) {
435
+ syncScrollTop(scrollTop)
436
+ }
437
+ }
438
+ }
439
+
440
+ expose({
441
+ nativeElement: containerRef,
442
+ getScrollInfo: getVirtualScrollInfo,
443
+ scrollTo,
444
+ })
445
+
446
+ // ================================ Effect ================================
447
+ watch(
448
+ [() => start.value, () => end.value, () => mergedData.value],
449
+ () => {
450
+ if (props.onVisibleChange) {
451
+ const renderList = mergedData.value.slice(start.value, end.value + 1)
452
+ props.onVisibleChange(renderList, mergedData.value)
453
+ }
454
+ },
455
+ )
456
+
457
+ // ================================ Render ================================
458
+ const renderChildren = () => {
459
+ const children: VNode[] = []
460
+ const data = mergedData.value
461
+ const defaultSlot = slots.default
462
+
463
+ if (!defaultSlot) {
464
+ return children
465
+ }
466
+
467
+ for (let i = start.value; i <= end.value; i += 1) {
468
+ const item = data[i]
469
+ const key = getKey(item)
470
+ // Call the slot function with item, index, and props
471
+ const nodes = defaultSlot({
472
+ item,
473
+ index: i,
474
+ style: {},
475
+ offsetX: offsetLeft.value,
476
+ })
477
+
478
+ // Wrap each node in Item component
479
+ const node = Array.isArray(nodes) ? nodes[0] : nodes
480
+ if (node) {
481
+ children.push(
482
+ <Item key={key} setRef={ele => setInstanceRef(item, ele)}>
483
+ {node}
484
+ </Item>,
485
+ )
486
+ }
487
+ }
488
+
489
+ return children
490
+ }
491
+
492
+ const getSize = useGetSize(mergedData, getKey, heights, props.itemHeight!)
493
+
494
+ return () => {
495
+ const componentStyle: CSSProperties = {}
496
+ if (props.height) {
497
+ componentStyle[props.fullHeight ? 'height' : 'maxHeight'] = `${props.height}px`
498
+ Object.assign(componentStyle, ScrollStyle)
499
+
500
+ // Use custom ScrollBar when virtual scrolling is enabled
501
+ if (useVirtual.value) {
502
+ componentStyle.overflowY = 'hidden'
503
+
504
+ if (scrollWidth.value) {
505
+ componentStyle.overflowX = 'hidden'
506
+ }
507
+
508
+ if (scrollMoving.value) {
509
+ componentStyle.pointerEvents = 'none'
510
+ }
511
+ }
512
+ }
513
+
514
+ const extraContent = props.extraRender?.({
515
+ start: start.value,
516
+ end: end.value,
517
+ virtual: inVirtual.value,
518
+ offsetX: offsetLeft.value,
519
+ offsetY: fillerOffset.value || 0,
520
+ rtl: false,
521
+ getSize: getSize.value,
522
+ })
523
+
524
+ const Component = props.component as any
525
+
526
+ return (
527
+ <div
528
+ ref={containerRef}
529
+ style={{ position: 'relative', ...(attrs.style as CSSProperties) }}
530
+ class={[props.prefixCls, attrs.class]}
531
+ >
532
+ <ResizeObserver onResize={onHolderResize}>
533
+ <Component
534
+ class={`${props.prefixCls}-holder`}
535
+ style={componentStyle}
536
+ ref={componentRef}
537
+ onScroll={onFallbackScroll}
538
+ onWheel={onWheel}
539
+ onMouseenter={delayHideScrollBar}
540
+ >
541
+ <Filler
542
+ prefixCls={props.prefixCls}
543
+ height={scrollHeight.value}
544
+ offsetX={offsetLeft.value}
545
+ offsetY={fillerOffset.value}
546
+ onInnerResize={collectHeight}
547
+ ref={fillerInnerRef}
548
+ innerProps={props.innerProps}
549
+ rtl={false}
550
+ extra={extraContent}
551
+ >
552
+ {renderChildren()}
553
+ </Filler>
554
+ </Component>
555
+ </ResizeObserver>
556
+
557
+ {/* ScrollBar */}
558
+ {inVirtual.value && scrollHeight.value > (props.height || 0) && (
559
+ <ScrollBar
560
+ ref={verticalScrollBarRef}
561
+ prefixCls={props.prefixCls}
562
+ scrollOffset={offsetTop.value}
563
+ scrollRange={scrollHeight.value}
564
+ rtl={false}
565
+ onScroll={onScrollBar}
566
+ onStartMove={onScrollbarStartMove}
567
+ onStopMove={onScrollbarStopMove}
568
+ spinSize={verticalScrollBarSpinSize.value}
569
+ containerSize={size.value.height}
570
+ showScrollBar="optional"
571
+ />
572
+ )}
573
+ </div>
574
+ )
575
+ }
576
+ },
577
+ })