1
1
import React , {
2
2
useState ,
3
3
useMemo ,
4
- useEffect , useRef , ReactNode
4
+ useEffect , useRef , ReactNode , useLayoutEffect , useImperativeHandle , forwardRef ,
5
5
} from 'react' ;
6
6
7
+ type OptionalKeys < T > = {
8
+ [ K in keyof T ] ?: T [ K ] ;
9
+ } ;
10
+
7
11
export type Props < ITEM > = {
8
12
itemSize ?: number ,
9
13
buffer ?: number ,
10
14
items : ITEM [ ] ,
11
- renderItem : ( item : ITEM , index : number ) => ReactNode
12
- } & typeof defaultProps
15
+ renderItem : ( item : ITEM , index : number ) => ReactNode ,
16
+ sticky ?: number [ ] , // index[]
17
+ className ?: string ,
18
+ style ?: React . CSSProperties ,
19
+ } & OptionalKeys < typeof defaultProps >
13
20
14
21
export const defaultProps = {
15
22
listSize : 1000 ,
16
23
}
17
- export function VirtualList < ITEM > (
18
- props : Props < ITEM > & React . HTMLProps < HTMLElement > ,
24
+
25
+ export interface VirtualListHandle {
26
+ scrollToIndex ( index : number ) : void
27
+ }
28
+
29
+ export const VirtualList = forwardRef ( function < ITEM > (
30
+ props : Props < ITEM > ,
31
+ ref : React . ForwardedRef < VirtualListHandle >
19
32
) {
20
33
const [ itemSize , setitemSize ] = useState ( props . itemSize || 100 ) ;
21
34
const buffer = useMemo ( ( ) => props . buffer || Math . max ( itemSize * 5 , 100 ) , [ props . buffer , itemSize ] ) ;
@@ -24,7 +37,7 @@ export function VirtualList<ITEM>(
24
37
const listInner = useRef < HTMLDivElement > ( null ) ;
25
38
const prevScrollTop = useRef ( 0 ) ;
26
39
const [ scrollTop , setscrollTop ] = useState ( 0 ) ;
27
- const [ listSize , setlistSize ] = useState ( props . listSize ) ;
40
+ const [ listSize , setlistSize ] = useState ( props . listSize ! ) ;
28
41
29
42
//
30
43
const totalSpace = itemSize * count
@@ -38,22 +51,29 @@ export function VirtualList<ITEM>(
38
51
} else {
39
52
startIndex = Math . floor ( topSpace / itemSize )
40
53
}
54
+ if ( bottomSpace < 0 ) {
55
+ bottomSpace = 0
56
+ }
41
57
if ( totalSpace <= listSize ) {
42
58
endIndex = count
43
59
} else {
44
60
endIndex = count - Math . floor ( bottomSpace / itemSize )
45
61
}
46
- if ( bottomSpace < 0 ) {
47
- bottomSpace = 0
62
+ const mainVisibleIndexes = Array . from ( { length : endIndex - startIndex } , ( _ , index ) => index + startIndex ) ;
63
+ let visibleIndexes = mainVisibleIndexes . concat ( props . sticky || [ ] )
64
+ if ( props . sticky ?. length ) {
65
+ visibleIndexes = [ ...new Set ( visibleIndexes ) ] . sort ( ( a , b ) => a - b )
48
66
}
49
- const visible = props . items . slice ( startIndex , endIndex )
67
+ const visible = visibleIndexes . map ( i => props . items [ i ] )
68
+
69
+ //
50
70
const listInnerStyle : any = { paddingTop : `${ topSpace } px` , boxSizing : 'border-box' }
51
71
if ( bottomSpace < itemSize * 5 ) {
52
72
listInnerStyle [ 'paddingBottom' ] = `${ bottomSpace } px`
53
73
} else {
54
74
listInnerStyle [ 'height' ] = `${ totalSpace } px`
55
75
}
56
- useEffect ( ( ) => {
76
+ useLayoutEffect ( ( ) => {
57
77
setlistSize ( list . current ! . clientHeight )
58
78
if ( props . itemSize == null ) {
59
79
// get avg item size
@@ -77,11 +97,17 @@ export function VirtualList<ITEM>(
77
97
}
78
98
}
79
99
//
100
+ useImperativeHandle ( ref , ( ) => ( {
101
+ scrollToIndex : ( index : number ) => {
102
+ list . current ! . scrollTop = index * itemSize
103
+ } ,
104
+ } ) , [ ] ) ;
105
+ //
80
106
return < div ref = { list } onScroll = { handleScroll } className = { props . className } style = { { overflow : 'auto' , ...props . style } } >
81
107
< div ref = { listInner } style = { { display : 'flex' , flexDirection : 'column' , ...listInnerStyle } } >
82
- { visible . map ( ( item , i ) => props . renderItem ( item , i + startIndex ) ) }
108
+ { visible . map ( ( item , i ) => props . renderItem ( item , visibleIndexes [ i ] ) ) }
83
109
</ div >
84
110
</ div >
85
- }
111
+ } )
86
112
87
- VirtualList . defaultProps = defaultProps
113
+ VirtualList . defaultProps = defaultProps
0 commit comments