• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

glideapps / glide-data-grid / 15960192055

29 Jun 2025 10:48PM UTC coverage: 90.989% (-0.2%) from 91.171%
15960192055

Pull #1063

github

web-flow
Merge c75d567bb into 3041b6f44
Pull Request #1063: Use pointer events data grid

2906 of 3604 branches covered (80.63%)

32 of 32 new or added lines in 2 files covered. (100.0%)

37 existing lines in 3 files now uncovered.

17913 of 19687 relevant lines covered (90.99%)

3061.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.96
/packages/core/src/internal/scrolling-data-grid/infinite-scroller.tsx
1
import { styled } from "@linaria/react";
1✔
2
import type { Rectangle } from "../../index.js";
1✔
3
import * as React from "react";
1✔
4
import { useResizeDetector } from "../../common/resize-detector.js";
1✔
5
import { browserIsSafari } from "../../common/browser-detect.js";
1✔
6
import { useEventListener } from "../../common/utils.js";
1✔
7
import useKineticScroll from "./use-kinetic-scroll.js";
1✔
8

1✔
9
interface Props {
1✔
10
    readonly children: React.ReactNode;
1✔
11
    readonly className?: string;
1✔
12
    readonly preventDiagonalScrolling?: boolean;
1✔
13
    readonly draggable: boolean;
1✔
14
    readonly paddingRight?: number;
1✔
15
    readonly paddingBottom?: number;
1✔
16
    readonly clientHeight: number;
1✔
17
    readonly scrollWidth: number;
1✔
18
    readonly scrollHeight: number;
1✔
19
    readonly initialSize?: readonly [width: number, height: number];
1✔
20
    readonly rightElementProps?: {
1✔
21
        readonly sticky?: boolean;
1✔
22
        readonly fill?: boolean;
1✔
23
    };
1✔
24
    readonly rightElement?: React.ReactNode;
1✔
25
    readonly kineticScrollPerfHack?: boolean;
1✔
26
    readonly scrollRef?: React.MutableRefObject<HTMLDivElement | null>;
1✔
27
    readonly update: (region: Rectangle & { paddingRight: number }) => void;
1✔
28
}
1✔
29

1✔
30
const ScrollRegionStyle = styled.div<{ isSafari: boolean }>`
1✔
31
    .dvn-scroller {
1✔
32
        overflow: ${p => (p.isSafari ? "scroll" : "auto")};
1✔
33
        transform: translate3d(0, 0, 0);
1✔
34
    }
1✔
35

1✔
36
    .dvn-hidden {
1✔
37
        visibility: hidden;
1✔
38
    }
1✔
39

1✔
40
    .dvn-scroll-inner {
1✔
41
        display: flex;
1✔
42
        pointer-events: none;
1✔
43

1✔
44
        > * {
1✔
45
            flex-shrink: 0;
1✔
46
        }
1✔
47

1✔
48
        .dvn-spacer {
1✔
49
            flex-grow: 1;
1✔
50
        }
1✔
51

1✔
52
        .dvn-stack {
1✔
53
            display: flex;
1✔
54
            flex-direction: column;
1✔
55
        }
1✔
56
    }
1✔
57

1✔
58
    .dvn-underlay > * {
1✔
59
        position: absolute;
1✔
60
        left: 0;
1✔
61
        top: 0;
1✔
62
    }
1✔
63

1✔
64
    canvas {
1✔
65
        outline: none;
1✔
66

1✔
67
        * {
1✔
68
            height: 0;
1✔
69
        }
1✔
70
    }
1✔
71
`;
1✔
72

1✔
73
// Browser's maximum div height limit. Varies a bit by browsers.
1✔
74
const BROWSER_MAX_DIV_HEIGHT = 33_554_400;
1✔
75
// Maximum height of a single padder segment to avoid browser performance issues.
1✔
76
// Padders are invisible div elements that create the scrollable area in the DOM.
1✔
77
// They trick the browser into showing a scrollbar for the full virtual content height
1✔
78
// without actually rendering millions of rows. We create multiple smaller padders
1✔
79
// (max 5M pixels each) instead of one large padder to avoid browser performance issues.
1✔
80
// The actual grid content is absolutely positioned and rendered on top of these padders
1✔
81
// based on the current scroll position.
1✔
82
const MAX_PADDER_SEGMENT_HEIGHT = 5_000_000;
1✔
83

1✔
84
type ScrollLock = [undefined, number] | [number, undefined] | undefined;
1✔
85

1✔
86
function useTouchUpDelayed(delay: number): boolean {
731✔
87
    const [hasTouches, setHasTouches] = React.useState(false);
731✔
88

731✔
89
    const safeWindow = typeof window === "undefined" ? null : window;
731!
90

731✔
91
    const cbTimer = React.useRef(0);
731✔
92

731✔
93
    useEventListener(
731✔
94
        "touchstart",
731✔
95
        React.useCallback(() => {
731✔
UNCOV
96
            window.clearTimeout(cbTimer.current);
×
UNCOV
97
            setHasTouches(true);
×
98
        }, []),
731✔
99
        safeWindow,
731✔
100
        true,
731✔
101
        false
731✔
102
    );
731✔
103

731✔
104
    useEventListener(
731✔
105
        "touchend",
731✔
106
        React.useCallback(
731✔
107
            e => {
731✔
UNCOV
108
                if (e.touches.length === 0) {
×
109
                    cbTimer.current = window.setTimeout(() => setHasTouches(false), delay);
×
110
                }
×
UNCOV
111
            },
×
112
            [delay]
731✔
113
        ),
731✔
114
        safeWindow,
731✔
115
        true,
731✔
116
        false
731✔
117
    );
731✔
118

731✔
119
    return hasTouches;
731✔
120
}
731✔
121

1✔
122
/**
1✔
123
 * InfiniteScroller provides virtual scrolling capabilities for the data grid.
1✔
124
 * It handles the mapping between DOM scroll positions and virtual scroll positions
1✔
125
 * when the content height exceeds browser limitations.
1✔
126
 *
1✔
127
 * Browser Limitations:
1✔
128
 * - Most browsers limit div heights to ~33.5 million pixels
1✔
129
 * - With large datasets (e.g., 100M rows × 31px = 3.1B pixels), we exceed this limit
1✔
130
 * - This component uses an offset-based approach to map the limited DOM scroll range
1✔
131
 *   to the full virtual scroll range
1✔
132
 */
1✔
133
export const InfiniteScroller: React.FC<Props> = p => {
1✔
134
    const {
731✔
135
        children,
731✔
136
        clientHeight,
731✔
137
        scrollHeight,
731✔
138
        scrollWidth,
731✔
139
        update,
731✔
140
        draggable,
731✔
141
        className,
731✔
142
        preventDiagonalScrolling = false,
731✔
143
        paddingBottom = 0,
731✔
144
        paddingRight = 0,
731✔
145
        rightElement,
731✔
146
        rightElementProps,
731✔
147
        kineticScrollPerfHack = false,
731✔
148
        scrollRef,
731✔
149
        initialSize,
731✔
150
    } = p;
731✔
151
    const padders: React.ReactNode[] = [];
731✔
152

731✔
153
    const rightElementSticky = rightElementProps?.sticky ?? false;
731!
154
    const rightElementFill = rightElementProps?.fill ?? false;
731!
155

731✔
156
    // Track the virtual scroll position directly for smooth scrolling
731✔
157
    const virtualScrollY = React.useRef(0);
731✔
158
    const lastScrollY = React.useRef(0);
731✔
159
    const scroller = React.useRef<HTMLDivElement | null>(null);
731✔
160

731✔
161
    const dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio;
731!
162
    const lastDpr = React.useRef(dpr);
731✔
163

731✔
164
    // Reset scroll tracking when device pixel ratio changes (e.g., browser zoom)
731✔
165
    React.useEffect(() => {
731✔
166
        if (lastDpr.current !== dpr) {
149!
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]);
731✔
176

731✔
177
    const lastScrollPosition = React.useRef({
731✔
178
        scrollLeft: 0,
731✔
179
        scrollTop: 0,
731✔
180
        lockDirection: undefined as ScrollLock,
731✔
181
    });
731✔
182

731✔
183
    const rightWrapRef = React.useRef<HTMLDivElement | null>(null);
731✔
184

731✔
185
    const hasTouches = useTouchUpDelayed(200);
731✔
186
    const [isIdle, setIsIdle] = React.useState(true);
731✔
187
    const idleTimer = React.useRef(0);
731✔
188

731✔
189
    React.useLayoutEffect(() => {
731✔
190
        if (!isIdle || hasTouches || lastScrollPosition.current.lockDirection === undefined) return;
149!
191
        const el = scroller.current;
×
192
        if (el === null) return;
×
193
        const [lx, ly] = lastScrollPosition.current.lockDirection;
×
194
        if (lx !== undefined) {
×
195
            el.scrollLeft = lx;
×
196
        } else if (ly !== undefined) {
×
197
            el.scrollTop = ly;
×
198
        }
×
199
        lastScrollPosition.current.lockDirection = undefined;
×
200
    }, [hasTouches, isIdle]);
731✔
201

731✔
202
    const onScroll = React.useCallback(
731✔
203
        (scrollLeft?: number, scrollTop?: number) => {
731✔
204
            const el = scroller.current;
288✔
205
            if (el === null) return;
288!
206

288✔
207
            scrollTop = scrollTop ?? el.scrollTop;
288✔
208
            scrollLeft = scrollLeft ?? el.scrollLeft;
288✔
209
            const lastScrollTop = lastScrollPosition.current.scrollTop;
288✔
210
            const lastScrollLeft = lastScrollPosition.current.scrollLeft;
288✔
211

288✔
212
            const dx = scrollLeft - lastScrollLeft;
288✔
213
            const dy = scrollTop - lastScrollTop;
288✔
214

288✔
215
            if (
288✔
216
                hasTouches &&
288!
UNCOV
217
                dx !== 0 &&
×
218
                dy !== 0 &&
×
219
                (Math.abs(dx) > 3 || Math.abs(dy) > 3) &&
×
220
                preventDiagonalScrolling &&
×
221
                lastScrollPosition.current.lockDirection === undefined
×
222
            ) {
288!
223
                lastScrollPosition.current.lockDirection =
×
224
                    Math.abs(dx) < Math.abs(dy) ? [lastScrollLeft, undefined] : [undefined, lastScrollTop];
×
225
            }
×
226

288✔
227
            const lock = lastScrollPosition.current.lockDirection;
288✔
228

288✔
229
            scrollLeft = lock?.[0] ?? scrollLeft;
288!
230
            scrollTop = lock?.[1] ?? scrollTop;
288!
231
            lastScrollPosition.current.scrollLeft = scrollLeft;
288✔
232
            lastScrollPosition.current.scrollTop = scrollTop;
288✔
233

288✔
234
            const cWidth = el.clientWidth;
288✔
235
            const cHeight = el.clientHeight;
288✔
236

288✔
237
            const newY = scrollTop;
288✔
238
            const delta = lastScrollY.current - newY;
288✔
239
            const scrollableHeight = el.scrollHeight - cHeight;
288✔
240
            lastScrollY.current = newY;
288✔
241

288✔
242
            // Calculate the virtual Y position
288✔
243
            let virtualY: number;
288✔
244

288✔
245
            // When content height exceeds browser limits, use hybrid approach
288✔
246
            if (scrollableHeight > 0 && scrollHeight > el.scrollHeight + 5) {
288✔
247
                // For large jumps (scrollbar interaction) or edge positions,
4✔
248
                // recalculate position based on percentage
4✔
249
                if (Math.abs(delta) > 2000 || newY === 0 || newY === scrollableHeight) {
4✔
250
                    const scrollProgress = Math.max(0, Math.min(1, newY / scrollableHeight));
3✔
251
                    const virtualScrollableHeight = scrollHeight - cHeight;
3✔
252
                    virtualY = scrollProgress * virtualScrollableHeight;
3✔
253
                    // Update our tracked position for subsequent smooth scrolling
3✔
254
                    virtualScrollY.current = virtualY;
3✔
255
                } else {
4✔
256
                    // For smooth scrolling, apply the delta directly to virtual position
1✔
257
                    // This preserves 1:1 pixel movement for smooth scrolling
1✔
258
                    virtualScrollY.current -= delta;
1✔
259
                    virtualY = virtualScrollY.current;
1✔
260
                }
1✔
261
            } else {
288✔
262
                // Direct mapping when within browser limits
284✔
263
                virtualY = newY;
284✔
264
                virtualScrollY.current = virtualY;
284✔
265
            }
284✔
266

288✔
267
            // Ensure virtual Y is within valid bounds
288✔
268
            virtualY = Math.max(0, Math.min(virtualY, scrollHeight - cHeight));
288✔
269
            virtualScrollY.current = virtualY; // Keep tracked position in bounds too
288✔
270

288✔
271
            if (lock !== undefined) {
288!
272
                window.clearTimeout(idleTimer.current);
×
273
                setIsIdle(false);
×
274
                idleTimer.current = window.setTimeout(() => setIsIdle(true), 200);
×
275
            }
×
276

288✔
277
            update({
288✔
278
                x: scrollLeft,
288✔
279
                y: virtualY,
288✔
280
                width: cWidth - paddingRight,
288✔
281
                height: cHeight - paddingBottom,
288✔
282
                paddingRight: rightWrapRef.current?.clientWidth ?? 0,
288!
283
            });
288✔
284
        },
288✔
285
        [paddingBottom, paddingRight, scrollHeight, update, preventDiagonalScrolling, hasTouches]
731✔
286
    );
731✔
287

731✔
288
    useKineticScroll(kineticScrollPerfHack && browserIsSafari.value, onScroll, scroller);
731!
289

731✔
290
    const onScrollRef = React.useRef(onScroll);
731✔
291
    onScrollRef.current = onScroll;
731✔
292

731✔
293
    const lastProps = React.useRef<{ width?: number; height?: number }>();
731✔
294

731✔
295
    const didFirstScroll = React.useRef(false);
731✔
296
    // if this is not a layout effect there will be a flicker when changing the number of freezeColumns
731✔
297
    // we need to document what this is needed at all.
731✔
298
    React.useLayoutEffect(() => {
731✔
299
        if (didFirstScroll.current) onScroll();
284✔
300
        else didFirstScroll.current = true;
149✔
301
    }, [onScroll, paddingBottom, paddingRight]);
731✔
302

731✔
303
    const setRefs = React.useCallback(
731✔
304
        (instance: HTMLDivElement | null) => {
731✔
305
            scroller.current = instance;
298✔
306
            if (scrollRef !== undefined) {
298✔
307
                scrollRef.current = instance;
298✔
308
            }
298✔
309
        },
298✔
310
        [scrollRef]
731✔
311
    );
731✔
312

731✔
313
    let key = 0;
731✔
314
    let h = 0;
731✔
315

731✔
316
    // Ensure we don't create padders that exceed browser limits
731✔
317
    const effectiveScrollHeight = Math.min(scrollHeight, BROWSER_MAX_DIV_HEIGHT);
731✔
318

731✔
319
    padders.push(<div key={key++} style={{ width: scrollWidth, height: 0 }} />);
731✔
320
    while (h < effectiveScrollHeight) {
731✔
321
        const toAdd = Math.min(MAX_PADDER_SEGMENT_HEIGHT, effectiveScrollHeight - h);
731✔
322
        padders.push(<div key={key++} style={{ width: 0, height: toAdd }} />);
731✔
323
        h += toAdd;
731✔
324
    }
731✔
325

731✔
326
    const { ref, width, height } = useResizeDetector<HTMLDivElement>(initialSize);
731✔
327

731✔
328
    if (typeof window !== "undefined" && (lastProps.current?.height !== height || lastProps.current?.width !== width)) {
731✔
329
        window.setTimeout(() => onScrollRef.current(), 0);
149✔
330
        lastProps.current = { width, height };
149✔
331
    }
149✔
332

731✔
333
    if ((width ?? 0) === 0 || (height ?? 0) === 0) return <div ref={ref} />;
731!
334

731✔
335
    return (
731✔
336
        <div ref={ref}>
731✔
337
            <ScrollRegionStyle isSafari={browserIsSafari.value}>
731✔
338
                <div className="dvn-underlay">{children}</div>
731✔
339
                <div
731✔
340
                    ref={setRefs}
731✔
341
                    style={lastProps.current}
731✔
342
                    draggable={draggable}
731✔
343
                    onDragStart={e => {
731✔
344
                        if (!draggable) {
1!
345
                            e.stopPropagation();
×
346
                            e.preventDefault();
×
347
                        }
×
348
                    }}
1✔
349
                    className={"dvn-scroller " + (className ?? "")}
731✔
350
                    onScroll={() => onScroll()}>
731✔
351
                    <div className={"dvn-scroll-inner" + (rightElement === undefined ? " dvn-hidden" : "")}>
731!
352
                        <div className="dvn-stack">{padders}</div>
731✔
353
                        {rightElement !== undefined && (
731!
354
                            <>
×
355
                                {!rightElementFill && <div className="dvn-spacer" />}
×
356
                                <div
×
357
                                    ref={rightWrapRef}
×
358
                                    style={{
×
359
                                        height,
×
360
                                        maxHeight: clientHeight - Math.ceil(dpr % 1),
×
361
                                        position: "sticky",
×
362
                                        top: 0,
×
363
                                        paddingLeft: 1,
×
364
                                        marginBottom: -40,
×
365
                                        marginRight: paddingRight,
×
366
                                        flexGrow: rightElementFill ? 1 : undefined,
×
367
                                        right: rightElementSticky ? (paddingRight ?? 0) : undefined,
×
368
                                        pointerEvents: "auto",
×
369
                                    }}>
×
370
                                    {rightElement}
×
371
                                </div>
×
372
                            </>
×
373
                        )}
731✔
374
                    </div>
731✔
375
                </div>
731✔
376
            </ScrollRegionStyle>
731✔
377
        </div>
731✔
378
    );
731✔
379
};
731✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc