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

glideapps / glide-data-grid / 15717132454

17 Jun 2025 08:17PM UTC coverage: 91.11% (-0.2%) from 91.298%
15717132454

Pull #960

github

web-flow
Merge 81fddd44d into d77720eee
Pull Request #960: use pointer events instead of mouse + touch

2867 of 3564 branches covered (80.44%)

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

37 existing lines in 3 files now uncovered.

17750 of 19482 relevant lines covered (91.11%)

3052.91 hits per line

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

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

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

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

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

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

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

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

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

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

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

1✔
72
type ScrollLock = [undefined, number] | [number, undefined] | undefined;
1✔
73

1✔
74
function useTouchUpDelayed(delay: number): boolean {
723✔
75
    const [hasTouches, setHasTouches] = React.useState(false);
723✔
76

723✔
77
    const safeWindow = typeof window === "undefined" ? null : window;
723!
78

723✔
79
    const cbTimer = React.useRef(0);
723✔
80

723✔
81
    useEventListener(
723✔
82
        "touchstart",
723✔
83
        React.useCallback(() => {
723✔
UNCOV
84
            window.clearTimeout(cbTimer.current);
×
UNCOV
85
            setHasTouches(true);
×
86
        }, []),
723✔
87
        safeWindow,
723✔
88
        true,
723✔
89
        false
723✔
90
    );
723✔
91

723✔
92
    useEventListener(
723✔
93
        "touchend",
723✔
94
        React.useCallback(
723✔
95
            e => {
723✔
UNCOV
96
                if (e.touches.length === 0) {
×
97
                    cbTimer.current = window.setTimeout(() => setHasTouches(false), delay);
×
98
                }
×
UNCOV
99
            },
×
100
            [delay]
723✔
101
        ),
723✔
102
        safeWindow,
723✔
103
        true,
723✔
104
        false
723✔
105
    );
723✔
106

723✔
107
    return hasTouches;
723✔
108
}
723✔
109

1✔
110
export const InfiniteScroller: React.FC<Props> = p => {
1✔
111
    const {
723✔
112
        children,
723✔
113
        clientHeight,
723✔
114
        scrollHeight,
723✔
115
        scrollWidth,
723✔
116
        update,
723✔
117
        draggable,
723✔
118
        className,
723✔
119
        preventDiagonalScrolling = false,
723✔
120
        paddingBottom = 0,
723✔
121
        paddingRight = 0,
723✔
122
        rightElement,
723✔
123
        rightElementProps,
723✔
124
        kineticScrollPerfHack = false,
723✔
125
        scrollRef,
723✔
126
        initialSize,
723✔
127
    } = p;
723✔
128
    const padders: React.ReactNode[] = [];
723✔
129

723✔
130
    const rightElementSticky = rightElementProps?.sticky ?? false;
723!
131
    const rightElementFill = rightElementProps?.fill ?? false;
723!
132

723✔
133
    const offsetY = React.useRef(0);
723✔
134
    const lastScrollY = React.useRef(0);
723✔
135
    const scroller = React.useRef<HTMLDivElement | null>(null);
723✔
136

723✔
137
    const dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio;
723!
138

723✔
139
    const lastScrollPosition = React.useRef({
723✔
140
        scrollLeft: 0,
723✔
141
        scrollTop: 0,
723✔
142
        lockDirection: undefined as ScrollLock,
723✔
143
    });
723✔
144

723✔
145
    const rightWrapRef = React.useRef<HTMLDivElement | null>(null);
723✔
146

723✔
147
    const hasTouches = useTouchUpDelayed(200);
723✔
148
    const [isIdle, setIsIdle] = React.useState(true);
723✔
149
    const idleTimer = React.useRef(0);
723✔
150

723✔
151
    React.useLayoutEffect(() => {
723✔
152
        if (!isIdle || hasTouches || lastScrollPosition.current.lockDirection === undefined) return;
147!
153
        const el = scroller.current;
×
154
        if (el === null) return;
×
155
        const [lx, ly] = lastScrollPosition.current.lockDirection;
×
156
        if (lx !== undefined) {
×
157
            el.scrollLeft = lx;
×
158
        } else if (ly !== undefined) {
×
159
            el.scrollTop = ly;
×
160
        }
×
161
        lastScrollPosition.current.lockDirection = undefined;
×
162
    }, [hasTouches, isIdle]);
723✔
163

723✔
164
    const onScroll = React.useCallback(
723✔
165
        (scrollLeft?: number, scrollTop?: number) => {
723✔
166
            const el = scroller.current;
285✔
167
            if (el === null) return;
285!
168

285✔
169
            scrollTop = scrollTop ?? el.scrollTop;
285✔
170
            scrollLeft = scrollLeft ?? el.scrollLeft;
285✔
171
            const lastScrollTop = lastScrollPosition.current.scrollTop;
285✔
172
            const lastScrollLeft = lastScrollPosition.current.scrollLeft;
285✔
173

285✔
174
            const dx = scrollLeft - lastScrollLeft;
285✔
175
            const dy = scrollTop - lastScrollTop;
285✔
176

285✔
177
            if (
285✔
178
                hasTouches &&
285!
UNCOV
179
                dx !== 0 &&
×
180
                dy !== 0 &&
×
181
                (Math.abs(dx) > 3 || Math.abs(dy) > 3) &&
×
182
                preventDiagonalScrolling &&
×
183
                lastScrollPosition.current.lockDirection === undefined
×
184
            ) {
285!
185
                lastScrollPosition.current.lockDirection =
×
186
                    Math.abs(dx) < Math.abs(dy) ? [lastScrollLeft, undefined] : [undefined, lastScrollTop];
×
187
            }
×
188

285✔
189
            const lock = lastScrollPosition.current.lockDirection;
285✔
190

285✔
191
            scrollLeft = lock?.[0] ?? scrollLeft;
285!
192
            scrollTop = lock?.[1] ?? scrollTop;
285!
193
            lastScrollPosition.current.scrollLeft = scrollLeft;
285✔
194
            lastScrollPosition.current.scrollTop = scrollTop;
285✔
195

285✔
196
            const cWidth = el.clientWidth;
285✔
197
            const cHeight = el.clientHeight;
285✔
198

285✔
199
            const newY = scrollTop;
285✔
200
            const delta = lastScrollY.current - newY;
285✔
201
            const scrollableHeight = el.scrollHeight - cHeight;
285✔
202
            lastScrollY.current = newY;
285✔
203

285✔
204
            if (
285✔
205
                scrollableHeight > 0 &&
285✔
206
                (Math.abs(delta) > 2000 || newY === 0 || newY === scrollableHeight) &&
4✔
207
                scrollHeight > el.scrollHeight + 5
3✔
208
            ) {
285✔
209
                const prog = newY / scrollableHeight;
3✔
210
                const recomputed = (scrollHeight - cHeight) * prog;
3✔
211
                offsetY.current = recomputed - newY;
3✔
212
            }
3✔
213

285✔
214
            if (lock !== undefined) {
285!
215
                window.clearTimeout(idleTimer.current);
×
216
                setIsIdle(false);
×
217
                idleTimer.current = window.setTimeout(() => setIsIdle(true), 200);
×
218
            }
×
219

285✔
220
            update({
285✔
221
                x: scrollLeft,
285✔
222
                y: newY + offsetY.current,
285✔
223
                width: cWidth - paddingRight,
285✔
224
                height: cHeight - paddingBottom,
285✔
225
                paddingRight: rightWrapRef.current?.clientWidth ?? 0,
285!
226
            });
285✔
227
        },
285✔
228
        [paddingBottom, paddingRight, scrollHeight, update, preventDiagonalScrolling, hasTouches]
723✔
229
    );
723✔
230

723✔
231
    useKineticScroll(kineticScrollPerfHack && browserIsSafari.value, onScroll, scroller);
723!
232

723✔
233
    const onScrollRef = React.useRef(onScroll);
723✔
234
    onScrollRef.current = onScroll;
723✔
235

723✔
236
    const lastProps = React.useRef<{ width?: number; height?: number }>();
723✔
237

723✔
238
    const didFirstScroll = React.useRef(false);
723✔
239
    // if this is not a layout effect there will be a flicker when changing the number of freezeColumns
723✔
240
    // we need to document what this is needed at all.
723✔
241
    React.useLayoutEffect(() => {
723✔
242
        if (didFirstScroll.current) onScroll();
281✔
243
        else didFirstScroll.current = true;
147✔
244
    }, [onScroll, paddingBottom, paddingRight]);
723✔
245

723✔
246
    const setRefs = React.useCallback(
723✔
247
        (instance: HTMLDivElement | null) => {
723✔
248
            scroller.current = instance;
294✔
249
            if (scrollRef !== undefined) {
294✔
250
                scrollRef.current = instance;
294✔
251
            }
294✔
252
        },
294✔
253
        [scrollRef]
723✔
254
    );
723✔
255

723✔
256
    let key = 0;
723✔
257
    let h = 0;
723✔
258
    padders.push(<div key={key++} style={{ width: scrollWidth, height: 0 }} />);
723✔
259
    while (h < scrollHeight) {
723✔
260
        const toAdd = Math.min(5_000_000, scrollHeight - h);
723✔
261
        padders.push(<div key={key++} style={{ width: 0, height: toAdd }} />);
723✔
262
        h += toAdd;
723✔
263
    }
723✔
264

723✔
265
    const { ref, width, height } = useResizeDetector<HTMLDivElement>(initialSize);
723✔
266

723✔
267
    if (typeof window !== "undefined" && (lastProps.current?.height !== height || lastProps.current?.width !== width)) {
723✔
268
        window.setTimeout(() => onScrollRef.current(), 0);
147✔
269
        lastProps.current = { width, height };
147✔
270
    }
147✔
271

723✔
272
    if ((width ?? 0) === 0 || (height ?? 0) === 0) return <div ref={ref} />;
723!
273

723✔
274
    return (
723✔
275
        <div ref={ref}>
723✔
276
            <ScrollRegionStyle isSafari={browserIsSafari.value}>
723✔
277
                <div className="dvn-underlay">{children}</div>
723✔
278
                <div
723✔
279
                    ref={setRefs}
723✔
280
                    style={lastProps.current}
723✔
281
                    draggable={draggable}
723✔
282
                    onDragStart={e => {
723✔
283
                        if (!draggable) {
1!
284
                            e.stopPropagation();
×
285
                            e.preventDefault();
×
286
                        }
×
287
                    }}
1✔
288
                    className={"dvn-scroller " + (className ?? "")}
723✔
289
                    onScroll={() => onScroll()}>
723✔
290
                    <div className={"dvn-scroll-inner" + (rightElement === undefined ? " dvn-hidden" : "")}>
723!
291
                        <div className="dvn-stack">{padders}</div>
723✔
292
                        {rightElement !== undefined && (
723!
293
                            <>
×
294
                                {!rightElementFill && <div className="dvn-spacer" />}
×
295
                                <div
×
296
                                    ref={rightWrapRef}
×
297
                                    style={{
×
298
                                        height,
×
299
                                        maxHeight: clientHeight - Math.ceil(dpr % 1),
×
300
                                        position: "sticky",
×
301
                                        top: 0,
×
302
                                        paddingLeft: 1,
×
303
                                        marginBottom: -40,
×
304
                                        marginRight: paddingRight,
×
305
                                        flexGrow: rightElementFill ? 1 : undefined,
×
306
                                        right: rightElementSticky ? paddingRight ?? 0 : undefined,
×
307
                                        pointerEvents: "auto",
×
308
                                    }}>
×
309
                                    {rightElement}
×
310
                                </div>
×
311
                            </>
×
312
                        )}
723✔
313
                    </div>
723✔
314
                </div>
723✔
315
            </ScrollRegionStyle>
723✔
316
        </div>
723✔
317
    );
723✔
318
};
723✔
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