@@ -70,6 +70,17 @@ const ScrollRegionStyle = styled.div<{ isSafari: boolean }>`
70
70
}
71
71
` ;
72
72
73
+ // Browser's maximum div height limit. Varies a bit by browsers.
74
+ const BROWSER_MAX_DIV_HEIGHT = 33_554_400 ;
75
+ // Maximum height of a single padder segment to avoid browser performance issues.
76
+ // Padders are invisible div elements that create the scrollable area in the DOM.
77
+ // They trick the browser into showing a scrollbar for the full virtual content height
78
+ // without actually rendering millions of rows. We create multiple smaller padders
79
+ // (max 5M pixels each) instead of one large padder to avoid browser performance issues.
80
+ // The actual grid content is absolutely positioned and rendered on top of these padders
81
+ // based on the current scroll position.
82
+ const MAX_PADDER_SEGMENT_HEIGHT = 5_000_000 ;
83
+
73
84
type ScrollLock = [ undefined , number ] | [ number , undefined ] | undefined ;
74
85
75
86
function useTouchUpDelayed ( delay : number ) : boolean {
@@ -108,6 +119,17 @@ function useTouchUpDelayed(delay: number): boolean {
108
119
return hasTouches ;
109
120
}
110
121
122
+ /**
123
+ * InfiniteScroller provides virtual scrolling capabilities for the data grid.
124
+ * It handles the mapping between DOM scroll positions and virtual scroll positions
125
+ * when the content height exceeds browser limitations.
126
+ *
127
+ * Browser Limitations:
128
+ * - Most browsers limit div heights to ~33.5 million pixels
129
+ * - With large datasets (e.g., 100M rows × 31px = 3.1B pixels), we exceed this limit
130
+ * - This component uses an offset-based approach to map the limited DOM scroll range
131
+ * to the full virtual scroll range
132
+ */
111
133
export const InfiniteScroller : React . FC < Props > = p => {
112
134
const {
113
135
children,
@@ -131,11 +153,26 @@ export const InfiniteScroller: React.FC<Props> = p => {
131
153
const rightElementSticky = rightElementProps ?. sticky ?? false ;
132
154
const rightElementFill = rightElementProps ?. fill ?? false ;
133
155
134
- const offsetY = React . useRef ( 0 ) ;
156
+ // Track the virtual scroll position directly for smooth scrolling
157
+ const virtualScrollY = React . useRef ( 0 ) ;
135
158
const lastScrollY = React . useRef ( 0 ) ;
136
159
const scroller = React . useRef < HTMLDivElement | null > ( null ) ;
137
160
138
161
const dpr = typeof window === "undefined" ? 1 : window . devicePixelRatio ;
162
+ const lastDpr = React . useRef ( dpr ) ;
163
+
164
+ // Reset scroll tracking when device pixel ratio changes (e.g., browser zoom)
165
+ React . useEffect ( ( ) => {
166
+ if ( lastDpr . current !== dpr ) {
167
+ virtualScrollY . current = 0 ;
168
+ lastScrollY . current = 0 ;
169
+ lastDpr . current = dpr ;
170
+ const el = scroller . current ;
171
+ if ( el !== null ) {
172
+ onScrollRef . current ( el . scrollLeft , el . scrollTop ) ;
173
+ }
174
+ }
175
+ } , [ dpr ] ) ;
139
176
140
177
const lastScrollPosition = React . useRef ( {
141
178
scrollLeft : 0 ,
@@ -202,16 +239,35 @@ export const InfiniteScroller: React.FC<Props> = p => {
202
239
const scrollableHeight = el . scrollHeight - cHeight ;
203
240
lastScrollY . current = newY ;
204
241
205
- if (
206
- scrollableHeight > 0 &&
207
- ( Math . abs ( delta ) > 2000 || newY === 0 || newY === scrollableHeight ) &&
208
- scrollHeight > el . scrollHeight + 5
209
- ) {
210
- const prog = newY / scrollableHeight ;
211
- const recomputed = ( scrollHeight - cHeight ) * prog ;
212
- offsetY . current = recomputed - newY ;
242
+ // Calculate the virtual Y position
243
+ let virtualY : number ;
244
+
245
+ // When content height exceeds browser limits, use hybrid approach
246
+ if ( scrollableHeight > 0 && scrollHeight > el . scrollHeight + 5 ) {
247
+ // For large jumps (scrollbar interaction) or edge positions,
248
+ // recalculate position based on percentage
249
+ if ( Math . abs ( delta ) > 2000 || newY === 0 || newY === scrollableHeight ) {
250
+ const scrollProgress = Math . max ( 0 , Math . min ( 1 , newY / scrollableHeight ) ) ;
251
+ const virtualScrollableHeight = scrollHeight - cHeight ;
252
+ virtualY = scrollProgress * virtualScrollableHeight ;
253
+ // Update our tracked position for subsequent smooth scrolling
254
+ virtualScrollY . current = virtualY ;
255
+ } else {
256
+ // For smooth scrolling, apply the delta directly to virtual position
257
+ // This preserves 1:1 pixel movement for smooth scrolling
258
+ virtualScrollY . current -= delta ;
259
+ virtualY = virtualScrollY . current ;
260
+ }
261
+ } else {
262
+ // Direct mapping when within browser limits
263
+ virtualY = newY ;
264
+ virtualScrollY . current = virtualY ;
213
265
}
214
266
267
+ // Ensure virtual Y is within valid bounds
268
+ virtualY = Math . max ( 0 , Math . min ( virtualY , scrollHeight - cHeight ) ) ;
269
+ virtualScrollY . current = virtualY ; // Keep tracked position in bounds too
270
+
215
271
if ( lock !== undefined ) {
216
272
window . clearTimeout ( idleTimer . current ) ;
217
273
setIsIdle ( false ) ;
@@ -220,7 +276,7 @@ export const InfiniteScroller: React.FC<Props> = p => {
220
276
221
277
update ( {
222
278
x : scrollLeft ,
223
- y : newY + offsetY . current ,
279
+ y : virtualY ,
224
280
width : cWidth - paddingRight ,
225
281
height : cHeight - paddingBottom ,
226
282
paddingRight : rightWrapRef . current ?. clientWidth ?? 0 ,
@@ -256,9 +312,13 @@ export const InfiniteScroller: React.FC<Props> = p => {
256
312
257
313
let key = 0 ;
258
314
let h = 0 ;
315
+
316
+ // Ensure we don't create padders that exceed browser limits
317
+ const effectiveScrollHeight = Math . min ( scrollHeight , BROWSER_MAX_DIV_HEIGHT ) ;
318
+
259
319
padders . push ( < div key = { key ++ } style = { { width : scrollWidth , height : 0 } } /> ) ;
260
- while ( h < scrollHeight ) {
261
- const toAdd = Math . min ( 5_000_000 , scrollHeight - h ) ;
320
+ while ( h < effectiveScrollHeight ) {
321
+ const toAdd = Math . min ( MAX_PADDER_SEGMENT_HEIGHT , effectiveScrollHeight - h ) ;
262
322
padders . push ( < div key = { key ++ } style = { { width : 0 , height : toAdd } } /> ) ;
263
323
h += toAdd ;
264
324
}
@@ -304,7 +364,7 @@ export const InfiniteScroller: React.FC<Props> = p => {
304
364
marginBottom : - 40 ,
305
365
marginRight : paddingRight ,
306
366
flexGrow : rightElementFill ? 1 : undefined ,
307
- right : rightElementSticky ? paddingRight ?? 0 : undefined ,
367
+ right : rightElementSticky ? ( paddingRight ?? 0 ) : undefined ,
308
368
pointerEvents : "auto" ,
309
369
} } >
310
370
{ rightElement }
0 commit comments