1
- import { useRef } from "react" ;
1
+ import { useMemo , useRef , useState } from "react" ;
2
2
import { mergeProps } from "@react-aria/utils" ;
3
- import { useVirtualizer } from "@tanstack/react-virtual" ;
3
+ import { useVirtualizer , VirtualItem } from "@tanstack/react-virtual" ;
4
4
import { isEmpty } from "@nextui-org/shared-utils" ;
5
+ import { Node } from "@react-types/shared" ;
6
+ import { ScrollShadowProps , useScrollShadow } from "@nextui-org/scroll-shadow" ;
7
+ import { filterDOMProps } from "@nextui-org/react-utils" ;
5
8
6
9
import ListboxItem from "./listbox-item" ;
7
10
import ListboxSection from "./listbox-section" ;
@@ -11,8 +14,50 @@ import {UseListboxReturn} from "./use-listbox";
11
14
interface Props extends UseListboxReturn {
12
15
isVirtualized ?: boolean ;
13
16
virtualization ?: VirtualizationProps ;
17
+ /* Here in virtualized listbox, scroll shadow needs custom implementation. Hence this is the only way to pass props to scroll shadow */
18
+ scrollShadowProps ?: Partial < ScrollShadowProps > ;
14
19
}
15
20
21
+ const getItemSizesForCollection = ( collection : Node < object > [ ] , itemHeight : number ) => {
22
+ const sizes : number [ ] = [ ] ;
23
+
24
+ for ( const item of collection ) {
25
+ if ( item . type === "section" ) {
26
+ /* +1 for the section header */
27
+ sizes . push ( ( [ ...item . childNodes ] . length + 1 ) * itemHeight ) ;
28
+ } else {
29
+ sizes . push ( itemHeight ) ;
30
+ }
31
+ }
32
+
33
+ return sizes ;
34
+ } ;
35
+
36
+ const getScrollState = ( element : HTMLDivElement | null ) => {
37
+ if (
38
+ ! element ||
39
+ element . scrollTop === undefined ||
40
+ element . clientHeight === undefined ||
41
+ element . scrollHeight === undefined
42
+ ) {
43
+ return {
44
+ isTop : false ,
45
+ isBottom : false ,
46
+ isMiddle : false ,
47
+ } ;
48
+ }
49
+
50
+ const isAtTop = element . scrollTop === 0 ;
51
+ const isAtBottom = Math . ceil ( element . scrollTop + element . clientHeight ) >= element . scrollHeight ;
52
+ const isInMiddle = ! isAtTop && ! isAtBottom ;
53
+
54
+ return {
55
+ isTop : isAtTop ,
56
+ isBottom : isAtBottom ,
57
+ isMiddle : isInMiddle ,
58
+ } ;
59
+ } ;
60
+
16
61
const VirtualizedListbox = ( props : Props ) => {
17
62
const {
18
63
Component,
@@ -29,6 +74,7 @@ const VirtualizedListbox = (props: Props) => {
29
74
disableAnimation,
30
75
getEmptyContentProps,
31
76
getListProps,
77
+ scrollShadowProps,
32
78
} = props ;
33
79
34
80
const { virtualization} = props ;
@@ -45,24 +91,29 @@ const VirtualizedListbox = (props: Props) => {
45
91
46
92
const listHeight = Math . min ( maxListboxHeight , itemHeight * state . collection . size ) ;
47
93
48
- const parentRef = useRef ( null ) ;
94
+ const parentRef = useRef < HTMLDivElement > ( null ) ;
95
+ const itemSizes = useMemo (
96
+ ( ) => getItemSizesForCollection ( [ ...state . collection ] , itemHeight ) ,
97
+ [ state . collection , itemHeight ] ,
98
+ ) ;
49
99
50
100
const rowVirtualizer = useVirtualizer ( {
51
- count : state . collection . size ,
101
+ count : [ ... state . collection ] . length ,
52
102
getScrollElement : ( ) => parentRef . current ,
53
- estimateSize : ( ) => itemHeight ,
103
+ estimateSize : ( i ) => itemSizes [ i ] ,
54
104
} ) ;
55
105
56
106
const virtualItems = rowVirtualizer . getVirtualItems ( ) ;
57
107
58
- const renderRow = ( {
59
- index,
60
- style : virtualizerStyle ,
61
- } : {
62
- index : number ;
63
- style : React . CSSProperties ;
64
- } ) => {
65
- const item = [ ...state . collection ] [ index ] ;
108
+ /* Here we need the base props for scroll shadow, contains the className (scrollbar-hide and scrollshadow config based on the user inputs on select props) */
109
+ const { getBaseProps : getBasePropsScrollShadow } = useScrollShadow ( { ...scrollShadowProps } ) ;
110
+
111
+ const renderRow = ( virtualItem : VirtualItem ) => {
112
+ const item = [ ...state . collection ] [ virtualItem . index ] ;
113
+
114
+ if ( ! item ) {
115
+ return null ;
116
+ }
66
117
67
118
const itemProps = {
68
119
color,
@@ -74,6 +125,15 @@ const VirtualizedListbox = (props: Props) => {
74
125
...item . props ,
75
126
} ;
76
127
128
+ const virtualizerStyle = {
129
+ position : "absolute" as const ,
130
+ top : 0 ,
131
+ left : 0 ,
132
+ width : "100%" ,
133
+ height : `${ virtualItem . size } px` ,
134
+ transform : `translateY(${ virtualItem . start } px)` ,
135
+ } ;
136
+
77
137
if ( item . type === "section" ) {
78
138
return (
79
139
< ListboxSection
@@ -102,6 +162,12 @@ const VirtualizedListbox = (props: Props) => {
102
162
return listboxItem ;
103
163
} ;
104
164
165
+ const [ scrollState , setScrollState ] = useState ( {
166
+ isTop : false ,
167
+ isBottom : true ,
168
+ isMiddle : false ,
169
+ } ) ;
170
+
105
171
const content = (
106
172
< Component { ...getListProps ( ) } >
107
173
{ ! state . collection . size && ! hideEmptyContent && (
@@ -110,11 +176,18 @@ const VirtualizedListbox = (props: Props) => {
110
176
</ li >
111
177
) }
112
178
< div
179
+ { ...filterDOMProps ( getBasePropsScrollShadow ( ) ) }
113
180
ref = { parentRef }
181
+ data-bottom-scroll = { scrollState . isTop }
182
+ data-top-bottom-scroll = { scrollState . isMiddle }
183
+ data-top-scroll = { scrollState . isBottom }
114
184
style = { {
115
185
height : maxListboxHeight ,
116
186
overflow : "auto" ,
117
187
} }
188
+ onScroll = { ( e ) => {
189
+ setScrollState ( getScrollState ( e . target as HTMLDivElement ) ) ;
190
+ } }
118
191
>
119
192
{ listHeight > 0 && itemHeight > 0 && (
120
193
< div
@@ -124,19 +197,7 @@ const VirtualizedListbox = (props: Props) => {
124
197
position : "relative" ,
125
198
} }
126
199
>
127
- { virtualItems . map ( ( virtualItem ) =>
128
- renderRow ( {
129
- index : virtualItem . index ,
130
- style : {
131
- position : "absolute" ,
132
- top : 0 ,
133
- left : 0 ,
134
- width : "100%" ,
135
- height : `${ virtualItem . size } px` ,
136
- transform : `translateY(${ virtualItem . start } px)` ,
137
- } ,
138
- } ) ,
139
- ) }
200
+ { virtualItems . map ( ( virtualItem ) => renderRow ( virtualItem ) ) }
140
201
</ div >
141
202
) }
142
203
</ div >
0 commit comments