11import React , {
22 useState ,
33 useMemo ,
4- useEffect , useRef , ReactNode
4+ useEffect , useRef , ReactNode , useLayoutEffect , useImperativeHandle , forwardRef ,
55} from 'react' ;
66
7+ type OptionalKeys < T > = {
8+ [ K in keyof T ] ?: T [ K ] ;
9+ } ;
10+
711export type Props < ITEM > = {
812 itemSize ?: number ,
913 buffer ?: number ,
1014 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 >
1320
1421export const defaultProps = {
1522 listSize : 1000 ,
1623}
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 >
1932) {
2033 const [ itemSize , setitemSize ] = useState ( props . itemSize || 100 ) ;
2134 const buffer = useMemo ( ( ) => props . buffer || Math . max ( itemSize * 5 , 100 ) , [ props . buffer , itemSize ] ) ;
@@ -24,7 +37,7 @@ export function VirtualList<ITEM>(
2437 const listInner = useRef < HTMLDivElement > ( null ) ;
2538 const prevScrollTop = useRef ( 0 ) ;
2639 const [ scrollTop , setscrollTop ] = useState ( 0 ) ;
27- const [ listSize , setlistSize ] = useState ( props . listSize ) ;
40+ const [ listSize , setlistSize ] = useState ( props . listSize ! ) ;
2841
2942 //
3043 const totalSpace = itemSize * count
@@ -38,22 +51,29 @@ export function VirtualList<ITEM>(
3851 } else {
3952 startIndex = Math . floor ( topSpace / itemSize )
4053 }
54+ if ( bottomSpace < 0 ) {
55+ bottomSpace = 0
56+ }
4157 if ( totalSpace <= listSize ) {
4258 endIndex = count
4359 } else {
4460 endIndex = count - Math . floor ( bottomSpace / itemSize )
4561 }
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 )
4866 }
49- const visible = props . items . slice ( startIndex , endIndex )
67+ const visible = visibleIndexes . map ( i => props . items [ i ] )
68+
69+ //
5070 const listInnerStyle : any = { paddingTop : `${ topSpace } px` , boxSizing : 'border-box' }
5171 if ( bottomSpace < itemSize * 5 ) {
5272 listInnerStyle [ 'paddingBottom' ] = `${ bottomSpace } px`
5373 } else {
5474 listInnerStyle [ 'height' ] = `${ totalSpace } px`
5575 }
56- useEffect ( ( ) => {
76+ useLayoutEffect ( ( ) => {
5777 setlistSize ( list . current ! . clientHeight )
5878 if ( props . itemSize == null ) {
5979 // get avg item size
@@ -77,11 +97,17 @@ export function VirtualList<ITEM>(
7797 }
7898 }
7999 //
100+ useImperativeHandle ( ref , ( ) => ( {
101+ scrollToIndex : ( index : number ) => {
102+ list . current ! . scrollTop = index * itemSize
103+ } ,
104+ } ) , [ ] ) ;
105+ //
80106 return < div ref = { list } onScroll = { handleScroll } className = { props . className } style = { { overflow : 'auto' , ...props . style } } >
81107 < 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 ] ) ) }
83109 </ div >
84110 </ div >
85- }
111+ } )
86112
87- VirtualList . defaultProps = defaultProps
113+ VirtualList . defaultProps = defaultProps
0 commit comments