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

glideapps / glide-data-grid / 6997418563

26 Nov 2023 06:56PM UTC coverage: 86.42% (+1.0%) from 85.416%
6997418563

push

github

web-flow
5.3.1 (#781)

* 5.3.1-alpha1

* Fix issue where data grid would not understand headers were not hovered when mouse over the rightElement

* Update prettier

* Modernize tooling

* Don't escape for commas, not needed

* Fix eslint

* 5.3.1-alpha2

* Improve naming of escape function

* Fix fuzzy drag handles, fixes #741

* Fix tests

* Resolve issue where editing would accidentally check the wrong cell for read/write.

* restore cyce  checking

* Ensure onCellClicked and renderer onClick events fire under the same conditions, fixes #690

* Fix paste eating some new lines

* 5.3.1-alpha3

* Fix clicking on scrollbars, fixes #762

* Hopefully fix source tests

* Implement cut and fix crasher in paste

* Fix cells tests

* Fix formatting issue with uri cells

* Add tests

* Overhaul copy/paste code to dramatically improve accuracy, compatibility, and remove accidentally encoding nbsp into users apps

* Fix an insane crash report

* Add a couple tests

* Increase test coverage

* Add more missing tests

* More missing tests

* Fix issue with apple numbers paste buffers

* Fix typo

* Add more tests

* More tests for image window loader

* Add distribution test

* Add support for returning bounds of entire scroll area (#788)

* Support returning bounds of entire scroll area

* Add scale to calculation

* Apply PR feedback

* Fix check

* Additional fix

* Fix docs

---------

Co-authored-by: Lukas Masuch <Lukas.Masuch@gmail.com>

3386 of 4456 branches covered (0.0%)

214 of 231 new or added lines in 29 files covered. (92.64%)

347 existing lines in 14 files now uncovered.

4022 of 4654 relevant lines covered (86.42%)

3209.78 hits per line

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

84.76
/packages/core/src/data-grid/data-grid.tsx
1
import * as React from "react";
9✔
2
import type { Theme } from "../common/styles";
3
import ImageWindowLoaderImpl from "../common/image-window-loader";
9✔
4
import {
9✔
5
    computeBounds,
6
    getColumnIndexForX,
7
    getEffectiveColumns,
8
    getRowIndexForY,
9
    getStickyWidth,
10
    useMappedColumns,
11
} from "./data-grid-lib";
12
import {
9✔
13
    GridCellKind,
14
    type Rectangle,
15
    type GridSelection,
16
    type GridMouseEventArgs,
17
    type GridDragEventArgs,
18
    type GridKeyEventArgs,
19
    type InnerGridCell,
20
    InnerGridCellKind,
21
    CompactSelection,
22
    type DrawCustomCellCallback,
23
    type CellList,
24
    type Item,
25
    type DrawHeaderCallback,
26
    isReadWriteCell,
27
    isInnerOnlyCell,
28
    booleanCellIsEditable,
29
    type InnerGridColumn,
30
    type TrailingRowType,
31
    groupHeaderKind,
32
    headerKind,
33
    outOfBoundsKind,
34
    type ImageWindowLoader,
35
} from "./data-grid-types";
36
import { SpriteManager, type SpriteMap } from "./data-grid-sprites";
9✔
37
import { direction, getScrollBarWidth, useDebouncedMemo, useEventListener } from "../common/utils";
9✔
38
import clamp from "lodash/clamp.js";
9✔
39
import makeRange from "lodash/range.js";
9✔
40
import {
9✔
41
    type BlitData,
42
    drawCell,
43
    drawGrid,
44
    type DrawGridArg,
45
    drawHeader,
46
    getActionBoundsForGroup,
47
    getHeaderMenuBounds,
48
    type GetRowThemeCallback,
49
    type GroupDetailsCallback,
50
    type Highlight,
51
    pointInRect,
52
} from "./data-grid-render";
53
import { AnimationManager, type StepCallback } from "./animation-manager";
9✔
54
import { browserIsFirefox, browserIsSafari } from "../common/browser-detect";
9✔
55
import { useAnimationQueue } from "./use-animation-queue";
9✔
56
import { assert } from "../common/support";
1,179✔
57
import type { CellRenderer, GetCellRendererCallback } from "./cells/cell-types";
58

59
export interface DataGridProps {
60
    readonly width: number;
61
    readonly height: number;
62

63
    readonly cellXOffset: number;
64
    readonly cellYOffset: number;
65

66
    readonly translateX: number | undefined;
67
    readonly translateY: number | undefined;
68

69
    readonly accessibilityHeight: number;
70

71
    readonly freezeColumns: number;
72
    readonly trailingRowType: TrailingRowType;
73
    readonly firstColAccessible: boolean;
74

75
    /**
76
     * Enables or disables the overlay shadow when scrolling horizontally
77
     * @group Style
78
     */
79
    readonly fixedShadowX: boolean | undefined;
80
    /**
81
     * Enables or disables the overlay shadow when scrolling vertical
82
     * @group Style
83
     */
84
    readonly fixedShadowY: boolean | undefined;
85

86
    readonly allowResize: boolean | undefined;
87
    readonly isResizing: boolean;
88
    readonly isDragging: boolean;
89
    readonly isFilling: boolean;
90
    readonly isFocused: boolean;
91

92
    readonly columns: readonly InnerGridColumn[];
93
    /**
94
     * The number of rows in the grid.
95
     * @group Data
96
     */
97
    readonly rows: number;
98

99
    readonly headerHeight: number;
100
    readonly groupHeaderHeight: number;
101
    readonly enableGroups: boolean;
102
    readonly rowHeight: number | ((index: number) => number);
103

104
    readonly canvasRef: React.MutableRefObject<HTMLCanvasElement | null> | undefined;
105

106
    readonly eventTargetRef: React.MutableRefObject<HTMLDivElement | null> | undefined;
107

108
    readonly getCellContent: (cell: Item, forceStrict?: boolean) => InnerGridCell;
109
    /**
110
     * Provides additional details about groups to extend group functionality.
111
     * @group Data
112
     */
113
    readonly getGroupDetails: GroupDetailsCallback | undefined;
114
    /**
115
     * Provides per row theme overrides.
116
     * @group Style
117
     */
118
    readonly getRowThemeOverride: GetRowThemeCallback | undefined;
119
    /**
120
     * Emitted when a header menu disclosure indicator is clicked.
121
     * @group Events
122
     */
123
    readonly onHeaderMenuClick: ((col: number, screenPosition: Rectangle) => void) | undefined;
124

125
    readonly selection: GridSelection;
126
    readonly prelightCells: readonly Item[] | undefined;
127
    /**
128
     * Highlight regions provide hints to users about relations between cells and selections.
129
     * @group Selection
130
     */
131
    readonly highlightRegions: readonly Highlight[] | undefined;
132

133
    /**
134
     * Enabled/disables the fill handle.
135
     * @defaultValue false
136
     * @group Editing
137
     */
138
    readonly fillHandle: boolean | undefined;
139

140
    readonly disabledRows: CompactSelection | undefined;
141
    /**
142
     * Allows passing a custom image window loader.
143
     * @group Advanced
144
     */
145
    readonly imageWindowLoader: ImageWindowLoader | undefined;
146

147
    /**
148
     * Emitted when an item is hovered.
149
     * @group Events
150
     */
151
    readonly onItemHovered: (args: GridMouseEventArgs) => void;
152
    readonly onMouseMove: (args: GridMouseEventArgs) => void;
153
    readonly onMouseDown: (args: GridMouseEventArgs) => void;
154
    readonly onMouseUp: (args: GridMouseEventArgs, isOutside: boolean) => void;
155
    readonly onContextMenu: (args: GridMouseEventArgs, preventDefault: () => void) => void;
156

157
    readonly onCanvasFocused: () => void;
158
    readonly onCanvasBlur: () => void;
159
    readonly onCellFocused: (args: Item) => void;
160

161
    readonly onMouseMoveRaw: (event: MouseEvent) => void;
162

163
    /**
164
     * Emitted when the canvas receives a key down event.
165
     * @group Events
166
     */
167
    readonly onKeyDown: (event: GridKeyEventArgs) => void;
168
    /**
169
     * Emitted when the canvas receives a key up event.
170
     * @group Events
171
     */
172
    readonly onKeyUp: ((event: GridKeyEventArgs) => void) | undefined;
173

174
    readonly verticalBorder: (col: number) => boolean;
175

176
    /**
177
     * Determines what can be dragged using HTML drag and drop
178
     * @defaultValue false
179
     * @group Drag and Drop
180
     */
181
    readonly isDraggable: boolean | "cell" | "header" | undefined;
182
    /**
183
     * If `isDraggable` is set, the grid becomes HTML draggable, and `onDragStart` will be called when dragging starts.
184
     * You can use this to build a UI where the user can drag the Grid around.
185
     * @group Drag and Drop
186
     */
187
    readonly onDragStart: (args: GridDragEventArgs) => void;
188
    readonly onDragEnd: () => void;
189

190
    /** @group Drag and Drop */
191
    readonly onDragOverCell: ((cell: Item, dataTransfer: DataTransfer | null) => void) | undefined;
192
    /** @group Drag and Drop */
193
    readonly onDragLeave: (() => void) | undefined;
194

195
    /**
196
     * Called when a HTML Drag and Drop event is ended on the data grid.
197
     * @group Drag and Drop
198
     */
199
    readonly onDrop: ((cell: Item, dataTransfer: DataTransfer | null) => void) | undefined;
200

201
    readonly drawCustomCell: DrawCustomCellCallback | undefined;
202
    /**
203
     * Overrides the rendering of a header. The grid will call this for every header it needs to render. Header
204
     * rendering is not as well optimized because they do not redraw as often, but very heavy drawing methods can
205
     * negatively impact horizontal scrolling performance.
206
     *
207
     * It is possible to return `false` after rendering just a background and the regular foreground rendering
208
     * will happen.
209
     * @group Drawing
210
     * @returns `false` if default header rendering should still happen, `true` to cancel rendering.
211
     */
212
    readonly drawHeader: DrawHeaderCallback | undefined;
213
    /**
214
     * Controls the drawing of the focus ring.
215
     * @defaultValue true
216
     * @group Style
217
     */
218
    readonly drawFocusRing: boolean | undefined;
219

220
    readonly dragAndDropState:
221
        | {
222
              src: number;
223
              dest: number;
224
          }
225
        | undefined;
226

227
    /**
228
     * Experimental features
229
     * @group Advanced
230
     * @experimental
231
     */
232
    readonly experimental:
233
        | {
234
              readonly paddingRight?: number;
235
              readonly paddingBottom?: number;
236
              readonly enableFirefoxRescaling?: boolean;
237
              readonly isSubGrid?: boolean;
238
              readonly strict?: boolean;
239
              readonly scrollbarWidthOverride?: number;
240
              readonly hyperWrapping?: boolean;
241
              readonly renderStrategy?: "single-buffer" | "double-buffer" | "direct";
242
          }
243
        | undefined;
244

245
    /**
246
     * Additional header icons for use by `GridColumn`.
247
     *
248
     * Providing custom header icons to the data grid must be done with a somewhat non-standard mechanism to allow
249
     * theming and scaling. The `headerIcons` property takes a dictionary which maps icon names to functions which can
250
     * take a foreground and background color and returns back a string representation of an svg. The svg should contain
251
     * a header similar to this `<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg">` and
252
     * interpolate the fg/bg colors into the string.
253
     *
254
     * We recognize this process is not fantastic from a graphics workflow standpoint, improvements are very welcome
255
     * here.
256
     *
257
     * @group Style
258
     */
259
    readonly headerIcons: SpriteMap | undefined;
260

261
    /** Controls smooth scrolling in the data grid. If smooth scrolling is not enabled the grid will always be cell
262
     * aligned.
263
     * @defaultValue `false`
264
     * @group Style
265
     */
266
    readonly smoothScrollX: boolean | undefined;
267
    /** Controls smooth scrolling in the data grid. If smooth scrolling is not enabled the grid will always be cell
268
     * aligned.
269
     * @defaultValue `false`
270
     * @group Style
271
     */
272
    readonly smoothScrollY: boolean | undefined;
273

274
    readonly theme: Theme;
275

276
    readonly getCellRenderer: <T extends InnerGridCell>(cell: T) => CellRenderer<T> | undefined;
277
}
278

279
type DamageUpdateList = readonly {
280
    cell: Item;
281
    // newValue: GridCell,
282
}[];
283

284
export interface DataGridRef {
285
    focus: () => void;
286
    getBounds: (col?: number, row?: number) => Rectangle | undefined;
287
    damage: (cells: DamageUpdateList) => void;
288
}
289

290
const getRowData = (cell: InnerGridCell, getCellRenderer?: GetCellRendererCallback) => {
9✔
291
    if (cell.kind === GridCellKind.Custom) return cell.copyData;
23,648✔
292
    const r = getCellRenderer?.(cell);
21,728!
293
    return r?.getAccessibilityString(cell) ?? "";
21,728!
294
};
295

296
const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p, forwardedRef) => {
9✔
297
    const {
298
        width,
299
        height,
300
        accessibilityHeight,
301
        columns,
302
        cellXOffset: cellXOffsetReal,
303
        cellYOffset,
304
        headerHeight,
305
        fillHandle = false,
695✔
306
        groupHeaderHeight,
307
        rowHeight,
308
        rows,
309
        getCellContent,
310
        getRowThemeOverride,
311
        onHeaderMenuClick,
312
        enableGroups,
313
        isFilling,
314
        onCanvasFocused,
315
        onCanvasBlur,
316
        isFocused,
317
        selection,
318
        freezeColumns,
319
        onContextMenu,
320
        trailingRowType: trailingRowType,
321
        fixedShadowX = true,
717✔
322
        fixedShadowY = true,
717✔
323
        drawFocusRing = true,
717✔
324
        onMouseDown,
325
        onMouseUp,
326
        onMouseMoveRaw,
327
        onMouseMove,
328
        onItemHovered,
329
        dragAndDropState,
330
        firstColAccessible,
331
        onKeyDown,
332
        onKeyUp,
333
        highlightRegions,
334
        canvasRef,
335
        onDragStart,
336
        onDragEnd,
337
        eventTargetRef,
338
        isResizing,
339
        isDragging,
340
        isDraggable = false,
713✔
341
        allowResize,
342
        disabledRows,
343
        getGroupDetails,
344
        theme,
345
        prelightCells,
346
        headerIcons,
347
        verticalBorder,
348
        drawHeader: drawHeaderCallback,
349
        drawCustomCell,
350
        onCellFocused,
351
        onDragOverCell,
352
        onDrop,
353
        onDragLeave,
354
        imageWindowLoader,
355
        smoothScrollX = false,
717✔
356
        smoothScrollY = false,
717✔
357
        experimental,
358
        getCellRenderer,
359
    } = p;
718✔
360
    const translateX = p.translateX ?? 0;
717✔
361
    const translateY = p.translateY ?? 0;
717✔
362
    const cellXOffset = Math.max(freezeColumns, Math.min(columns.length - 1, cellXOffsetReal));
717✔
363

364
    const ref = React.useRef<HTMLCanvasElement | null>(null);
717✔
365
    const imageWindowLoaderInternal = React.useMemo<ImageWindowLoader>(() => new ImageWindowLoaderImpl(), []);
717✔
366
    const imageLoader = imageWindowLoader ?? imageWindowLoaderInternal;
717!
367
    const damageRegion = React.useRef<readonly Item[] | undefined>();
717✔
368
    const [scrolling, setScrolling] = React.useState<boolean>(false);
717✔
369
    const hoverValues = React.useRef<readonly { item: Item; hoverAmount: number }[]>([]);
717✔
370
    const lastBlitData = React.useRef<BlitData | undefined>();
717✔
371
    const [hoveredItemInfo, setHoveredItemInfo] = React.useState<[Item, readonly [number, number]] | undefined>();
717✔
372
    const [hoveredOnEdge, setHoveredOnEdge] = React.useState<boolean>();
717✔
373
    const overlayRef = React.useRef<HTMLCanvasElement | null>(null);
717✔
374

375
    const [lastWasTouch, setLastWasTouch] = React.useState(false);
717✔
376
    const lastWasTouchRef = React.useRef(lastWasTouch);
717✔
377
    lastWasTouchRef.current = lastWasTouch;
717✔
378

379
    const spriteManager = React.useMemo(
717✔
380
        () =>
381
            new SpriteManager(headerIcons, () => {
144✔
382
                lastArgsRef.current = undefined;
×
383
                lastDrawRef.current();
×
384
            }),
385
        [headerIcons]
386
    );
387
    const totalHeaderHeight = enableGroups ? groupHeaderHeight + headerHeight : headerHeight;
717✔
388

389
    const scrollingStopRef = React.useRef(-1);
717✔
390
    const disableFirefoxRescaling = experimental?.enableFirefoxRescaling !== true;
717✔
391
    React.useLayoutEffect(() => {
717✔
392
        if (!browserIsFirefox.value || window.devicePixelRatio === 1 || disableFirefoxRescaling) return;
148!
393
        // We don't want to go into scroll mode for a single repaint
394
        if (scrollingStopRef.current !== -1) {
395
            setScrolling(true);
×
396
        }
397
        window.clearTimeout(scrollingStopRef.current);
×
398
        scrollingStopRef.current = window.setTimeout(() => {
×
399
            setScrolling(false);
×
400
            scrollingStopRef.current = -1;
×
401
        }, 200);
402
    }, [cellYOffset, cellXOffset, translateX, translateY, disableFirefoxRescaling]);
403

404
    const mappedColumns = useMappedColumns(columns, freezeColumns);
717✔
405

406
    // row: -1 === columnHeader, -2 === groupHeader
407
    const getBoundsForItem = React.useCallback(
717✔
408
        (canvas: HTMLCanvasElement, col: number, row: number): Rectangle | undefined => {
409
            const rect = canvas.getBoundingClientRect();
537✔
410

411
            if (col >= mappedColumns.length || row >= rows) {
1,074✔
412
                return undefined;
×
413
            }
414

415
            const scale = rect.width / width;
537✔
416

417
            const result = computeBounds(
537✔
418
                col,
419
                row,
420
                width,
421
                height,
422
                groupHeaderHeight,
423
                totalHeaderHeight,
424
                cellXOffset,
425
                cellYOffset,
426
                translateX,
427
                translateY,
428
                rows,
429
                freezeColumns,
430
                trailingRowType === "sticky",
431
                mappedColumns,
432
                rowHeight
433
            );
434

435
            if (scale !== 1) {
436
                result.x *= scale;
×
437
                result.y *= scale;
×
438
                result.width *= scale;
×
439
                result.height *= scale;
×
440
            }
441

442
            result.x += rect.x;
537✔
443
            result.y += rect.y;
537✔
444

445
            return result;
537✔
446
        },
447
        [
448
            width,
449
            height,
450
            groupHeaderHeight,
451
            totalHeaderHeight,
452
            cellXOffset,
453
            cellYOffset,
454
            translateX,
455
            translateY,
456
            rows,
457
            freezeColumns,
458
            trailingRowType,
459
            mappedColumns,
460
            rowHeight,
461
        ]
462
    );
463

464
    const getMouseArgsForPosition = React.useCallback(
717✔
465
        (canvas: HTMLCanvasElement, posX: number, posY: number, ev?: MouseEvent | TouchEvent): GridMouseEventArgs => {
466
            const rect = canvas.getBoundingClientRect();
429✔
467
            const scale = rect.width / width;
429✔
468
            const x = (posX - rect.left) / scale;
429✔
469
            const y = (posY - rect.top) / scale;
429✔
470
            const edgeDetectionBuffer = 5;
429✔
471

472
            const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, undefined, translateX);
429✔
473

474
            let button = 0;
429✔
475
            if (ev instanceof MouseEvent) {
476
                button = ev.button;
420✔
477
            }
478

479
            // -1 === off right edge
480
            const col = getColumnIndexForX(x, effectiveCols, translateX);
429✔
481

482
            // -1: header or above
483
            // undefined: offbottom
484
            const row = getRowIndexForY(
429✔
485
                y,
486
                height,
487
                enableGroups,
488
                headerHeight,
489
                groupHeaderHeight,
490
                rows,
491
                rowHeight,
492
                cellYOffset,
493
                translateY,
494
                trailingRowType === "sticky"
495
            );
496

497
            const shiftKey = ev?.shiftKey === true;
429✔
498
            const ctrlKey = ev?.ctrlKey === true;
429✔
499
            const metaKey = ev?.metaKey === true;
429✔
500
            const isTouch = (ev !== undefined && !(ev instanceof MouseEvent)) || (ev as any)?.pointerType === "touch";
429✔
501

502
            const edgeSize = 20;
429✔
503
            const scrollEdge: GridMouseEventArgs["scrollEdge"] = [
429✔
504
                Math.abs(x) < edgeSize ? -1 : Math.abs(rect.width - x) < edgeSize ? 1 : 0,
806✔
505
                Math.abs(y) < edgeSize ? -1 : Math.abs(rect.height - y) < edgeSize ? 1 : 0,
766✔
506
            ];
507

508
            let result: GridMouseEventArgs;
509
            if (col === -1 || y < 0 || x < 0 || row === undefined || x > width || y > height) {
2,516✔
510
                const horizontal = x > width ? -1 : x < 0 ? 1 : 0;
14!
511
                const vertical = y > height ? 1 : y < 0 ? -1 : 0;
14!
512

513
                let isEdge = false;
14✔
514
                if (col === -1 && row === -1) {
24✔
515
                    const b = getBoundsForItem(canvas, mappedColumns.length - 1, -1);
4✔
516
                    assert(b !== undefined);
4✔
517
                    isEdge = posX < b.x + b.width + edgeDetectionBuffer;
4✔
518
                }
519

520
                // This is used to ensure that clicking on the scrollbar doesn't unset the selection.
521
                // Unfortunately this doesn't work for overlay scrollbars because they are just a broken interaction
522
                // by design.
523
                const isMaybeScrollbar =
524
                    (x > width && x < width + getScrollBarWidth()) || (y > height && y < height + getScrollBarWidth());
14!
525

526
                result = {
14✔
527
                    kind: outOfBoundsKind,
528
                    location: [col !== -1 ? col : x < 0 ? 0 : mappedColumns.length - 1, row ?? rows - 1],
66!
529
                    direction: [horizontal, vertical],
530
                    shiftKey,
531
                    ctrlKey,
532
                    metaKey,
533
                    isEdge,
534
                    isTouch,
535
                    button,
536
                    scrollEdge,
537
                    isMaybeScrollbar,
538
                };
539
            } else if (row <= -1) {
540
                let bounds = getBoundsForItem(canvas, col, row);
89✔
541
                assert(bounds !== undefined);
89✔
542
                let isEdge = bounds !== undefined && bounds.x + bounds.width - posX <= edgeDetectionBuffer;
89✔
543

544
                const previousCol = col - 1;
89✔
545
                if (posX - bounds.x <= edgeDetectionBuffer && previousCol >= 0) {
91✔
546
                    isEdge = true;
×
547
                    bounds = getBoundsForItem(canvas, previousCol, row);
×
548
                    assert(bounds !== undefined);
×
549
                    result = {
×
550
                        kind: enableGroups && row === -2 ? groupHeaderKind : headerKind,
×
551
                        location: [previousCol, row] as any,
552
                        bounds: bounds,
553
                        group: mappedColumns[previousCol].group ?? "",
×
554
                        isEdge,
555
                        shiftKey,
556
                        ctrlKey,
557
                        metaKey,
558
                        isTouch,
559
                        localEventX: posX - bounds.x,
560
                        localEventY: posY - bounds.y,
561
                        button,
562
                        scrollEdge,
563
                    };
564
                } else {
565
                    result = {
89✔
566
                        kind: enableGroups && row === -2 ? groupHeaderKind : headerKind,
196✔
567
                        group: mappedColumns[col].group ?? "",
267✔
568
                        location: [col, row] as any,
569
                        bounds: bounds,
570
                        isEdge,
571
                        shiftKey,
572
                        ctrlKey,
573
                        metaKey,
574
                        isTouch,
575
                        localEventX: posX - bounds.x,
576
                        localEventY: posY - bounds.y,
577
                        button,
578
                        scrollEdge,
579
                    };
580
                }
581
            } else {
582
                const bounds = getBoundsForItem(canvas, col, row);
326✔
583
                assert(bounds !== undefined);
326✔
584
                const isEdge = bounds !== undefined && bounds.x + bounds.width - posX < edgeDetectionBuffer;
326✔
585
                const isFillHandle =
586
                    fillHandle &&
326✔
587
                    bounds !== undefined &&
588
                    bounds.x + bounds.width - posX < 6 &&
589
                    bounds.y + bounds.height - posY < 6;
590
                result = {
326✔
591
                    kind: "cell",
592
                    location: [col, row],
593
                    bounds: bounds,
594
                    isEdge,
595
                    shiftKey,
596
                    ctrlKey,
597
                    isFillHandle,
598
                    metaKey,
599
                    isTouch,
600
                    localEventX: posX - bounds.x,
601
                    localEventY: posY - bounds.y,
602
                    button,
603
                    scrollEdge,
604
                };
605
            }
606
            return result;
429✔
607
        },
608
        [
609
            mappedColumns,
610
            cellXOffset,
611
            width,
612
            translateX,
613
            height,
614
            enableGroups,
615
            headerHeight,
616
            groupHeaderHeight,
617
            rows,
618
            rowHeight,
619
            cellYOffset,
620
            translateY,
621
            trailingRowType,
622
            getBoundsForItem,
623
            fillHandle,
624
        ]
625
    );
626

627
    function isSameItem(item: GridMouseEventArgs | undefined, other: GridMouseEventArgs | undefined) {
628
        if (item === other) return true;
41!
629
        return (
41✔
630
            item?.kind === other?.kind &&
292!
631
            item?.location[0] === other?.location[0] &&
18!
632
            item?.location[1] === other?.location[1]
12!
633
        );
634
    }
635

636
    const [hoveredItem] = hoveredItemInfo ?? [];
717✔
637

638
    const enqueueRef = React.useRef((_item: Item) => {
717✔
639
        // do nothing
640
    });
641
    const hoverInfoRef = React.useRef(hoveredItemInfo);
717✔
642
    hoverInfoRef.current = hoveredItemInfo;
717✔
643

644
    const [bufferA, bufferB] = React.useMemo(() => {
717✔
645
        const a = document.createElement("canvas");
144✔
646
        const b = document.createElement("canvas");
144✔
647
        a.style["display"] = "none";
144✔
648
        a.style["opacity"] = "0";
144✔
649
        a.style["position"] = "fixed";
144✔
650
        b.style["display"] = "none";
144✔
651
        b.style["opacity"] = "0";
144✔
652
        b.style["position"] = "fixed";
144✔
653
        return [a, b];
144✔
654
    }, []);
655

656
    React.useLayoutEffect(() => {
717✔
657
        document.documentElement.append(bufferA);
144✔
658
        document.documentElement.append(bufferB);
144✔
659
        return () => {
144✔
660
            bufferA.remove();
144✔
661
            bufferB.remove();
144✔
662
        };
663
    }, [bufferA, bufferB]);
664

665
    const lastArgsRef = React.useRef<DrawGridArg>();
717✔
666
    const draw = React.useCallback(() => {
717✔
667
        const canvas = ref.current;
608✔
668
        const overlay = overlayRef.current;
608✔
669
        if (canvas === null || overlay === null) return;
608✔
670

671
        const last = lastArgsRef.current;
557✔
672
        const current = {
557✔
673
            canvas,
674
            bufferA,
675
            bufferB,
676
            headerCanvas: overlay,
677
            width,
678
            height,
679
            cellXOffset,
680
            cellYOffset,
681
            translateX: Math.round(translateX),
682
            translateY: Math.round(translateY),
683
            mappedColumns,
684
            enableGroups,
685
            freezeColumns,
686
            dragAndDropState,
687
            theme,
688
            headerHeight,
689
            groupHeaderHeight,
690
            disabledRows: disabledRows ?? CompactSelection.empty(),
1,671✔
691
            rowHeight,
692
            verticalBorder,
693
            isResizing,
694
            isFocused,
695
            selection,
696
            fillHandle,
697
            lastRowSticky: trailingRowType,
698
            rows,
699
            drawFocus: drawFocusRing,
700
            getCellContent,
701
            getGroupDetails: getGroupDetails ?? (name => ({ name })),
122✔
702
            getRowThemeOverride,
703
            drawCustomCell,
704
            drawHeaderCallback,
705
            prelightCells,
706
            highlightRegions,
707
            imageLoader,
708
            lastBlitData,
709
            damage: damageRegion.current,
710
            hoverValues: hoverValues.current,
711
            hoverInfo: hoverInfoRef.current,
712
            spriteManager,
713
            scrolling,
714
            hyperWrapping: experimental?.hyperWrapping ?? false,
3,342!
715
            touchMode: lastWasTouch,
716
            enqueue: enqueueRef.current,
717
            renderStrategy: experimental?.renderStrategy ?? (browserIsSafari.value ? "double-buffer" : "single-buffer"),
3,895!
718
            getCellRenderer,
719
        };
720

721
        // This confusing bit of code due to some poor design. Long story short, the damage property is only used
722
        // with what is effectively the "last args" for the last normal draw anyway. We don't want the drawing code
723
        // to look at this and go "shit dawg, nothing changed" so we force it to draw frash, but the damage restricts
724
        // the draw anyway.
725
        //
726
        // Dear future Jason, I'm sorry. It was expedient, it worked, and had almost zero perf overhead. THe universe
727
        // basically made me do it. What choice did I have?
728
        if (current.damage === undefined) {
729
            lastArgsRef.current = current;
505✔
730
            drawGrid(current, last);
505✔
731
        } else {
732
            drawGrid(current, undefined);
52✔
733
        }
734
    }, [
735
        bufferA,
736
        bufferB,
737
        width,
738
        height,
739
        cellXOffset,
740
        cellYOffset,
741
        translateX,
742
        translateY,
743
        mappedColumns,
744
        enableGroups,
745
        freezeColumns,
746
        dragAndDropState,
747
        theme,
748
        headerHeight,
749
        groupHeaderHeight,
750
        disabledRows,
751
        rowHeight,
752
        verticalBorder,
753
        isResizing,
754
        isFocused,
755
        selection,
756
        fillHandle,
757
        trailingRowType,
758
        rows,
759
        drawFocusRing,
760
        getCellContent,
761
        getGroupDetails,
762
        getRowThemeOverride,
763
        drawCustomCell,
764
        drawHeaderCallback,
765
        prelightCells,
766
        highlightRegions,
767
        imageLoader,
768
        spriteManager,
769
        scrolling,
770
        experimental?.hyperWrapping,
2,151✔
771
        experimental?.renderStrategy,
2,151✔
772
        lastWasTouch,
773
        getCellRenderer,
774
    ]);
775

776
    const lastDrawRef = React.useRef(draw);
717✔
777
    React.useLayoutEffect(() => {
717✔
778
        draw();
505✔
779
        lastDrawRef.current = draw;
505✔
780
    }, [draw]);
781

782
    React.useLayoutEffect(() => {
717✔
783
        const fn = async () => {
144✔
784
            if (document?.fonts?.ready === undefined) return;
144!
785
            await document.fonts.ready;
×
786
            lastArgsRef.current = undefined;
×
787
            lastDrawRef.current();
×
788
        };
789
        void fn();
144✔
790
    }, []);
791

792
    const damageInternal = React.useCallback((locations: CellList) => {
717✔
793
        damageRegion.current = locations;
30✔
794
        lastDrawRef.current();
30✔
795
        damageRegion.current = undefined;
30✔
796
    }, []);
797

798
    const enqueue = useAnimationQueue(damageInternal);
717✔
799
    enqueueRef.current = enqueue;
717✔
800

801
    const damage = React.useCallback(
717✔
802
        (cells: DamageUpdateList) => {
803
            damageInternal(cells.map(x => x.cell));
1,108✔
804
        },
805
        [damageInternal]
806
    );
807

808
    imageLoader.setCallback(damageInternal);
717✔
809

810
    const [overFill, setOverFill] = React.useState(false);
717✔
811

812
    const [hCol, hRow] = hoveredItem ?? [];
717✔
813
    const headerHovered = hCol !== undefined && hRow === -1;
717✔
814
    const groupHeaderHovered = hCol !== undefined && hRow === -2;
717✔
815
    let clickableInnerCellHovered = false;
717✔
816
    let editableBoolHovered = false;
717✔
817
    let cursorOverride: React.CSSProperties["cursor"] | undefined;
818
    if (hCol !== undefined && hRow !== undefined && hRow > -1) {
853✔
819
        const cell = getCellContent([hCol, hRow], true);
34✔
820
        clickableInnerCellHovered =
34✔
821
            cell.kind === InnerGridCellKind.NewRow ||
74✔
822
            (cell.kind === InnerGridCellKind.Marker && cell.markerKind !== "number");
823
        editableBoolHovered = cell.kind === GridCellKind.Boolean && booleanCellIsEditable(cell);
34!
824
        cursorOverride = cell.cursor;
34✔
825
    }
826
    const canDrag = hoveredOnEdge ?? false;
717✔
827
    const cursor = isDragging
717✔
828
        ? "grabbing"
829
        : canDrag || isResizing
2,139✔
830
        ? "col-resize"
831
        : overFill || isFilling
2,103✔
832
        ? "crosshair"
833
        : cursorOverride !== undefined
694!
834
        ? cursorOverride
835
        : headerHovered || clickableInnerCellHovered || editableBoolHovered || groupHeaderHovered
3,384✔
836
        ? "pointer"
837
        : "default";
838
    const style = React.useMemo(
717✔
839
        () => ({
181✔
840
            // width,
841
            // height,
842
            contain: "strict",
843
            display: "block",
844
            cursor,
845
        }),
846
        [cursor]
847
    );
848

849
    const lastSetCursor = React.useRef<typeof cursor>("default");
717✔
850
    const target = eventTargetRef?.current;
717✔
851
    if (target !== null && target !== undefined && lastSetCursor.current !== style.cursor) {
1,871✔
852
        // because we have an event target we need to set its cursor instead.
853
        target.style.cursor = lastSetCursor.current = style.cursor;
34✔
854
    }
855

856
    const groupHeaderActionForEvent = React.useCallback(
717✔
857
        (group: string, bounds: Rectangle, localEventX: number, localEventY: number) => {
858
            if (getGroupDetails === undefined) return undefined;
15!
859
            const groupDesc = getGroupDetails(group);
15✔
860
            if (groupDesc.actions !== undefined) {
861
                const boxes = getActionBoundsForGroup(bounds, groupDesc.actions);
3✔
862
                for (const [i, box] of boxes.entries()) {
863
                    if (pointInRect(box, localEventX + bounds.x, localEventY + box.y)) {
864
                        return groupDesc.actions[i];
3✔
865
                    }
866
                }
867
            }
868
            return undefined;
12✔
869
        },
870
        [getGroupDetails]
871
    );
872

873
    const isOverHeaderMenu = React.useCallback(
717✔
874
        (canvas: HTMLCanvasElement, col: number, clientX: number, clientY: number) => {
875
            const header = columns[col];
265✔
876

877
            if (!isDragging && !isResizing && header.hasMenu === true && !(hoveredOnEdge ?? false)) {
810✔
878
                const headerBounds = getBoundsForItem(canvas, col, -1);
6✔
879
                assert(headerBounds !== undefined);
6✔
880
                const menuBounds = getHeaderMenuBounds(
6✔
881
                    headerBounds.x,
882
                    headerBounds.y,
883
                    headerBounds.width,
884
                    headerBounds.height,
885
                    direction(header.title) === "rtl"
886
                );
887
                if (
888
                    clientX > menuBounds.x &&
24✔
889
                    clientX < menuBounds.x + menuBounds.width &&
890
                    clientY > menuBounds.y &&
891
                    clientY < menuBounds.y + menuBounds.height
892
                ) {
893
                    return headerBounds;
6✔
894
                }
895
            }
896
            return undefined;
259✔
897
        },
898
        [columns, getBoundsForItem, hoveredOnEdge, isDragging, isResizing]
899
    );
900

901
    const downTime = React.useRef(0);
717✔
902
    const downPosition = React.useRef<Item>();
717✔
903
    const mouseDown = React.useRef(false);
717✔
904
    const onMouseDownImpl = React.useCallback(
717✔
905
        (ev: MouseEvent | TouchEvent) => {
906
            const canvas = ref.current;
134✔
907
            const eventTarget = eventTargetRef?.current;
134✔
908
            if (canvas === null || (ev.target !== canvas && ev.target !== eventTarget)) return;
134✔
909
            mouseDown.current = true;
133✔
910

911
            let clientX: number;
912
            let clientY: number;
913
            if (ev instanceof MouseEvent) {
914
                clientX = ev.clientX;
129✔
915
                clientY = ev.clientY;
129✔
916
            } else {
917
                clientX = ev.touches[0].clientX;
4✔
918
                clientY = ev.touches[0].clientY;
4✔
919
            }
920
            if (ev.target === eventTarget && eventTarget !== null) {
134✔
921
                const bounds = eventTarget.getBoundingClientRect();
1✔
922
                if (clientX > bounds.right || clientY > bounds.bottom) return;
1!
923
            }
924

925
            const args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
133✔
926
            downPosition.current = args.location;
133✔
927

928
            if (args.isTouch) {
929
                downTime.current = Date.now();
4✔
930
            }
931
            if (lastWasTouchRef.current !== args.isTouch) {
932
                setLastWasTouch(args.isTouch);
4✔
933
            }
934

935
            if (
936
                args.kind === headerKind &&
153✔
937
                isOverHeaderMenu(canvas, args.location[0], clientX, clientY) !== undefined
938
            ) {
939
                return;
2✔
940
            } else if (args.kind === groupHeaderKind) {
941
                const action = groupHeaderActionForEvent(args.group, args.bounds, args.localEventX, args.localEventY);
5✔
942
                if (action !== undefined) {
943
                    return;
1✔
944
                }
945
            }
946

947
            onMouseDown?.(args);
130!
948
            if (!args.isTouch && isDraggable !== true && isDraggable !== args.kind) {
381✔
949
                // preventing default in touch events stops scroll
950
                ev.preventDefault();
125✔
951
            }
952
        },
953
        [eventTargetRef, isDraggable, getMouseArgsForPosition, groupHeaderActionForEvent, isOverHeaderMenu, onMouseDown]
954
    );
955
    useEventListener("touchstart", onMouseDownImpl, window, false);
717✔
956
    useEventListener("mousedown", onMouseDownImpl, window, false);
717✔
957

958
    const onMouseUpImpl = React.useCallback(
717✔
959
        (ev: MouseEvent | TouchEvent) => {
960
            const canvas = ref.current;
132✔
961
            mouseDown.current = false;
132✔
962
            if (onMouseUp === undefined || canvas === null) return;
132!
963
            const eventTarget = eventTargetRef?.current;
132✔
964

965
            const isOutside = ev.target !== canvas && ev.target !== eventTarget;
132✔
966

967
            let clientX: number;
968
            let clientY: number;
969
            if (ev instanceof MouseEvent) {
970
                clientX = ev.clientX;
128✔
971
                clientY = ev.clientY;
128✔
972
                if ((ev as any).pointerType === "touch") {
UNCOV
973
                    return;
×
974
                }
975
            } else {
976
                clientX = ev.changedTouches[0].clientX;
4✔
977
                clientY = ev.changedTouches[0].clientY;
4✔
978
            }
979

980
            let args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
132✔
981

982
            if (args.isTouch && downTime.current !== 0 && Date.now() - downTime.current > 500) {
140✔
UNCOV
983
                args = {
×
984
                    ...args,
985
                    isLongTouch: true,
986
                };
987
            }
988

989
            if (lastWasTouchRef.current !== args.isTouch) {
UNCOV
990
                setLastWasTouch(args.isTouch);
×
991
            }
992

993
            if (!isOutside && ev.cancelable) {
263✔
994
                ev.preventDefault();
131✔
995
            }
996

997
            const [col] = args.location;
132✔
998
            const headerBounds = isOverHeaderMenu(canvas, col, clientX, clientY);
132✔
999
            if (args.kind === headerKind && headerBounds !== undefined) {
152✔
1000
                if (args.button !== 0 || downPosition.current?.[0] !== col || downPosition.current?.[1] !== -1) {
27!
1001
                    // force outside so that click will not process
1002
                    onMouseUp(args, true);
1✔
1003
                }
1004
                return;
3✔
1005
            } else if (args.kind === groupHeaderKind) {
1006
                const action = groupHeaderActionForEvent(args.group, args.bounds, args.localEventX, args.localEventY);
5✔
1007
                if (action !== undefined) {
1008
                    if (args.button === 0) {
1009
                        action.onClick(args);
1✔
1010
                    }
1011
                    return;
1✔
1012
                }
1013
            }
1014

1015
            onMouseUp(args, isOutside);
128✔
1016
        },
1017
        [onMouseUp, eventTargetRef, getMouseArgsForPosition, isOverHeaderMenu, groupHeaderActionForEvent]
1018
    );
1019
    useEventListener("mouseup", onMouseUpImpl, window, false);
717✔
1020
    useEventListener("touchend", onMouseUpImpl, window, false);
717✔
1021

1022
    const onClickImpl = React.useCallback(
717✔
1023
        (ev: MouseEvent | TouchEvent) => {
1024
            const canvas = ref.current;
113✔
1025
            if (canvas === null) return;
113!
1026
            const eventTarget = eventTargetRef?.current;
113✔
1027

1028
            const isOutside = ev.target !== canvas && ev.target !== eventTarget;
113✔
1029

1030
            let clientX: number;
1031
            let clientY: number;
1032
            if (ev instanceof MouseEvent) {
1033
                clientX = ev.clientX;
113✔
1034
                clientY = ev.clientY;
113✔
1035
            } else {
1036
                clientX = ev.changedTouches[0].clientX;
×
UNCOV
1037
                clientY = ev.changedTouches[0].clientY;
×
1038
            }
1039

1040
            const args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
113✔
1041

1042
            if (lastWasTouchRef.current !== args.isTouch) {
1043
                setLastWasTouch(args.isTouch);
4✔
1044
            }
1045

1046
            if (!isOutside && ev.cancelable) {
225✔
1047
                ev.preventDefault();
112✔
1048
            }
1049

1050
            const [col] = args.location;
113✔
1051
            const headerBounds = isOverHeaderMenu(canvas, col, clientX, clientY);
113✔
1052
            if (args.kind === headerKind && headerBounds !== undefined) {
127✔
1053
                if (args.button === 0 && downPosition.current?.[0] === col && downPosition.current?.[1] === -1) {
9!
1054
                    onHeaderMenuClick?.(col, headerBounds);
1!
1055
                }
1056
            } else if (args.kind === groupHeaderKind) {
1057
                const action = groupHeaderActionForEvent(args.group, args.bounds, args.localEventX, args.localEventY);
5✔
1058
                if (action !== undefined && args.button === 0) {
6✔
1059
                    action.onClick(args);
1✔
1060
                }
1061
            }
1062
        },
1063
        [eventTargetRef, getMouseArgsForPosition, isOverHeaderMenu, onHeaderMenuClick, groupHeaderActionForEvent]
1064
    );
1065
    useEventListener("click", onClickImpl, window, false);
717✔
1066

1067
    const onContextMenuImpl = React.useCallback(
717✔
1068
        (ev: MouseEvent) => {
1069
            const canvas = ref.current;
7✔
1070
            if (canvas === null || onContextMenu === undefined) return;
7!
1071
            const args = getMouseArgsForPosition(canvas, ev.clientX, ev.clientY, ev);
7✔
1072
            onContextMenu(args, () => {
7✔
UNCOV
1073
                if (ev.cancelable) ev.preventDefault();
×
1074
            });
1075
        },
1076
        [getMouseArgsForPosition, onContextMenu]
1077
    );
1078
    useEventListener("contextmenu", onContextMenuImpl, eventTargetRef?.current ?? null, false);
717✔
1079

1080
    const onAnimationFrame = React.useCallback<StepCallback>(values => {
717✔
1081
        damageRegion.current = values.map(x => x.item);
74✔
1082
        hoverValues.current = values;
73✔
1083
        lastDrawRef.current();
73✔
1084
        damageRegion.current = undefined;
73✔
1085
    }, []);
1086

1087
    const animManagerValue = React.useMemo(() => new AnimationManager(onAnimationFrame), [onAnimationFrame]);
717✔
1088
    const animationManager = React.useRef(animManagerValue);
717✔
1089
    animationManager.current = animManagerValue;
717✔
1090
    React.useLayoutEffect(() => {
717✔
1091
        const am = animationManager.current;
205✔
1092
        if (hoveredItem === undefined || hoveredItem[1] < 0) {
243✔
1093
            am.setHovered(hoveredItem);
187✔
1094
            return;
187✔
1095
        }
1096
        const cell = getCellContent(hoveredItem as [number, number]);
18✔
1097
        const r = getCellRenderer(cell);
18✔
1098
        am.setHovered(
18✔
1099
            (r === undefined && cell.kind === GridCellKind.Custom) || r?.needsHover === true ? hoveredItem : undefined
108!
1100
        );
1101
    }, [getCellContent, getCellRenderer, hoveredItem]);
1102

1103
    const hoveredRef = React.useRef<GridMouseEventArgs>();
717✔
1104
    const onMouseMoveImpl = React.useCallback(
717✔
1105
        (ev: MouseEvent) => {
1106
            const canvas = ref.current;
43✔
1107
            if (canvas === null) return;
43!
1108

1109
            const eventTarget = eventTargetRef?.current;
43✔
1110
            const isIndirect = ev.target !== canvas && ev.target !== eventTarget;
43✔
1111

1112
            const args = getMouseArgsForPosition(canvas, ev.clientX, ev.clientY, ev);
43✔
1113
            if (args.kind !== "out-of-bounds" && isIndirect && !mouseDown.current && !args.isTouch) {
86✔
1114
                // we are obscured by something else, so we want to not register events if we are not doing anything
1115
                // important already
1116
                return;
2✔
1117
            }
1118

1119
            if (!isSameItem(args, hoveredRef.current)) {
1120
                onItemHovered?.(args);
39!
1121
                setHoveredItemInfo(
39✔
1122
                    args.kind === outOfBoundsKind ? undefined : [args.location, [args.localEventX, args.localEventY]]
39✔
1123
                );
1124
                hoveredRef.current = args;
39✔
1125
            } else if (args.kind === "cell" || args.kind === headerKind || args.kind === groupHeaderKind) {
4!
1126
                const newInfo: typeof hoverInfoRef.current = [args.location, [args.localEventX, args.localEventY]];
2✔
1127
                setHoveredItemInfo(newInfo);
2✔
1128
                hoverInfoRef.current = newInfo;
2✔
1129

1130
                if (args.kind === "cell") {
UNCOV
1131
                    const toCheck = getCellContent(args.location);
×
1132
                    if (toCheck.kind === GridCellKind.Custom || getCellRenderer(toCheck)?.needsHoverPosition === true) {
×
UNCOV
1133
                        damageInternal([args.location]);
×
1134
                    }
1135
                } else if (args.kind === groupHeaderKind) {
UNCOV
1136
                    damageInternal([args.location]);
×
1137
                }
1138
            }
1139

1140
            const notRowMarkerCol = args.location[0] >= (firstColAccessible ? 0 : 1);
41✔
1141
            setHoveredOnEdge(args.kind === headerKind && args.isEdge && notRowMarkerCol && allowResize === true);
41✔
1142

1143
            if (fillHandle && selection.current !== undefined) {
45✔
1144
                const [col, row] = selection.current.cell;
4✔
1145
                const sb = getBoundsForItem(canvas, col, row);
4✔
1146
                const x = ev.clientX;
4✔
1147
                const y = ev.clientY;
4✔
1148
                assert(sb !== undefined);
4✔
1149
                setOverFill(
4✔
1150
                    x >= sb.x + sb.width - 6 &&
16✔
1151
                        x <= sb.x + sb.width &&
1152
                        y >= sb.y + sb.height - 6 &&
1153
                        y <= sb.y + sb.height
1154
                );
1155
            } else {
1156
                setOverFill(false);
37✔
1157
            }
1158

1159
            onMouseMoveRaw?.(ev);
41!
1160
            onMouseMove(args);
41✔
1161
        },
1162
        [
1163
            eventTargetRef,
1164
            getMouseArgsForPosition,
1165
            firstColAccessible,
1166
            allowResize,
1167
            fillHandle,
1168
            selection,
1169
            onMouseMoveRaw,
1170
            onMouseMove,
1171
            onItemHovered,
1172
            getCellContent,
1173
            getCellRenderer,
1174
            damageInternal,
1175
            getBoundsForItem,
1176
        ]
1177
    );
1178
    useEventListener("mousemove", onMouseMoveImpl, window, true);
717✔
1179

1180
    const onKeyDownImpl = React.useCallback(
717✔
1181
        (event: React.KeyboardEvent<HTMLCanvasElement>) => {
1182
            const canvas = ref.current;
57✔
1183
            if (canvas === null) return;
57!
1184

1185
            let bounds: Rectangle | undefined;
1186
            let location: Item | undefined = undefined;
57✔
1187
            if (selection.current !== undefined) {
1188
                bounds = getBoundsForItem(canvas, selection.current.cell[0], selection.current.cell[1]);
54✔
1189
                location = selection.current.cell;
54✔
1190
            }
1191

1192
            onKeyDown?.({
57!
1193
                bounds,
1194
                stopPropagation: () => event.stopPropagation(),
41✔
1195
                preventDefault: () => event.preventDefault(),
41✔
UNCOV
1196
                cancel: () => undefined,
×
1197
                ctrlKey: event.ctrlKey,
1198
                metaKey: event.metaKey,
1199
                shiftKey: event.shiftKey,
1200
                altKey: event.altKey,
1201
                key: event.key,
1202
                keyCode: event.keyCode,
1203
                rawEvent: event,
1204
                location,
1205
            });
1206
        },
1207
        [onKeyDown, selection, getBoundsForItem]
1208
    );
1209

1210
    const onKeyUpImpl = React.useCallback(
717✔
1211
        (event: React.KeyboardEvent<HTMLCanvasElement>) => {
1212
            const canvas = ref.current;
7✔
1213
            if (canvas === null) return;
7!
1214

1215
            let bounds: Rectangle | undefined;
1216
            let location: Item | undefined = undefined;
7✔
1217
            if (selection.current !== undefined) {
1218
                bounds = getBoundsForItem(canvas, selection.current.cell[0], selection.current.cell[1]);
7✔
1219
                location = selection.current.cell;
7✔
1220
            }
1221

1222
            onKeyUp?.({
7✔
1223
                bounds,
1224
                stopPropagation: () => event.stopPropagation(),
×
1225
                preventDefault: () => event.preventDefault(),
×
1226
                cancel: () => undefined,
×
1227
                ctrlKey: event.ctrlKey,
1228
                metaKey: event.metaKey,
1229
                shiftKey: event.shiftKey,
1230
                altKey: event.altKey,
1231
                key: event.key,
1232
                keyCode: event.keyCode,
1233
                rawEvent: event,
1234
                location,
1235
            });
1236
        },
1237
        [onKeyUp, selection, getBoundsForItem]
1238
    );
1239

1240
    const refImpl = React.useCallback(
717✔
1241
        (instance: HTMLCanvasElement | null) => {
1242
            ref.current = instance;
288✔
1243
            if (canvasRef !== undefined) {
1244
                canvasRef.current = instance;
264✔
1245
            }
1246
        },
1247
        [canvasRef]
1248
    );
1249

1250
    const onDragStartImpl = React.useCallback(
717✔
1251
        (event: DragEvent) => {
1252
            const canvas = ref.current;
1✔
1253
            if (canvas === null || isDraggable === false || isResizing) {
3✔
UNCOV
1254
                event.preventDefault();
×
UNCOV
1255
                return;
×
1256
            }
1257

1258
            let dragMime: string | undefined;
1259
            let dragData: string | undefined;
1260

1261
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
1✔
1262

1263
            if (isDraggable !== true && args.kind !== isDraggable) {
1!
UNCOV
1264
                event.preventDefault();
×
UNCOV
1265
                return;
×
1266
            }
1267

1268
            const setData = (mime: string, payload: string) => {
1✔
1269
                dragMime = mime;
1✔
1270
                dragData = payload;
1✔
1271
            };
1272

1273
            let dragImage: Element | undefined;
1274
            let dragImageX: number | undefined;
1275
            let dragImageY: number | undefined;
1276
            const setDragImage = (image: Element, x: number, y: number) => {
1✔
UNCOV
1277
                dragImage = image;
×
UNCOV
1278
                dragImageX = x;
×
UNCOV
1279
                dragImageY = y;
×
1280
            };
1281

1282
            let prevented = false;
1✔
1283

1284
            onDragStart?.({
1!
1285
                ...args,
1286
                setData,
1287
                setDragImage,
UNCOV
1288
                preventDefault: () => (prevented = true),
×
1289
                defaultPrevented: () => prevented,
2✔
1290
            });
1291
            if (!prevented && dragMime !== undefined && dragData !== undefined && event.dataTransfer !== null) {
4✔
1292
                event.dataTransfer.setData(dragMime, dragData);
1✔
1293
                event.dataTransfer.effectAllowed = "copyLink";
1✔
1294

1295
                if (dragImage !== undefined && dragImageX !== undefined && dragImageY !== undefined) {
1!
UNCOV
1296
                    event.dataTransfer.setDragImage(dragImage, dragImageX, dragImageY);
×
1297
                } else {
1298
                    const [col, row] = args.location;
1✔
1299
                    if (row !== undefined) {
1300
                        const offscreen = document.createElement("canvas");
1✔
1301
                        const boundsForDragTarget = getBoundsForItem(canvas, col, row);
1✔
1302

1303
                        assert(boundsForDragTarget !== undefined);
1✔
1304
                        const dpr = Math.ceil(window.devicePixelRatio ?? 1);
1!
1305
                        offscreen.width = boundsForDragTarget.width * dpr;
1✔
1306
                        offscreen.height = boundsForDragTarget.height * dpr;
1✔
1307

1308
                        const ctx = offscreen.getContext("2d");
1✔
1309
                        if (ctx !== null) {
1310
                            ctx.scale(dpr, dpr);
1✔
1311
                            ctx.textBaseline = "middle";
1✔
1312
                            if (row === -1) {
UNCOV
1313
                                ctx.font = `${theme.headerFontStyle} ${theme.fontFamily}`;
×
UNCOV
1314
                                ctx.fillStyle = theme.bgHeader;
×
UNCOV
1315
                                ctx.fillRect(0, 0, offscreen.width, offscreen.height);
×
UNCOV
1316
                                drawHeader(
×
1317
                                    ctx,
1318
                                    0,
1319
                                    0,
1320
                                    boundsForDragTarget.width,
1321
                                    boundsForDragTarget.height,
1322
                                    mappedColumns[col],
1323
                                    false,
1324
                                    theme,
1325
                                    false,
1326
                                    false,
1327
                                    0,
1328
                                    spriteManager,
1329
                                    drawHeaderCallback,
1330
                                    false
1331
                                );
1332
                            } else {
1333
                                ctx.font = `${theme.baseFontStyle} ${theme.fontFamily}`;
1✔
1334
                                ctx.fillStyle = theme.bgCell;
1✔
1335
                                ctx.fillRect(0, 0, offscreen.width, offscreen.height);
1✔
1336
                                drawCell(
1✔
1337
                                    ctx,
1338
                                    row,
1339
                                    getCellContent([col, row]),
1340
                                    0,
1341
                                    0,
1342
                                    0,
1343
                                    boundsForDragTarget.width,
1344
                                    boundsForDragTarget.height,
1345
                                    false,
1346
                                    theme,
1347
                                    drawCustomCell,
1348
                                    imageLoader,
1349
                                    spriteManager,
1350
                                    1,
1351
                                    undefined,
1352
                                    false,
1353
                                    0,
1354
                                    undefined,
1355
                                    undefined,
1356
                                    getCellRenderer
1357
                                );
1358
                            }
1359
                        }
1360

1361
                        offscreen.style.left = "-100%";
1✔
1362
                        offscreen.style.position = "absolute";
1✔
1363
                        offscreen.style.width = `${boundsForDragTarget.width}px`;
1✔
1364
                        offscreen.style.height = `${boundsForDragTarget.height}px`;
1✔
1365

1366
                        document.body.append(offscreen);
1✔
1367

1368
                        event.dataTransfer.setDragImage(
1✔
1369
                            offscreen,
1370
                            boundsForDragTarget.width / 2,
1371
                            boundsForDragTarget.height / 2
1372
                        );
1373

1374
                        window.setTimeout(() => {
1✔
1375
                            offscreen.remove();
1✔
1376
                        }, 0);
1377
                    }
1378
                }
1379
            } else {
UNCOV
1380
                event.preventDefault();
×
1381
            }
1382
        },
1383
        [
1384
            isDraggable,
1385
            isResizing,
1386
            getMouseArgsForPosition,
1387
            onDragStart,
1388
            getBoundsForItem,
1389
            theme,
1390
            mappedColumns,
1391
            spriteManager,
1392
            drawHeaderCallback,
1393
            getCellContent,
1394
            drawCustomCell,
1395
            imageLoader,
1396
            getCellRenderer,
1397
        ]
1398
    );
1399
    useEventListener("dragstart", onDragStartImpl, eventTargetRef?.current ?? null, false, false);
717✔
1400

1401
    const activeDropTarget = React.useRef<Item | undefined>();
717✔
1402

1403
    const onDragOverImpl = React.useCallback(
717✔
1404
        (event: DragEvent) => {
1405
            const canvas = ref.current;
×
1406
            if (onDrop !== undefined) {
1407
                // Need to preventDefault to allow drop
UNCOV
1408
                event.preventDefault();
×
1409
            }
1410

1411
            if (canvas === null || onDragOverCell === undefined) {
×
UNCOV
1412
                return;
×
1413
            }
1414

UNCOV
1415
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
×
1416

UNCOV
1417
            const [rawCol, row] = args.location;
×
UNCOV
1418
            const col = rawCol - (firstColAccessible ? 0 : 1);
×
UNCOV
1419
            const [activeCol, activeRow] = activeDropTarget.current ?? [];
×
1420

1421
            if (activeCol !== col || activeRow !== row) {
×
UNCOV
1422
                activeDropTarget.current = [col, row];
×
UNCOV
1423
                onDragOverCell([col, row], event.dataTransfer);
×
1424
            }
1425
        },
1426
        [firstColAccessible, getMouseArgsForPosition, onDragOverCell, onDrop]
1427
    );
1428
    useEventListener("dragover", onDragOverImpl, eventTargetRef?.current ?? null, false, false);
717✔
1429

1430
    const onDragEndImpl = React.useCallback(() => {
717✔
UNCOV
1431
        activeDropTarget.current = undefined;
×
UNCOV
1432
        onDragEnd?.();
×
1433
    }, [onDragEnd]);
1434
    useEventListener("dragend", onDragEndImpl, eventTargetRef?.current ?? null, false, false);
717✔
1435

1436
    const onDropImpl = React.useCallback(
717✔
1437
        (event: DragEvent) => {
UNCOV
1438
            const canvas = ref.current;
×
1439
            if (canvas === null || onDrop === undefined) {
×
UNCOV
1440
                return;
×
1441
            }
1442

1443
            // Default can mess up sometimes.
UNCOV
1444
            event.preventDefault();
×
1445

UNCOV
1446
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
×
1447

UNCOV
1448
            const [rawCol, row] = args.location;
×
UNCOV
1449
            const col = rawCol - (firstColAccessible ? 0 : 1);
×
1450

1451
            onDrop([col, row], event.dataTransfer);
×
1452
        },
1453
        [firstColAccessible, getMouseArgsForPosition, onDrop]
1454
    );
1455
    useEventListener("drop", onDropImpl, eventTargetRef?.current ?? null, false, false);
717✔
1456

1457
    const onDragLeaveImpl = React.useCallback(() => {
717✔
UNCOV
1458
        onDragLeave?.();
×
1459
    }, [onDragLeave]);
1460
    useEventListener("dragleave", onDragLeaveImpl, eventTargetRef?.current ?? null, false, false);
717✔
1461

1462
    const selectionRef = React.useRef(selection);
717✔
1463
    selectionRef.current = selection;
717✔
1464
    const focusRef = React.useRef<HTMLElement | null>(null);
717✔
1465
    const focusElement = React.useCallback(
717✔
1466
        (el: HTMLElement | null) => {
1467
            // We don't want to steal the focus if we don't currently own the focus.
1468
            if (ref.current === null || !ref.current.contains(document.activeElement)) return;
59✔
1469
            if (el === null && selectionRef.current.current !== undefined) {
37✔
1470
                canvasRef?.current?.focus({
7!
1471
                    preventScroll: true,
1472
                });
1473
            } else if (el !== null) {
1474
                el.focus({
23✔
1475
                    preventScroll: true,
1476
                });
1477
            }
1478
            focusRef.current = el;
30✔
1479
        },
1480
        [canvasRef]
1481
    );
1482

1483
    React.useImperativeHandle(
717✔
1484
        forwardedRef,
1485
        () => ({
285✔
1486
            focus: () => {
1487
                const el = focusRef.current;
33✔
1488
                // The element in the ref may have been removed however our callback method ref
1489
                // won't see the removal so bad things happen. Checking to see if the element is
1490
                // no longer attached is enough to resolve the problem. In the future this
1491
                // should be replaced with something much more robust.
1492
                if (el === null || !document.contains(el)) {
39✔
1493
                    canvasRef?.current?.focus({
27!
1494
                        preventScroll: true,
1495
                    });
1496
                } else {
1497
                    el.focus({
6✔
1498
                        preventScroll: true,
1499
                    });
1500
                }
1501
            },
1502
            getBounds: (col?: number, row?: number) => {
1503
                if (canvasRef === undefined || canvasRef.current === null) {
90✔
1504
                    return undefined;
×
1505
                }
1506

1507
                return getBoundsForItem(canvasRef.current, col ?? 0, row ?? -1);
45!
1508
            },
1509
            damage,
1510
        }),
1511
        [canvasRef, damage, getBoundsForItem]
1512
    );
1513

1514
    const lastFocusedSubdomNode = React.useRef<Item>();
717✔
1515

1516
    const accessibilityTree = useDebouncedMemo(
717✔
1517
        () => {
1518
            if (width < 50) return null;
343✔
1519
            let effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX);
81✔
1520
            const colOffset = firstColAccessible ? 0 : -1;
81✔
1521
            if (!firstColAccessible && effectiveCols[0]?.sourceIndex === 0) {
105!
1522
                effectiveCols = effectiveCols.slice(1);
6✔
1523
            }
1524

1525
            const [fCol, fRow] = selection.current?.cell ?? [];
81✔
1526
            const range = selection.current?.range;
81✔
1527

1528
            const visibleCols = effectiveCols.map(c => c.sourceIndex);
747✔
1529
            const visibleRows = makeRange(cellYOffset, Math.min(rows, cellYOffset + accessibilityHeight));
81✔
1530

1531
            // Maintain focus within grid if we own it but focused cell is outside visible viewport
1532
            // and not rendered.
1533
            if (
1534
                fCol !== undefined &&
153✔
1535
                fRow !== undefined &&
1536
                !(visibleCols.includes(fCol) && visibleRows.includes(fRow))
71✔
1537
            ) {
1538
                focusElement(null);
1✔
1539
            }
1540

1541
            return (
81✔
1542
                <table
1543
                    key="access-tree"
1544
                    role="grid"
1545
                    aria-rowcount={rows + 1}
1546
                    aria-multiselectable="true"
1547
                    aria-colcount={mappedColumns.length + colOffset}>
1548
                    <thead role="rowgroup">
1549
                        <tr role="row" aria-rowindex={1}>
1550
                            {effectiveCols.map(c => (
1551
                                <th
747✔
1552
                                    role="columnheader"
1553
                                    aria-selected={selection.columns.hasIndex(c.sourceIndex)}
1554
                                    aria-colindex={c.sourceIndex + 1 + colOffset}
1555
                                    tabIndex={-1}
1556
                                    onFocus={e => {
UNCOV
1557
                                        if (e.target === focusRef.current) return;
×
UNCOV
1558
                                        return onCellFocused?.([c.sourceIndex, -1]);
×
1559
                                    }}
1560
                                    key={c.sourceIndex}>
1561
                                    {c.title}
1562
                                </th>
1563
                            ))}
1564
                        </tr>
1565
                    </thead>
1566
                    <tbody role="rowgroup">
1567
                        {visibleRows.map(row => (
1568
                            <tr
2,678✔
1569
                                role="row"
1570
                                aria-selected={selection.rows.hasIndex(row)}
1571
                                key={row}
1572
                                aria-rowindex={row + 2}>
1573
                                {effectiveCols.map(c => {
1574
                                    const col = c.sourceIndex;
23,648✔
1575
                                    const key = `${col},${row}`;
23,648✔
1576
                                    const focused = fCol === col && fRow === row;
23,648✔
1577
                                    const selected =
1578
                                        range !== undefined &&
23,648✔
1579
                                        col >= range.x &&
1580
                                        col < range.x + range.width &&
1581
                                        row >= range.y &&
1582
                                        row < range.y + range.height;
1583
                                    const id = `glide-cell-${col}-${row}`;
23,648✔
1584
                                    const location: Item = [col, row];
23,648✔
1585
                                    const cellContent = getCellContent(location, true);
23,648✔
1586
                                    return (
23,648✔
1587
                                        <td
1588
                                            key={key}
1589
                                            role="gridcell"
1590
                                            aria-colindex={col + 1 + colOffset}
1591
                                            aria-selected={selected}
1592
                                            aria-readonly={
1593
                                                isInnerOnlyCell(cellContent) || !isReadWriteCell(cellContent)
47,246✔
1594
                                            }
1595
                                            id={id}
1596
                                            data-testid={id}
1597
                                            onClick={() => {
1598
                                                const canvas = canvasRef?.current;
1!
1599
                                                if (canvas === null || canvas === undefined) return;
1!
1600
                                                return onKeyDown?.({
1!
1601
                                                    bounds: getBoundsForItem(canvas, col, row),
UNCOV
1602
                                                    cancel: () => undefined,
×
UNCOV
1603
                                                    preventDefault: () => undefined,
×
UNCOV
1604
                                                    stopPropagation: () => undefined,
×
1605
                                                    ctrlKey: false,
1606
                                                    key: "Enter",
1607
                                                    keyCode: 13,
1608
                                                    metaKey: false,
1609
                                                    shiftKey: false,
1610
                                                    altKey: false,
1611
                                                    rawEvent: undefined,
1612
                                                    location,
1613
                                                });
1614
                                            }}
1615
                                            onFocusCapture={e => {
1616
                                                if (
27✔
1617
                                                    e.target === focusRef.current ||
55✔
1618
                                                    (lastFocusedSubdomNode.current?.[0] === col &&
72✔
1619
                                                        lastFocusedSubdomNode.current?.[1] === row)
12!
1620
                                                )
1621
                                                    return;
3✔
1622
                                                lastFocusedSubdomNode.current = location;
24✔
1623
                                                return onCellFocused?.(location);
24!
1624
                                            }}
1625
                                            ref={focused ? focusElement : undefined}
23,648✔
1626
                                            tabIndex={-1}>
1627
                                            {getRowData(cellContent, getCellRenderer)}
1628
                                        </td>
1629
                                    );
1630
                                })}
1631
                            </tr>
1632
                        ))}
1633
                    </tbody>
1634
                </table>
1635
            );
1636
        },
1637
        [
1638
            width,
1639
            mappedColumns,
1640
            cellXOffset,
1641
            dragAndDropState,
1642
            translateX,
1643
            rows,
1644
            cellYOffset,
1645
            accessibilityHeight,
1646
            selection,
1647
            focusElement,
1648
            getCellContent,
1649
            canvasRef,
1650
            onKeyDown,
1651
            getBoundsForItem,
1652
            onCellFocused,
1653
        ],
1654
        200
1655
    );
1656

1657
    const stickyX = fixedShadowX ? getStickyWidth(mappedColumns, dragAndDropState) : 0;
717!
1658
    const opacityX =
1659
        freezeColumns === 0 || !fixedShadowX ? 0 : cellXOffset > freezeColumns ? 1 : clamp(-translateX / 100, 0, 1);
717✔
1660

1661
    const absoluteOffsetY = -cellYOffset * 32 + translateY;
717✔
1662
    const opacityY = !fixedShadowY ? 0 : clamp(-absoluteOffsetY / 100, 0, 1);
717!
1663

1664
    const stickyShadow = React.useMemo(() => {
717✔
1665
        if (!opacityX && !opacityY) {
555✔
1666
            return null;
275✔
1667
        }
1668

1669
        const styleX: React.CSSProperties = {
3✔
1670
            position: "absolute",
1671
            top: 0,
1672
            left: stickyX,
1673
            width: width - stickyX,
1674
            height: height,
1675
            opacity: opacityX,
1676
            pointerEvents: "none",
1677
            transition: !smoothScrollX ? "opacity 0.2s" : undefined,
3!
1678
            boxShadow: "inset 13px 0 10px -13px rgba(0, 0, 0, 0.2)",
1679
        };
1680

1681
        const styleY: React.CSSProperties = {
3✔
1682
            position: "absolute",
1683
            top: totalHeaderHeight,
1684
            left: 0,
1685
            width: width,
1686
            height: height,
1687
            opacity: opacityY,
1688
            pointerEvents: "none",
1689
            transition: !smoothScrollY ? "opacity 0.2s" : undefined,
3!
1690
            boxShadow: "inset 0 13px 10px -13px rgba(0, 0, 0, 0.2)",
1691
        };
1692

1693
        return (
3✔
1694
            <>
1695
                {opacityX > 0 && <div id="shadow-x" style={styleX} />}
4✔
1696
                {opacityY > 0 && <div id="shadow-y" style={styleY} />}
5✔
1697
            </>
1698
        );
1699
    }, [opacityX, opacityY, stickyX, width, smoothScrollX, totalHeaderHeight, height, smoothScrollY]);
1700

1701
    const overlayStyle = React.useMemo<React.CSSProperties>(
717✔
1702
        () => ({
144✔
1703
            position: "absolute",
1704
            top: 0,
1705
            left: 0,
1706
        }),
1707
        []
1708
    );
1709

1710
    return (
717✔
1711
        <>
1712
            <canvas
1713
                data-testid="data-grid-canvas"
1714
                tabIndex={0}
1715
                onKeyDown={onKeyDownImpl}
1716
                onKeyUp={onKeyUpImpl}
1717
                onFocus={onCanvasFocused}
1718
                onBlur={onCanvasBlur}
1719
                ref={refImpl}
1720
                style={style}>
1721
                {accessibilityTree}
1722
            </canvas>
1723
            <canvas ref={overlayRef} style={overlayStyle} />
1724
            {stickyShadow}
1725
        </>
1726
    );
1727
};
1728

1729
export default React.memo(React.forwardRef(DataGrid));
9✔
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