1
1
import * as React from 'react' ;
2
- import { findDOMNode } from 'react-dom' ;
3
2
import Filler from './Filler' ;
4
- import { getLocationItem , getScrollPercentage } from './util' ;
3
+ import { getLocationItem , getScrollPercentage , getNodeHeight } from './util' ;
5
4
6
5
type RenderFunc < T > = ( item : T ) => React . ReactNode ;
7
6
@@ -16,6 +15,8 @@ export interface ListProps<T> extends React.HTMLAttributes<any> {
16
15
interface ListState {
17
16
status : 'NONE' | 'MEASURE_START' | 'MEASURE_DONE' ;
18
17
18
+ scrollTop : number | null ;
19
+ scrollPtg : number ;
19
20
itemIndex : number ;
20
21
itemOffsetPtg : number ;
21
22
startIndex : number ;
@@ -40,6 +41,8 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
40
41
41
42
state : ListState = {
42
43
status : 'NONE' ,
44
+ scrollTop : null ,
45
+ scrollPtg : 0 ,
43
46
itemIndex : 0 ,
44
47
itemOffsetPtg : 0 ,
45
48
startIndex : 0 ,
@@ -50,65 +53,83 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
50
53
51
54
itemElements : { [ index : number ] : HTMLElement } = { } ;
52
55
56
+ itemElementHeights : { [ index : number ] : number } = { } ;
57
+
53
58
/**
54
- * Initial should sync with default scroll top
59
+ * Phase 1: Initial should sync with default scroll top
55
60
*/
56
61
public componentDidMount ( ) {
57
62
this . listRef . current . scrollTop = 0 ;
58
63
this . onScroll ( ) ;
59
64
}
60
65
66
+ /**
67
+ * Phase 4: Record used item height
68
+ * Phase 5: Trigger re-render to use correct position
69
+ */
61
70
public componentDidUpdate ( ) {
62
71
const { status, startIndex, endIndex } = this . state ;
63
72
if ( status === 'MEASURE_START' ) {
64
- const heightList : number [ ] = [ ] ;
73
+ // Record here since measure item height will get warning in `render`
65
74
for ( let index = startIndex ; index <= endIndex ; index += 1 ) {
66
- const element : HTMLElement = this . itemElements [ index ] ;
67
- heightList [ index ] =
68
- 'offsetHeight' in element
69
- ? element . offsetHeight
70
- : ( findDOMNode ( element ) as HTMLElement ) . offsetHeight ;
75
+ this . itemElementHeights [ index ] = getNodeHeight ( this . itemElements [ index ] ) ;
71
76
}
77
+
72
78
this . setState ( { status : 'MEASURE_DONE' } ) ;
73
79
}
74
80
}
75
81
82
+ public getItemHeight = ( index : number ) => this . itemElementHeights [ index ] || 0 ;
83
+
76
84
/**
77
85
* Phase 2: Trigger render since we should re-calculate current position.
78
86
*/
79
87
public onScroll = ( ) => {
80
88
const { dataSource, height, itemHeight } = this . props ;
81
89
82
- const scrollTopPtg = getScrollPercentage ( this . listRef . current ) ;
83
- const { index, offsetPtg } = getLocationItem ( scrollTopPtg , dataSource . length ) ;
90
+ const { scrollTop } = this . listRef . current ;
91
+
92
+ // Skip if `scrollTop` not change to avoid shake
93
+ if ( scrollTop === this . state . scrollTop ) {
94
+ return ;
95
+ }
96
+
97
+ const scrollPtg = getScrollPercentage ( this . listRef . current ) ;
98
+
99
+ const { index, offsetPtg } = getLocationItem ( scrollPtg , dataSource . length ) ;
84
100
const visibleCount = Math . ceil ( height / itemHeight ) ;
85
101
86
- const beforeCount = Math . ceil ( scrollTopPtg * visibleCount ) ;
87
- const afterCount = Math . ceil ( ( 1 - scrollTopPtg ) * visibleCount ) ;
102
+ const beforeCount = Math . ceil ( scrollPtg * visibleCount ) ;
103
+ const afterCount = Math . ceil ( ( 1 - scrollPtg ) * visibleCount ) ;
88
104
89
105
this . setState ( {
90
106
status : 'MEASURE_START' ,
107
+ scrollTop,
108
+ scrollPtg,
91
109
itemIndex : index ,
92
110
itemOffsetPtg : offsetPtg ,
93
111
startIndex : Math . max ( 0 , index - beforeCount ) ,
94
112
endIndex : Math . min ( dataSource . length - 1 , index + afterCount ) ,
95
113
} ) ;
96
114
} ;
97
115
98
- public renderChildren = ( list : T [ ] , renderFunc : RenderFunc < T > ) =>
116
+ /**
117
+ * Phase 4: Render item and get all the visible items height
118
+ */
119
+ public renderChildren = ( list : T [ ] , startIndex : number , renderFunc : RenderFunc < T > ) =>
99
120
// We should measure rendered item height
100
- list . map ( ( item , index ) => {
121
+ list . map ( ( item , index ) => {
101
122
const node = renderFunc ( item ) as React . ReactElement ;
123
+ const eleIndex = startIndex + index ;
102
124
103
125
// Pass `key` and `ref` for internal measure
104
126
return React . cloneElement ( node , {
105
- key : index ,
127
+ key : eleIndex ,
106
128
ref : ( ele : HTMLElement ) => {
107
- this . itemElements [ index ] = ele ;
129
+ this . itemElements [ eleIndex ] = ele ;
108
130
} ,
109
131
} ) ;
110
- } )
111
- ;
132
+ } ) ;
112
133
113
134
public render ( ) {
114
135
const {
@@ -125,28 +146,44 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
125
146
if ( height === undefined ) {
126
147
return (
127
148
< Component style = { style } { ...restProps } >
128
- { this . renderChildren ( dataSource , children ) }
149
+ { this . renderChildren ( dataSource , 0 , children ) }
129
150
</ Component >
130
151
) ;
131
152
}
132
153
133
- const { itemIndex , startIndex, endIndex } = this . state ;
154
+ const { status , startIndex, endIndex, itemIndex , itemOffsetPtg , scrollPtg } = this . state ;
134
155
135
156
const contentHeight = dataSource . length * itemHeight ;
136
157
158
+ // TODO: refactor
159
+ let startItemTop = 0 ;
160
+ if ( status === 'MEASURE_DONE' ) {
161
+ const locatedItemHeight = this . getItemHeight ( itemIndex ) ;
162
+ const locatedItemTop = scrollPtg * this . listRef . current . clientHeight ;
163
+ const locatedItemOffset = itemOffsetPtg * locatedItemHeight ;
164
+ const locatedItemMergedTop =
165
+ this . listRef . current . scrollTop + locatedItemTop - locatedItemOffset ;
166
+
167
+ startItemTop = locatedItemMergedTop ;
168
+ for ( let index = itemIndex - 1 ; index >= startIndex ; index -= 1 ) {
169
+ startItemTop -= this . getItemHeight ( index ) ;
170
+ }
171
+ }
172
+
137
173
return (
138
174
< Component
139
175
style = { {
140
176
...style ,
141
177
height,
142
178
overflowY : 'auto' ,
179
+ overflowAnchor : 'none' ,
143
180
} }
144
181
{ ...restProps }
145
182
onScroll = { this . onScroll }
146
183
ref = { this . listRef }
147
184
>
148
- < Filler height = { contentHeight } >
149
- { this . renderChildren ( dataSource . slice ( startIndex , endIndex + 1 ) , children ) }
185
+ < Filler height = { contentHeight } offset = { status === 'MEASURE_DONE' ? startItemTop : 0 } >
186
+ { this . renderChildren ( dataSource . slice ( startIndex , endIndex + 1 ) , startIndex , children ) }
150
187
</ Filler >
151
188
</ Component >
152
189
) ;
0 commit comments