@@ -28,7 +28,8 @@ export const defaultProps = {
28
28
}
29
29
30
30
export interface VirtualListHandle {
31
- scrollToIndex ( index : number ) : void
31
+ scrollToIndex ( index : number , block ?: 'start' | 'end' | 'center' | 'nearest' ) : void
32
+ forceUpdate ( ) : void
32
33
}
33
34
34
35
export const VirtualList = forwardRef ( function < ITEM > (
@@ -41,9 +42,11 @@ export const VirtualList = forwardRef(function <ITEM>(
41
42
const list = useRef < HTMLDivElement > ( null ) ;
42
43
const listInner = useRef < HTMLDivElement > ( null ) ;
43
44
const prevScrollTop = useRef ( 0 ) ;
45
+ const scrollToIndexRef = useRef < { index : number , block : string } > ( ) ;
44
46
const [ scrollTop , setscrollTop ] = useState ( 0 ) ;
45
47
const [ listSize , setlistSize ] = useState ( props . listSize ! ) ;
46
-
48
+ const [ forceRerender , setforceRerender ] = useState ( [ ] ) ; // change value to force rerender
49
+ const ignoreScrollOnce = useRef ( false ) ;
47
50
//
48
51
const totalSpace = itemSize * count
49
52
let topSpace = scrollTop - buffer
@@ -64,12 +67,12 @@ export const VirtualList = forwardRef(function <ITEM>(
64
67
} else {
65
68
endIndex = count - Math . floor ( bottomSpace / itemSize )
66
69
}
67
- const mainVisibleIndexes = Array . from ( { length : endIndex - startIndex } , ( _ , index ) => index + startIndex ) ;
68
- let visibleIndexes = mainVisibleIndexes . concat ( props . persistentIndices || [ ] )
70
+ const mainVisibleIndices = Array . from ( { length : endIndex - startIndex } , ( _ , index ) => index + startIndex ) ;
71
+ let visibleIndices = mainVisibleIndices . concat ( props . persistentIndices || [ ] )
69
72
if ( props . persistentIndices ?. length ) {
70
- visibleIndexes = [ ...new Set ( visibleIndexes ) ] . sort ( ( a , b ) => a - b )
73
+ visibleIndices = [ ...new Set ( visibleIndices ) ] . sort ( ( a , b ) => a - b )
71
74
}
72
- const visible = visibleIndexes . map ( i => props . items [ i ] )
75
+ const visible = visibleIndices . map ( i => props . items [ i ] )
73
76
74
77
//
75
78
const listInnerStyle : any = { paddingTop : `${ topSpace } px` , boxSizing : 'border-box' }
@@ -78,39 +81,72 @@ export const VirtualList = forwardRef(function <ITEM>(
78
81
} else {
79
82
listInnerStyle [ 'height' ] = `${ totalSpace } px`
80
83
}
84
+
81
85
useLayoutEffect ( ( ) => {
82
86
setlistSize ( list . current ! . clientHeight )
87
+ // get avg item size
83
88
if ( props . itemSize == null ) {
84
- // get avg item size
85
89
let count = 0
86
90
let totalHeight = 0
91
+ const persistentIndices = new Set ( props . persistentIndices || [ ] )
92
+ let i = - 1
87
93
for ( const el of listInner . current ! . children ) {
94
+ i ++
95
+ if ( persistentIndices . has ( visibleIndices [ i ] ) ) {
96
+ continue
97
+ }
88
98
const style = getComputedStyle ( el )
89
99
totalHeight += ( el as HTMLElement ) . offsetHeight + parseFloat ( style . marginTop ) + parseFloat ( style . marginBottom )
90
100
count ++
91
101
}
92
102
setitemSize ( totalHeight / count )
93
103
}
94
- } , [ props . itemSize , props . items ] ) ;
104
+ } , [ props . itemSize , props . items , forceRerender ] ) ;
95
105
//
96
106
const handleScroll = ( ) => {
107
+ if ( ignoreScrollOnce . current ) {
108
+ ignoreScrollOnce . current = false
109
+ return
110
+ }
97
111
setlistSize ( list . current ! . clientHeight )
98
112
const scrollTop2 = list . current ! . scrollTop
99
113
if ( Math . abs ( prevScrollTop . current - scrollTop2 ) > itemSize ) {
100
114
setscrollTop ( scrollTop2 )
101
115
prevScrollTop . current = scrollTop2
116
+ } else if ( scrollToIndexRef . current ) {
117
+ setforceRerender ( [ ] )
102
118
}
103
119
}
104
120
//
105
121
useImperativeHandle ( ref , ( ) => ( {
106
- scrollToIndex : ( index : number ) => {
122
+ scrollToIndex ( index , block = 'start' ) {
123
+ scrollToIndexRef . current = {
124
+ index,
125
+ block
126
+ }
107
127
list . current ! . scrollTop = index * itemSize
108
128
} ,
109
- } ) , [ ] ) ;
129
+ forceUpdate ( ) {
130
+ setforceRerender ( [ ] )
131
+ }
132
+ } ) , [ itemSize ] ) ;
133
+ useLayoutEffect ( ( ) => {
134
+ if ( scrollToIndexRef . current ) {
135
+ const { index, block } = scrollToIndexRef . current ;
136
+ scrollToIndexRef . current = undefined
137
+ const indexInVisible = visibleIndices . indexOf ( index )
138
+ const el = listInner . current ! . children [ indexInVisible ] as HTMLElement
139
+ if ( el ) {
140
+ // @ts -ignore
141
+ el . scrollIntoView ( { block } )
142
+ ignoreScrollOnce . current = true
143
+ }
144
+ }
145
+ } , [ visibleIndices ] )
110
146
//
111
147
return < div ref = { list } onScroll = { handleScroll } className = { props . className } style = { { overflow : 'auto' , ...props . style } } >
112
148
< div ref = { listInner } style = { { display : 'flex' , flexDirection : 'column' , ...listInnerStyle } } >
113
- { visible . map ( ( item , i ) => props . renderItem ( item , visibleIndexes [ i ] ) ) }
149
+ { visible . map ( ( item , i ) => props . renderItem ( item , visibleIndices [ i ] ) ) }
114
150
</ div >
115
151
</ div >
116
152
} )
0 commit comments