-
Notifications
You must be signed in to change notification settings - Fork 380
Expand file tree
/
Copy pathVirtualList.tsx
More file actions
128 lines (118 loc) · 3.41 KB
/
VirtualList.tsx
File metadata and controls
128 lines (118 loc) · 3.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import {
useVirtualizer,
Virtualizer,
type VirtualItem,
} from '@tanstack/react-virtual'
import React from 'react'
import { HorizontalContainer } from '../HorizontalContainer/HorizontalContainer'
import { cn } from '@sqlmesh-common/utils'
import { Button } from '../Button/Button'
import { ScrollContainer } from '../ScrollContainer/ScrollContainer'
import { VerticalContainer } from '../VerticalContainer/VerticalContainer'
export interface VirtualListProps<TItem> {
items: TItem[]
estimatedListItemHeight: number
renderListItem: (
item: TItem,
virtualItem?: VirtualItem,
virtualizer?: Virtualizer<HTMLDivElement, Element>,
) => React.ReactNode
isSelected?: (item: TItem) => boolean
className?: string
}
export function VirtualList<TItem>({
items,
estimatedListItemHeight,
renderListItem,
isSelected,
className,
}: VirtualListProps<TItem>) {
const scrollableAreaRef = React.useRef<HTMLDivElement>(null)
const [activeItemIndex] = React.useMemo(() => {
let activeIndex = -1
const itemsLength = items.length
for (let i = 0; i < itemsLength; i++) {
if (isSelected?.(items[i])) {
activeIndex = i
break
}
}
return [activeIndex]
}, [items, isSelected])
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => scrollableAreaRef.current,
estimateSize: () => estimatedListItemHeight,
})
const scrollToItem = React.useCallback(
({
itemIndex,
isSmoothScroll = true,
}: {
itemIndex: number
isSmoothScroll?: boolean
}): void => {
rowVirtualizer.scrollToIndex(itemIndex, {
align: 'center',
behavior: isSmoothScroll ? 'smooth' : 'auto',
})
},
[rowVirtualizer],
)
const isOutsideVisibleRange = React.useCallback(
(itemIndex: number): boolean => {
const range = rowVirtualizer.range
return (
range !== null &&
(range.startIndex > itemIndex || range?.endIndex < itemIndex)
)
},
[rowVirtualizer],
)
/**
* The return button should appear when the
* active item is available in the list (not
* filtered out) and it is not in the visible
* range of the virtualized list
*/
const shouldShowReturnButton =
activeItemIndex > -1 && isOutsideVisibleRange(activeItemIndex)
const rows = rowVirtualizer.getVirtualItems()
const totalSize = rowVirtualizer.getTotalSize()
return (
<HorizontalContainer
data-component="VirtualList"
className={cn('p-1', className)}
>
{shouldShowReturnButton && (
<Button
className="absolute left-[50%] translate-x-[-50%] z-10 shadow-md top-1"
onClick={() => scrollToItem({ itemIndex: activeItemIndex })}
size="2xs"
variant="alternative"
>
Scroll to selected
</Button>
)}
<ScrollContainer
ref={scrollableAreaRef}
className="h-auto overflow-auto"
>
<div
style={{
height: totalSize > 0 ? `${totalSize}px` : '100%',
contain: 'strict',
}}
>
<VerticalContainer
className="absolute top-0 left-0 px-1"
style={{ transform: `translateY(${rows[0]?.start ?? 0}px)` }}
role="list"
>
{rows.map(row => renderListItem(items[row.index]))}
</VerticalContainer>
</div>
</ScrollContainer>
</HorizontalContainer>
)
}