11import { isArray } from 'lodash' ;
2- import React , { useCallback , useEffect , useLayoutEffect , useMemo , useRef } from 'react' ;
2+ import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
33import { flushSync } from 'react-dom' ;
44
5- import { useAsync , useImmer , useRefCallback } from '../../hooks' ;
5+ import { useAsync , useRefCallback } from '../../hooks' ;
66
77export interface DItemRenderProps {
88 'aria-setsize' ?: number ;
@@ -54,18 +54,15 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
5454 //#endregion
5555
5656 const dataRef = useRef < {
57- isFirst : boolean ;
57+ hasScrollChange : boolean ;
5858 hasInitFocus : boolean ;
5959 } > ( {
60- isFirst : true ,
60+ hasScrollChange : false ,
6161 hasInitFocus : dHasSelected ,
6262 } ) ;
6363
6464 const asyncCapture = useAsync ( ) ;
6565
66- const [ list , setList ] = useImmer < React . ReactNode [ ] > ( [ ] ) ;
67- const [ fillSize , setFillSize ] = useImmer < [ React . CSSProperties , React . CSSProperties ] > ( [ { } , { } ] ) ;
68-
6966 const [ flatOptions , focusIndex ] = useMemo ( ( ) => {
7067 const flatOptions : Array < T | undefined > = [ ] ;
7168 let focusIndex = - 1 ;
@@ -101,76 +98,77 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
10198 return [ flatOptions , hasFind ? focusIndex : - 1 ] ;
10299 } , [ dCompareOption , dEmpty , dList , dNestedKey , dFocusOption ] ) ;
103100
104- const updateList = useCallback ( ( ) => {
105- if ( listEl ) {
106- dataRef . current . isFirst = false ;
107-
108- const maxScrollSize = dItemSize * flatOptions . length + dPaddingSize * 2 - dSize ;
109- const scrollSize = Math . min ( maxScrollSize , dScrollY ? listEl . scrollTop : listEl . scrollLeft ) ;
110-
111- const startCount = Math . floor ( ( scrollSize - dPaddingSize ) / dItemSize ) - 2 ;
112- const endCount = Math . ceil ( ( scrollSize - dPaddingSize + dSize ) / dItemSize ) + 2 ;
113-
114- let count = 0 ;
115- let skipCount = 0 ;
116- let renderCount = 0 ;
117- const loop = ( arr : T [ ] ) => {
118- const list : React . ReactNode [ ] = [ ] ;
119- if ( arr . length === 0 ) {
120- if ( dEmpty ) {
121- count += 1 ;
122- if ( count > endCount ) {
123- return list ;
124- }
125- const shouldRender = count > startCount ;
126- if ( shouldRender ) {
101+ const getStates = useCallback ( ( ) => {
102+ const maxScrollSize = dItemSize * flatOptions . length + dPaddingSize * 2 - dSize ;
103+ const scrollSize = Math . min ( maxScrollSize , dScrollY ? listEl ?. scrollTop ?? 0 : listEl ?. scrollLeft ?? 0 ) ;
104+
105+ const startCount = Math . floor ( ( scrollSize - dPaddingSize ) / dItemSize ) - 2 ;
106+ const endCount = Math . ceil ( ( scrollSize - dPaddingSize + dSize ) / dItemSize ) + 2 ;
107+
108+ let count = 0 ;
109+ let skipCount = 0 ;
110+ let renderCount = 0 ;
111+ const loop = ( arr : T [ ] ) => {
112+ const list : React . ReactNode [ ] = [ ] ;
113+ if ( arr . length === 0 ) {
114+ if ( dEmpty ) {
115+ count += 1 ;
116+ if ( count > endCount ) {
117+ return list ;
118+ }
119+ const shouldRender = count > startCount ;
120+ if ( shouldRender ) {
121+ renderCount += 1 ;
122+ list . push ( dEmpty ) ;
123+ } else {
124+ skipCount += 1 ;
125+ }
126+ }
127+ } else {
128+ for ( let index = 0 ; index < arr . length ; index ++ ) {
129+ count += 1 ;
130+ if ( count > endCount ) {
131+ return list ;
132+ }
133+ const shouldRender = count > startCount ;
134+ if ( dNestedKey && isArray ( arr [ index ] [ dNestedKey ] ) ) {
135+ const children = loop ( arr [ index ] [ dNestedKey ] as T [ ] ) ;
136+ if ( shouldRender || children . length > 0 ) {
127137 renderCount += 1 ;
128- list . push ( dEmpty ) ;
138+ list . push ( dItemRender ( arr [ index ] , { children } ) ) ;
129139 } else {
130140 skipCount += 1 ;
131141 }
132- }
133- } else {
134- for ( let index = 0 ; index < arr . length ; index ++ ) {
135- count += 1 ;
136- if ( count > endCount ) {
137- return list ;
138- }
139- const shouldRender = count > startCount ;
140- if ( dNestedKey && isArray ( arr [ index ] [ dNestedKey ] ) ) {
141- const children = loop ( arr [ index ] [ dNestedKey ] as T [ ] ) ;
142- if ( shouldRender || children . length > 0 ) {
143- renderCount += 1 ;
144- list . push ( dItemRender ( arr [ index ] , { children } ) ) ;
145- } else {
146- skipCount += 1 ;
147- }
142+ } else {
143+ if ( shouldRender ) {
144+ renderCount += 1 ;
145+ list . push (
146+ dItemRender ( arr [ index ] , {
147+ 'aria-setsize' : arr . length ,
148+ 'aria-posinset' : index + 1 ,
149+ } )
150+ ) ;
148151 } else {
149- if ( shouldRender ) {
150- renderCount += 1 ;
151- list . push (
152- dItemRender ( arr [ index ] , {
153- 'aria-setsize' : arr . length ,
154- 'aria-posinset' : index + 1 ,
155- } )
156- ) ;
157- } else {
158- skipCount += 1 ;
159- }
152+ skipCount += 1 ;
160153 }
161154 }
162155 }
163- return list ;
164- } ;
165-
166- setList ( loop ( dList ) ) ;
156+ }
157+ return list ;
158+ } ;
167159
168- setFillSize ( [
160+ return {
161+ list : loop ( dList ) ,
162+ fillSize : [
169163 { [ dScrollY ? 'height' : 'width' ] : dItemSize * skipCount } ,
170164 { [ dScrollY ? 'height' : 'width' ] : dItemSize * ( flatOptions . length - skipCount - renderCount ) } ,
171- ] ) ;
172- }
173- } , [ dEmpty , dItemRender , dItemSize , dList , dNestedKey , dPaddingSize , dScrollY , dSize , flatOptions . length , listEl , setFillSize , setList ] ) ;
165+ ] ,
166+ } ;
167+ } , [ dEmpty , dItemRender , dItemSize , dList , dNestedKey , dPaddingSize , dScrollY , dSize , flatOptions . length , listEl ] ) ;
168+ const [ { list, fillSize } , _updateList ] = useState ( ( ) => getStates ( ) ) ;
169+ const updateList = useCallback ( ( ) => {
170+ _updateList ( getStates ( ) ) ;
171+ } , [ getStates ] ) ;
174172
175173 const handleScroll = useCallback (
176174 ( e ) => {
@@ -186,44 +184,47 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
186184 }
187185
188186 flushSync ( ( ) => updateList ( ) ) ;
187+ dataRef . current . hasScrollChange = false ;
189188 } ,
190189 [ dScrollY , listEl , onScroll , onScrollEnd , updateList ]
191190 ) ;
192191
193- useLayoutEffect ( ( ) => {
194- if ( ! dataRef . current . isFirst ) {
195- if ( dRendered ) {
196- if ( dataRef . current . hasInitFocus && listEl ) {
197- dataRef . current . hasInitFocus = false ;
198- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = focusIndex * dItemSize ;
199- } else {
192+ useEffect ( ( ) => {
193+ if ( dRendered ) {
194+ if ( dataRef . current . hasInitFocus && listEl ) {
195+ dataRef . current . hasInitFocus = false ;
196+ listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = focusIndex * dItemSize ;
197+ } else {
198+ if ( ! dataRef . current . hasScrollChange ) {
200199 updateList ( ) ;
201200 }
201+ dataRef . current . hasScrollChange = false ;
202202 }
203203 }
204204 // eslint-disable-next-line react-hooks/exhaustive-deps
205205 } , [ dRendered , updateList ] ) ;
206206
207- useLayoutEffect ( ( ) => {
208- if ( dataRef . current . isFirst ) {
209- updateList ( ) ;
210- }
211- } , [ updateList ] ) ;
212-
213207 useEffect ( ( ) => {
214208 const [ asyncGroup , asyncId ] = asyncCapture . createGroup ( ) ;
215209
216210 if ( listEl && dRendered && focusIndex !== - 1 ) {
211+ const changeScroll = ( num : number ) => {
212+ const pre = listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] ;
213+ listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = num ;
214+ const now = listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] ;
215+ dataRef . current . hasScrollChange = pre !== now ;
216+ } ;
217+
217218 const changeFocusByKeydown = ( next = true ) => {
218219 let index = focusIndex ;
219220 let option : T | undefined ;
220221 const getOption = ( ) => {
221222 if ( ! next && index === 0 ) {
222- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = 0 ;
223+ changeScroll ( 0 ) ;
223224 return ;
224225 }
225226 if ( next && index === flatOptions . length - 1 ) {
226- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = listEl [ dScrollY ? ' scrollHeight' : 'scrollWidth' ] ;
227+ changeScroll ( listEl [ dScrollY ? 'scrollHeight' : 'scrollWidth' ] ) ;
227228 return ;
228229 }
229230 index = next ? index + 1 : index - 1 ;
@@ -242,17 +243,17 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
242243 const listElClientSize = listEl [ dScrollY ? 'clientHeight' : 'clientWidth' ] ;
243244
244245 if ( listElScrollSize > elOffset [ 1 ] ) {
245- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = elOffset [ 0 ] - dPaddingSize ;
246+ changeScroll ( elOffset [ 0 ] - dPaddingSize ) ;
246247 } else if ( elOffset [ 0 ] > listElScrollSize + listElClientSize ) {
247- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = elOffset [ 1 ] - listElClientSize + dPaddingSize ;
248+ changeScroll ( elOffset [ 1 ] - listElClientSize + dPaddingSize ) ;
248249 } else {
249250 if ( next ) {
250251 if ( elOffset [ 1 ] > listElScrollSize + listElClientSize ) {
251- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = elOffset [ 1 ] - listElClientSize + dPaddingSize ;
252+ changeScroll ( elOffset [ 1 ] - listElClientSize + dPaddingSize ) ;
252253 }
253254 } else {
254255 if ( listElScrollSize > elOffset [ 0 ] ) {
255- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = elOffset [ 0 ] - dPaddingSize ;
256+ changeScroll ( elOffset [ 0 ] - dPaddingSize ) ;
256257 }
257258 }
258259 }
@@ -263,6 +264,8 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
263264
264265 asyncGroup . fromEvent < KeyboardEvent > ( window , 'keydown' ) . subscribe ( {
265266 next : ( e ) => {
267+ let option : T | undefined ;
268+
266269 switch ( e . code ) {
267270 case 'ArrowUp' :
268271 e . preventDefault ( ) ;
@@ -294,25 +297,27 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
294297
295298 case 'Home' :
296299 e . preventDefault ( ) ;
297- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = 0 ;
300+ changeScroll ( 0 ) ;
298301 for ( const item of flatOptions ) {
299302 if ( item && dCanSelectOption ( item ) ) {
300- onFocusChange ?. ( item ) ;
303+ option = item ;
301304 break ;
302305 }
303306 }
307+ onFocusChange ?.( option ?? null ) ;
304308 break ;
305309
306310 case 'End' :
307311 e . preventDefault ( ) ;
308- listEl [ dScrollY ? 'scrollTop' : 'scrollLeft' ] = listEl [ dScrollY ? ' scrollHeight' : 'scrollWidth' ] ;
312+ changeScroll ( listEl [ dScrollY ? 'scrollHeight' : 'scrollWidth' ] ) ;
309313 for ( let index = flatOptions . length - 1 ; index >= 0 ; index -- ) {
310314 const item = flatOptions [ index ] ;
311315 if ( item && dCanSelectOption ( item ) ) {
312- onFocusChange ?. ( item ) ;
316+ option = item ;
313317 break ;
314318 }
315319 }
320+ onFocusChange ?.( option ?? null ) ;
316321 break ;
317322
318323 default :
0 commit comments