Skip to content

Commit ed43f13

Browse files
lukasmasuchCopilot
andauthored
Fix crash with large grid and browser zoom (#1052)
* Fix crash with large grid and browser zoom * Fix * Simplify * Update comment * Update packages/core/src/internal/scrolling-data-grid/infinite-scroller.tsx Co-authored-by: Copilot <[email protected]> * Improvements * Apply comments * Update comment --------- Co-authored-by: Copilot <[email protected]>
1 parent 4cfd115 commit ed43f13

File tree

2 files changed

+78
-13
lines changed

2 files changed

+78
-13
lines changed

packages/core/src/internal/scrolling-data-grid/infinite-scroller.tsx

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ const ScrollRegionStyle = styled.div<{ isSafari: boolean }>`
7070
}
7171
`;
7272

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+
7384
type ScrollLock = [undefined, number] | [number, undefined] | undefined;
7485

7586
function useTouchUpDelayed(delay: number): boolean {
@@ -108,6 +119,17 @@ function useTouchUpDelayed(delay: number): boolean {
108119
return hasTouches;
109120
}
110121

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+
*/
111133
export const InfiniteScroller: React.FC<Props> = p => {
112134
const {
113135
children,
@@ -131,11 +153,26 @@ export const InfiniteScroller: React.FC<Props> = p => {
131153
const rightElementSticky = rightElementProps?.sticky ?? false;
132154
const rightElementFill = rightElementProps?.fill ?? false;
133155

134-
const offsetY = React.useRef(0);
156+
// Track the virtual scroll position directly for smooth scrolling
157+
const virtualScrollY = React.useRef(0);
135158
const lastScrollY = React.useRef(0);
136159
const scroller = React.useRef<HTMLDivElement | null>(null);
137160

138161
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]);
139176

140177
const lastScrollPosition = React.useRef({
141178
scrollLeft: 0,
@@ -202,16 +239,35 @@ export const InfiniteScroller: React.FC<Props> = p => {
202239
const scrollableHeight = el.scrollHeight - cHeight;
203240
lastScrollY.current = newY;
204241

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;
213265
}
214266

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+
215271
if (lock !== undefined) {
216272
window.clearTimeout(idleTimer.current);
217273
setIsIdle(false);
@@ -220,7 +276,7 @@ export const InfiniteScroller: React.FC<Props> = p => {
220276

221277
update({
222278
x: scrollLeft,
223-
y: newY + offsetY.current,
279+
y: virtualY,
224280
width: cWidth - paddingRight,
225281
height: cHeight - paddingBottom,
226282
paddingRight: rightWrapRef.current?.clientWidth ?? 0,
@@ -256,9 +312,13 @@ export const InfiniteScroller: React.FC<Props> = p => {
256312

257313
let key = 0;
258314
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+
259319
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);
262322
padders.push(<div key={key++} style={{ width: 0, height: toAdd }} />);
263323
h += toAdd;
264324
}
@@ -304,7 +364,7 @@ export const InfiniteScroller: React.FC<Props> = p => {
304364
marginBottom: -40,
305365
marginRight: paddingRight,
306366
flexGrow: rightElementFill ? 1 : undefined,
307-
right: rightElementSticky ? paddingRight ?? 0 : undefined,
367+
right: rightElementSticky ? (paddingRight ?? 0) : undefined,
308368
pointerEvents: "auto",
309369
}}>
310370
{rightElement}

packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ const GridScroller: React.FunctionComponent<ScrollingDataGridProps> = p => {
194194
}
195195
}
196196

197+
// Ensure cellY and cellBottom never exceed the actual row count
198+
// This is a safeguard to prevent unexpected out-of-bounds access with large datasets
199+
cellY = Math.max(0, Math.min(cellY, rows - 1));
200+
cellBottom = Math.max(cellY, Math.min(cellBottom, rows));
201+
197202
const rect: Rectangle = {
198203
x: cellX,
199204
y: cellY,

0 commit comments

Comments
 (0)