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

glideapps / glide-data-grid / 7213760293

14 Dec 2023 07:41PM UTC coverage: 90.701% (+4.3%) from 86.42%
7213760293

Pull #810

github

jassmith
5.99.9-beta5
Pull Request #810: 6.0.0

2548 of 3185 branches covered (0.0%)

2933 of 3342 new or added lines in 61 files covered. (87.76%)

264 existing lines in 12 files now uncovered.

15547 of 17141 relevant lines covered (90.7%)

2973.79 hits per line

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

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

1✔
60
export interface DataGridProps {
1✔
61
    readonly width: number;
1✔
62
    readonly height: number;
1✔
63

1✔
64
    readonly cellXOffset: number;
1✔
65
    readonly cellYOffset: number;
1✔
66

1✔
67
    readonly translateX: number | undefined;
1✔
68
    readonly translateY: number | undefined;
1✔
69

1✔
70
    readonly accessibilityHeight: number;
1✔
71

1✔
72
    readonly freezeColumns: number;
1✔
73
    readonly trailingRowType: TrailingRowType;
1✔
74
    readonly firstColAccessible: boolean;
1✔
75

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

1✔
87
    readonly allowResize: boolean | undefined;
1✔
88
    readonly isResizing: boolean;
1✔
89
    readonly resizeColumn: number | undefined;
1✔
90
    readonly isDragging: boolean;
1✔
91
    readonly isFilling: boolean;
1✔
92
    readonly isFocused: boolean;
1✔
93

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

1✔
101
    readonly headerHeight: number;
1✔
102
    readonly groupHeaderHeight: number;
1✔
103
    readonly enableGroups: boolean;
1✔
104
    readonly rowHeight: number | ((index: number) => number);
1✔
105

1✔
106
    readonly canvasRef: React.MutableRefObject<HTMLCanvasElement | null> | undefined;
1✔
107

1✔
108
    readonly eventTargetRef: React.MutableRefObject<HTMLDivElement | null> | undefined;
1✔
109

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

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

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

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

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

1✔
159
    readonly onCanvasFocused: () => void;
1✔
160
    readonly onCanvasBlur: () => void;
1✔
161
    readonly onCellFocused: (args: Item) => void;
1✔
162

1✔
163
    readonly onMouseMoveRaw: (event: MouseEvent) => void;
1✔
164

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

1✔
176
    readonly verticalBorder: (col: number) => boolean;
1✔
177

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

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

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

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

1✔
215
    readonly drawCell: DrawCellCallback | undefined;
1✔
216

1✔
217
    /**
1✔
218
     * Controls the drawing of the focus ring.
1✔
219
     * @defaultValue true
1✔
220
     * @group Style
1✔
221
     */
1✔
222
    readonly drawFocusRing: boolean | undefined;
1✔
223

1✔
224
    readonly dragAndDropState:
1✔
225
        | {
1✔
226
              src: number;
1✔
227
              dest: number;
1✔
228
          }
1✔
229
        | undefined;
1✔
230

1✔
231
    /**
1✔
232
     * Experimental features
1✔
233
     * @group Advanced
1✔
234
     * @experimental
1✔
235
     */
1✔
236
    readonly experimental:
1✔
237
        | {
1✔
238
              readonly disableAccessibilityTree?: boolean;
1✔
239
              readonly disableMinimumCellWidth?: boolean;
1✔
240
              readonly paddingRight?: number;
1✔
241
              readonly paddingBottom?: number;
1✔
242
              readonly enableFirefoxRescaling?: boolean;
1✔
243
              readonly kineticScrollPerfHack?: boolean;
1✔
244
              readonly isSubGrid?: boolean;
1✔
245
              readonly strict?: boolean;
1✔
246
              readonly scrollbarWidthOverride?: number;
1✔
247
              readonly hyperWrapping?: boolean;
1✔
248
              readonly renderStrategy?: "single-buffer" | "double-buffer" | "direct";
1✔
249
          }
1✔
250
        | undefined;
1✔
251

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

1✔
268
    /** Controls smooth scrolling in the data grid. If smooth scrolling is not enabled the grid will always be cell
1✔
269
     * aligned.
1✔
270
     * @defaultValue `false`
1✔
271
     * @group Style
1✔
272
     */
1✔
273
    readonly smoothScrollX: boolean | undefined;
1✔
274
    /** Controls smooth scrolling in the data grid. If smooth scrolling is not enabled the grid will always be cell
1✔
275
     * aligned.
1✔
276
     * @defaultValue `false`
1✔
277
     * @group Style
1✔
278
     */
1✔
279
    readonly smoothScrollY: boolean | undefined;
1✔
280

1✔
281
    readonly theme: FullTheme;
1✔
282

1✔
283
    readonly getCellRenderer: <T extends InnerGridCell>(cell: T) => CellRenderer<T> | undefined;
1✔
284
}
1✔
285

1✔
286
type DamageUpdateList = readonly {
1✔
287
    cell: Item;
1✔
288
    // newValue: GridCell,
1✔
289
}[];
1✔
290

1✔
291
const fillHandleClickSize = 8;
1✔
292

1✔
293
export interface DataGridRef {
1✔
294
    focus: () => void;
1✔
295
    getBounds: (col?: number, row?: number) => Rectangle | undefined;
1✔
296
    damage: (cells: DamageUpdateList) => void;
1✔
297
}
1✔
298

1✔
299
const getRowData = (cell: InnerGridCell, getCellRenderer?: GetCellRendererCallback) => {
1✔
300
    if (cell.kind === GridCellKind.Custom) return cell.copyData;
23,680✔
301
    const r = getCellRenderer?.(cell);
21,760✔
302
    return r?.getAccessibilityString(cell) ?? "";
23,680!
303
};
23,680✔
304

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

777✔
374
    const ref = React.useRef<HTMLCanvasElement | null>(null);
777✔
375
    const imageLoader = imageWindowLoader;
777✔
376
    const damageRegion = React.useRef<CellSet | undefined>();
777✔
377
    const [scrolling, setScrolling] = React.useState<boolean>(false);
777✔
378
    const hoverValues = React.useRef<readonly { item: Item; hoverAmount: number }[]>([]);
777✔
379
    const lastBlitData = React.useRef<BlitData | undefined>();
777✔
380
    const [hoveredItemInfo, setHoveredItemInfo] = React.useState<[Item, readonly [number, number]] | undefined>();
777✔
381
    const [hoveredOnEdge, setHoveredOnEdge] = React.useState<boolean>();
777✔
382
    const overlayRef = React.useRef<HTMLCanvasElement | null>(null);
777✔
383
    const [drawCursorOverride, setDrawCursorOverride] = React.useState<React.CSSProperties["cursor"] | undefined>();
777✔
384

777✔
385
    const [lastWasTouch, setLastWasTouch] = React.useState(false);
777✔
386
    const lastWasTouchRef = React.useRef(lastWasTouch);
777✔
387
    lastWasTouchRef.current = lastWasTouch;
777✔
388

777✔
389
    const spriteManager = React.useMemo(
777✔
390
        () =>
777✔
391
            new SpriteManager(headerIcons, () => {
145✔
392
                lastArgsRef.current = undefined;
×
393
                lastDrawRef.current();
×
394
            }),
145✔
395
        [headerIcons]
777✔
396
    );
777✔
397
    const totalHeaderHeight = enableGroups ? groupHeaderHeight + headerHeight : headerHeight;
777✔
398

777✔
399
    const scrollingStopRef = React.useRef(-1);
777✔
400
    const disableFirefoxRescaling = experimental?.enableFirefoxRescaling !== true;
777✔
401
    React.useLayoutEffect(() => {
777✔
402
        if (!browserIsFirefox.value || window.devicePixelRatio === 1 || disableFirefoxRescaling) return;
149!
403
        // We don't want to go into scroll mode for a single repaint
×
404
        if (scrollingStopRef.current !== -1) {
×
405
            setScrolling(true);
×
406
        }
×
407
        window.clearTimeout(scrollingStopRef.current);
×
408
        scrollingStopRef.current = window.setTimeout(() => {
×
409
            setScrolling(false);
×
410
            scrollingStopRef.current = -1;
×
411
        }, 200);
×
412
    }, [cellYOffset, cellXOffset, translateX, translateY, disableFirefoxRescaling]);
777✔
413

777✔
414
    const mappedColumns = useMappedColumns(columns, freezeColumns);
777✔
415

777✔
416
    // row: -1 === columnHeader, -2 === groupHeader
777✔
417
    const getBoundsForItem = React.useCallback(
777✔
418
        (canvas: HTMLCanvasElement, col: number, row: number): Rectangle | undefined => {
777✔
419
            const rect = canvas.getBoundingClientRect();
544✔
420

544✔
421
            if (col >= mappedColumns.length || row >= rows) {
544!
422
                return undefined;
×
423
            }
×
424

544✔
425
            const scale = rect.width / width;
544✔
426

544✔
427
            const result = computeBounds(
544✔
428
                col,
544✔
429
                row,
544✔
430
                width,
544✔
431
                height,
544✔
432
                groupHeaderHeight,
544✔
433
                totalHeaderHeight,
544✔
434
                cellXOffset,
544✔
435
                cellYOffset,
544✔
436
                translateX,
544✔
437
                translateY,
544✔
438
                rows,
544✔
439
                freezeColumns,
544✔
440
                trailingRowType === "sticky",
544✔
441
                mappedColumns,
544✔
442
                rowHeight
544✔
443
            );
544✔
444

544✔
445
            if (scale !== 1) {
544!
446
                result.x *= scale;
×
447
                result.y *= scale;
×
448
                result.width *= scale;
×
449
                result.height *= scale;
×
450
            }
×
451

544✔
452
            result.x += rect.x;
544✔
453
            result.y += rect.y;
544✔
454

544✔
455
            return result;
544✔
456
        },
544✔
457
        [
777✔
458
            width,
777✔
459
            height,
777✔
460
            groupHeaderHeight,
777✔
461
            totalHeaderHeight,
777✔
462
            cellXOffset,
777✔
463
            cellYOffset,
777✔
464
            translateX,
777✔
465
            translateY,
777✔
466
            rows,
777✔
467
            freezeColumns,
777✔
468
            trailingRowType,
777✔
469
            mappedColumns,
777✔
470
            rowHeight,
777✔
471
        ]
777✔
472
    );
777✔
473

777✔
474
    const getMouseArgsForPosition = React.useCallback(
777✔
475
        (canvas: HTMLCanvasElement, posX: number, posY: number, ev?: MouseEvent | TouchEvent): GridMouseEventArgs => {
777✔
476
            const rect = canvas.getBoundingClientRect();
434✔
477
            const scale = rect.width / width;
434✔
478
            const x = (posX - rect.left) / scale;
434✔
479
            const y = (posY - rect.top) / scale;
434✔
480
            const edgeDetectionBuffer = 5;
434✔
481

434✔
482
            const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, undefined, translateX);
434✔
483

434✔
484
            let button = 0;
434✔
485
            if (ev instanceof MouseEvent) {
434✔
486
                button = ev.button;
425✔
487
            }
425✔
488

434✔
489
            // -1 === off right edge
434✔
490
            const col = getColumnIndexForX(x, effectiveCols, translateX);
434✔
491

434✔
492
            // -1: header or above
434✔
493
            // undefined: offbottom
434✔
494
            const row = getRowIndexForY(
434✔
495
                y,
434✔
496
                height,
434✔
497
                enableGroups,
434✔
498
                headerHeight,
434✔
499
                groupHeaderHeight,
434✔
500
                rows,
434✔
501
                rowHeight,
434✔
502
                cellYOffset,
434✔
503
                translateY,
434✔
504
                trailingRowType === "sticky"
434✔
505
            );
434✔
506

434✔
507
            const shiftKey = ev?.shiftKey === true;
434✔
508
            const ctrlKey = ev?.ctrlKey === true;
434✔
509
            const metaKey = ev?.metaKey === true;
434✔
510
            const isTouch = (ev !== undefined && !(ev instanceof MouseEvent)) || (ev as any)?.pointerType === "touch";
434✔
511

434✔
512
            const edgeSize = 20;
434✔
513
            const scrollEdge: GridMouseEventArgs["scrollEdge"] = [
434✔
514
                Math.abs(x) < edgeSize ? -1 : Math.abs(rect.width - x) < edgeSize ? 1 : 0,
434✔
515
                Math.abs(y) < edgeSize ? -1 : Math.abs(rect.height - y) < edgeSize ? 1 : 0,
434✔
516
            ];
434✔
517

434✔
518
            let result: GridMouseEventArgs;
434✔
519
            if (col === -1 || y < 0 || x < 0 || row === undefined || x > width || y > height) {
434✔
520
                const horizontal = x > width ? 1 : x < 0 ? -1 : 0;
14!
521
                const vertical = y > height ? 1 : y < 0 ? -1 : 0;
14!
522

14✔
523
                let innerHorizontal: OutOfBoundsRegionAxis = horizontal * 2;
14✔
524
                let innerVertical: OutOfBoundsRegionAxis = vertical * 2;
14✔
525
                if (horizontal === 0)
14✔
526
                    innerHorizontal = col === -1 ? OutOfBoundsRegionAxis.EndPadding : OutOfBoundsRegionAxis.Center;
14✔
527
                if (vertical === 0)
14✔
528
                    innerVertical = row === undefined ? OutOfBoundsRegionAxis.EndPadding : OutOfBoundsRegionAxis.Center;
14✔
529

14✔
530
                let isEdge = false;
14✔
531
                if (col === -1 && row === -1) {
14✔
532
                    const b = getBoundsForItem(canvas, mappedColumns.length - 1, -1);
4✔
533
                    assert(b !== undefined);
4✔
534
                    isEdge = posX < b.x + b.width + edgeDetectionBuffer;
4✔
535
                }
4✔
536

14✔
537
                // This is used to ensure that clicking on the scrollbar doesn't unset the selection.
14✔
538
                // Unfortunately this doesn't work for overlay scrollbars because they are just a broken interaction
14✔
539
                // by design.
14✔
540
                const isMaybeScrollbar =
14✔
541
                    (x > width && x < width + getScrollBarWidth()) || (y > height && y < height + getScrollBarWidth());
14!
542

14✔
543
                result = {
14✔
544
                    kind: outOfBoundsKind,
14✔
545
                    location: [col !== -1 ? col : x < 0 ? 0 : mappedColumns.length - 1, row ?? rows - 1],
14!
546
                    region: [innerHorizontal, innerVertical],
14✔
547
                    shiftKey,
14✔
548
                    ctrlKey,
14✔
549
                    metaKey,
14✔
550
                    isEdge,
14✔
551
                    isTouch,
14✔
552
                    button,
14✔
553
                    scrollEdge,
14✔
554
                    isMaybeScrollbar,
14✔
555
                };
14✔
556
            } else if (row <= -1) {
434✔
557
                let bounds = getBoundsForItem(canvas, col, row);
89✔
558
                assert(bounds !== undefined);
89✔
559
                let isEdge = bounds !== undefined && bounds.x + bounds.width - posX <= edgeDetectionBuffer;
89✔
560

89✔
561
                const previousCol = col - 1;
89✔
562
                if (posX - bounds.x <= edgeDetectionBuffer && previousCol >= 0) {
89!
563
                    isEdge = true;
×
564
                    bounds = getBoundsForItem(canvas, previousCol, row);
×
565
                    assert(bounds !== undefined);
×
566
                    result = {
×
567
                        kind: enableGroups && row === -2 ? groupHeaderKind : headerKind,
×
568
                        location: [previousCol, row] as any,
×
569
                        bounds: bounds,
×
570
                        group: mappedColumns[previousCol].group ?? "",
×
571
                        isEdge,
×
572
                        shiftKey,
×
573
                        ctrlKey,
×
574
                        metaKey,
×
575
                        isTouch,
×
576
                        localEventX: posX - bounds.x,
×
577
                        localEventY: posY - bounds.y,
×
578
                        button,
×
579
                        scrollEdge,
×
580
                    };
×
581
                } else {
89✔
582
                    result = {
89✔
583
                        kind: enableGroups && row === -2 ? groupHeaderKind : headerKind,
89✔
584
                        group: mappedColumns[col].group ?? "",
89✔
585
                        location: [col, row] as any,
89✔
586
                        bounds: bounds,
89✔
587
                        isEdge,
89✔
588
                        shiftKey,
89✔
589
                        ctrlKey,
89✔
590
                        metaKey,
89✔
591
                        isTouch,
89✔
592
                        localEventX: posX - bounds.x,
89✔
593
                        localEventY: posY - bounds.y,
89✔
594
                        button,
89✔
595
                        scrollEdge,
89✔
596
                    };
89✔
597
                }
89✔
598
            } else {
420✔
599
                const bounds = getBoundsForItem(canvas, col, row);
331✔
600
                assert(bounds !== undefined);
331✔
601
                const isEdge = bounds !== undefined && bounds.x + bounds.width - posX < edgeDetectionBuffer;
331✔
602

331✔
603
                const canBeFillHandle =
331✔
604
                    fillHandle &&
331✔
605
                    bounds !== undefined &&
25✔
606
                    bounds.x + bounds.width - posX < fillHandleClickSize &&
25✔
607
                    bounds.y + bounds.height - posY < fillHandleClickSize;
10✔
608

331✔
609
                let isFillHandle = false;
331✔
610
                if (selection.current !== undefined && canBeFillHandle) {
331✔
611
                    const targetCell = [
7✔
612
                        selection.current.range.x + selection.current.range.width - 1,
7✔
613
                        selection.current.range.y + selection.current.range.height - 1,
7✔
614
                    ];
7✔
615
                    isFillHandle = targetCell[0] === col && targetCell[1] === row;
7✔
616
                }
7✔
617

331✔
618
                result = {
331✔
619
                    kind: "cell",
331✔
620
                    location: [col, row],
331✔
621
                    bounds: bounds,
331✔
622
                    isEdge,
331✔
623
                    shiftKey,
331✔
624
                    ctrlKey,
331✔
625
                    isFillHandle,
331✔
626
                    metaKey,
331✔
627
                    isTouch,
331✔
628
                    localEventX: posX - bounds.x,
331✔
629
                    localEventY: posY - bounds.y,
331✔
630
                    button,
331✔
631
                    scrollEdge,
331✔
632
                };
331✔
633
            }
331✔
634
            return result;
434✔
635
        },
434✔
636
        [
777✔
637
            width,
777✔
638
            mappedColumns,
777✔
639
            cellXOffset,
777✔
640
            translateX,
777✔
641
            height,
777✔
642
            enableGroups,
777✔
643
            headerHeight,
777✔
644
            groupHeaderHeight,
777✔
645
            rows,
777✔
646
            rowHeight,
777✔
647
            cellYOffset,
777✔
648
            translateY,
777✔
649
            trailingRowType,
777✔
650
            getBoundsForItem,
777✔
651
            fillHandle,
777✔
652
            selection,
777✔
653
        ]
777✔
654
    );
777✔
655

777✔
656
    function isSameItem(item: GridMouseEventArgs | undefined, other: GridMouseEventArgs | undefined) {
777✔
657
        if (item === other) return true;
42!
658

42✔
659
        if (item?.kind === "out-of-bounds") {
42✔
660
            return (
4✔
661
                item?.kind === other?.kind &&
4!
NEW
662
                item?.location[0] === other?.location[0] &&
×
NEW
663
                item?.location[1] === other?.location[1] &&
×
NEW
664
                item?.region[0] === other?.region[0] &&
×
NEW
665
                item?.region[1] === other?.region[1]
×
666
            );
4✔
667
        }
4✔
668

38✔
669
        return (
38✔
670
            item?.kind === other?.kind &&
42✔
671
            item?.location[0] === other?.location[0] &&
3✔
672
            item?.location[1] === other?.location[1]
2✔
673
        );
42✔
674
    }
42✔
675

777✔
676
    const [hoveredItem] = hoveredItemInfo ?? [];
777✔
677

777✔
678
    const enqueueRef = React.useRef<EnqueueCallback>(() => {
777✔
679
        // do nothing
×
680
    });
777✔
681
    const hoverInfoRef = React.useRef(hoveredItemInfo);
777✔
682
    hoverInfoRef.current = hoveredItemInfo;
777✔
683

777✔
684
    const [bufferA, bufferB] = React.useMemo(() => {
777✔
685
        const a = document.createElement("canvas");
145✔
686
        const b = document.createElement("canvas");
145✔
687
        a.style["display"] = "none";
145✔
688
        a.style["opacity"] = "0";
145✔
689
        a.style["position"] = "fixed";
145✔
690
        b.style["display"] = "none";
145✔
691
        b.style["opacity"] = "0";
145✔
692
        b.style["position"] = "fixed";
145✔
693
        return [a, b];
145✔
694
    }, []);
777✔
695

777✔
696
    React.useLayoutEffect(() => {
777✔
697
        document.documentElement.append(bufferA);
145✔
698
        document.documentElement.append(bufferB);
145✔
699
        return () => {
145✔
700
            bufferA.remove();
145✔
701
            bufferB.remove();
145✔
702
        };
145✔
703
    }, [bufferA, bufferB]);
777✔
704

777✔
705
    const renderStateProvider = React.useMemo(() => new RenderStateProvider(), []);
777✔
706

777✔
707
    const minimumCellWidth = experimental?.disableMinimumCellWidth === true ? 1 : 10;
777!
708
    const lastArgsRef = React.useRef<DrawGridArg>();
777✔
709
    const draw = React.useCallback(() => {
777✔
710
        const canvas = ref.current;
638✔
711
        const overlay = overlayRef.current;
638✔
712
        if (canvas === null || overlay === null) return;
638✔
713

572✔
714
        let didOverride = false;
572✔
715
        const overrideCursor = (cursor: React.CSSProperties["cursor"]) => {
572✔
NEW
716
            didOverride = true;
×
NEW
717
            setDrawCursorOverride(cursor);
×
NEW
718
        };
×
719

572✔
720
        const last = lastArgsRef.current;
572✔
721
        const current = {
572✔
722
            canvas,
572✔
723
            bufferA,
572✔
724
            bufferB,
572✔
725
            headerCanvas: overlay,
572✔
726
            width,
572✔
727
            height,
572✔
728
            cellXOffset,
572✔
729
            cellYOffset,
572✔
730
            translateX: Math.round(translateX),
572✔
731
            translateY: Math.round(translateY),
572✔
732
            mappedColumns,
572✔
733
            enableGroups,
572✔
734
            freezeColumns,
572✔
735
            dragAndDropState,
572✔
736
            theme,
572✔
737
            headerHeight,
572✔
738
            groupHeaderHeight,
572✔
739
            disabledRows: disabledRows ?? CompactSelection.empty(),
623✔
740
            rowHeight,
638✔
741
            verticalBorder,
638✔
742
            isResizing,
638✔
743
            resizeCol,
638✔
744
            isFocused,
638✔
745
            selection,
638✔
746
            fillHandle,
638✔
747
            drawCellCallback,
638✔
748
            overrideCursor,
638✔
749
            lastRowSticky: trailingRowType,
638✔
750
            rows,
638✔
751
            drawFocus: drawFocusRing,
638✔
752
            getCellContent,
638✔
753
            getGroupDetails: getGroupDetails ?? (name => ({ name })),
638✔
754
            getRowThemeOverride,
638✔
755
            drawHeaderCallback,
638✔
756
            prelightCells,
638✔
757
            highlightRegions,
638✔
758
            imageLoader,
638✔
759
            lastBlitData,
638✔
760
            damage: damageRegion.current,
638✔
761
            hoverValues: hoverValues.current,
638✔
762
            hoverInfo: hoverInfoRef.current,
638✔
763
            spriteManager,
638✔
764
            scrolling,
638✔
765
            hyperWrapping: experimental?.hyperWrapping ?? false,
638✔
766
            touchMode: lastWasTouch,
638✔
767
            enqueue: enqueueRef.current,
638✔
768
            renderStateProvider,
638✔
769
            renderStrategy: experimental?.renderStrategy ?? (browserIsSafari.value ? "double-buffer" : "single-buffer"),
638!
770
            getCellRenderer,
638✔
771
            minimumCellWidth,
638✔
772
        };
638✔
773

638✔
774
        // This confusing bit of code due to some poor design. Long story short, the damage property is only used
638✔
775
        // with what is effectively the "last args" for the last normal draw anyway. We don't want the drawing code
638✔
776
        // to look at this and go "shit dawg, nothing changed" so we force it to draw frash, but the damage restricts
638✔
777
        // the draw anyway.
638✔
778
        //
638✔
779
        // Dear future Jason, I'm sorry. It was expedient, it worked, and had almost zero perf overhead. THe universe
638✔
780
        // basically made me do it. What choice did I have?
638✔
781
        if (current.damage === undefined) {
638✔
782
            lastArgsRef.current = current;
517✔
783
            drawGrid(current, last);
517✔
784
        } else {
623✔
785
            drawGrid(current, undefined);
55✔
786
        }
55✔
787

572✔
788
        // don't reset on damage events
572✔
789
        if (!didOverride && (current.damage === undefined || current.damage.has(hoverInfoRef?.current?.[0]))) {
638✔
790
            setDrawCursorOverride(undefined);
542✔
791
        }
542✔
792
    }, [
777✔
793
        bufferA,
777✔
794
        bufferB,
777✔
795
        width,
777✔
796
        height,
777✔
797
        cellXOffset,
777✔
798
        cellYOffset,
777✔
799
        translateX,
777✔
800
        translateY,
777✔
801
        mappedColumns,
777✔
802
        enableGroups,
777✔
803
        freezeColumns,
777✔
804
        dragAndDropState,
777✔
805
        theme,
777✔
806
        headerHeight,
777✔
807
        groupHeaderHeight,
777✔
808
        disabledRows,
777✔
809
        rowHeight,
777✔
810
        verticalBorder,
777✔
811
        isResizing,
777✔
812
        resizeCol,
777✔
813
        isFocused,
777✔
814
        selection,
777✔
815
        fillHandle,
777✔
816
        trailingRowType,
777✔
817
        rows,
777✔
818
        drawFocusRing,
777✔
819
        getCellContent,
777✔
820
        getGroupDetails,
777✔
821
        getRowThemeOverride,
777✔
822
        drawCellCallback,
777✔
823
        drawHeaderCallback,
777✔
824
        prelightCells,
777✔
825
        highlightRegions,
777✔
826
        imageLoader,
777✔
827
        spriteManager,
777✔
828
        scrolling,
777✔
829
        experimental?.hyperWrapping,
777✔
830
        experimental?.renderStrategy,
777✔
831
        lastWasTouch,
777✔
832
        renderStateProvider,
777✔
833
        getCellRenderer,
777✔
834
        minimumCellWidth,
777✔
835
    ]);
777✔
836

777✔
837
    const lastDrawRef = React.useRef(draw);
777✔
838
    React.useLayoutEffect(() => {
777✔
839
        draw();
517✔
840
        lastDrawRef.current = draw;
517✔
841
    }, [draw]);
777✔
842

777✔
843
    React.useLayoutEffect(() => {
777✔
844
        const fn = async () => {
145✔
845
            if (document?.fonts?.ready === undefined) return;
145!
846
            await document.fonts.ready;
×
847
            lastArgsRef.current = undefined;
×
848
            lastDrawRef.current();
×
849
        };
145✔
850
        void fn();
145✔
851
    }, []);
777✔
852

777✔
853
    const damageInternal = React.useCallback((locations: CellSet) => {
777✔
854
        damageRegion.current = locations;
31✔
855
        lastDrawRef.current();
31✔
856
        damageRegion.current = undefined;
31✔
857
    }, []);
777✔
858

777✔
859
    const enqueue = useAnimationQueue(damageInternal);
777✔
860
    enqueueRef.current = enqueue;
777✔
861

777✔
862
    const damage = React.useCallback(
777✔
863
        (cells: DamageUpdateList) => {
777✔
864
            damageInternal(new CellSet(cells.map(x => x.cell)));
31✔
865
        },
31✔
866
        [damageInternal]
777✔
867
    );
777✔
868

777✔
869
    imageLoader.setCallback(damageInternal);
777✔
870

777✔
871
    const [overFill, setOverFill] = React.useState(false);
777✔
872

777✔
873
    const [hCol, hRow] = hoveredItem ?? [];
777✔
874
    const headerHovered = hCol !== undefined && hRow === -1;
777✔
875
    const groupHeaderHovered = hCol !== undefined && hRow === -2;
777✔
876
    let clickableInnerCellHovered = false;
777✔
877
    let editableBoolHovered = false;
777✔
878
    let cursorOverride: React.CSSProperties["cursor"] | undefined = drawCursorOverride;
777✔
879
    if (cursorOverride === undefined && hCol !== undefined && hRow !== undefined && hRow > -1) {
777✔
880
        const cell = getCellContent([hCol, hRow], true);
48✔
881
        clickableInnerCellHovered =
48✔
882
            cell.kind === InnerGridCellKind.NewRow ||
48✔
883
            (cell.kind === InnerGridCellKind.Marker && cell.markerKind !== "number");
45✔
884
        editableBoolHovered = cell.kind === GridCellKind.Boolean && booleanCellIsEditable(cell);
48!
885
        cursorOverride = cell.cursor;
48✔
886
    }
48✔
887
    const canDrag = hoveredOnEdge ?? false;
777✔
888
    const cursor = isDragging
777✔
889
        ? "grabbing"
5✔
890
        : canDrag || isResizing
771✔
891
        ? "col-resize"
12✔
892
        : overFill || isFilling
759✔
893
        ? "crosshair"
14✔
894
        : cursorOverride !== undefined
745!
895
        ? cursorOverride
×
896
        : headerHovered || clickableInnerCellHovered || editableBoolHovered || groupHeaderHovered
745✔
897
        ? "pointer"
40✔
898
        : "default";
705✔
899
    const style = React.useMemo(
777✔
900
        () => ({
777✔
901
            // width,
184✔
902
            // height,
184✔
903
            contain: "strict",
184✔
904
            display: "block",
184✔
905
            cursor,
184✔
906
        }),
184✔
907
        [cursor]
777✔
908
    );
777✔
909

777✔
910
    const lastSetCursor = React.useRef<typeof cursor>("default");
777✔
911
    const target = eventTargetRef?.current;
777✔
912
    if (target !== null && target !== undefined && lastSetCursor.current !== style.cursor) {
777✔
913
        // because we have an event target we need to set its cursor instead.
36✔
914
        target.style.cursor = lastSetCursor.current = style.cursor;
36✔
915
    }
36✔
916

776✔
917
    const groupHeaderActionForEvent = React.useCallback(
776✔
918
        (group: string, bounds: Rectangle, localEventX: number, localEventY: number) => {
776✔
919
            if (getGroupDetails === undefined) return undefined;
15!
920
            const groupDesc = getGroupDetails(group);
15✔
921
            if (groupDesc.actions !== undefined) {
15✔
922
                const boxes = getActionBoundsForGroup(bounds, groupDesc.actions);
3✔
923
                for (const [i, box] of boxes.entries()) {
3✔
924
                    if (pointInRect(box, localEventX + bounds.x, localEventY + box.y)) {
3✔
925
                        return groupDesc.actions[i];
3✔
926
                    }
3✔
927
                }
3!
928
            }
✔
929
            return undefined;
12✔
930
        },
15✔
931
        [getGroupDetails]
776✔
932
    );
776✔
933

776✔
934
    const isOverHeaderMenu = React.useCallback(
776✔
935
        (canvas: HTMLCanvasElement, col: number, clientX: number, clientY: number) => {
776✔
936
            const header = columns[col];
268✔
937

268✔
938
            if (!isDragging && !isResizing && header.hasMenu === true && !(hoveredOnEdge ?? false)) {
268✔
939
                const headerBounds = getBoundsForItem(canvas, col, -1);
6✔
940
                assert(headerBounds !== undefined);
6✔
941
                const menuBounds = getHeaderMenuBounds(
6✔
942
                    headerBounds.x,
6✔
943
                    headerBounds.y,
6✔
944
                    headerBounds.width,
6✔
945
                    headerBounds.height,
6✔
946
                    direction(header.title) === "rtl"
6✔
947
                );
6✔
948
                if (
6✔
949
                    clientX > menuBounds.x &&
6✔
950
                    clientX < menuBounds.x + menuBounds.width &&
6✔
951
                    clientY > menuBounds.y &&
6✔
952
                    clientY < menuBounds.y + menuBounds.height
6✔
953
                ) {
6✔
954
                    return headerBounds;
6✔
955
                }
6✔
956
            }
6✔
957
            return undefined;
262✔
958
        },
268✔
959
        [columns, getBoundsForItem, hoveredOnEdge, isDragging, isResizing]
776✔
960
    );
776✔
961

776✔
962
    const downTime = React.useRef(0);
776✔
963
    const downPosition = React.useRef<Item>();
776✔
964
    const mouseDown = React.useRef(false);
776✔
965
    const onMouseDownImpl = React.useCallback(
776✔
966
        (ev: MouseEvent | TouchEvent) => {
776✔
967
            const canvas = ref.current;
135✔
968
            const eventTarget = eventTargetRef?.current;
135✔
969
            if (canvas === null || (ev.target !== canvas && ev.target !== eventTarget)) return;
135!
970
            mouseDown.current = true;
135✔
971

135✔
972
            let clientX: number;
135✔
973
            let clientY: number;
135✔
974
            if (ev instanceof MouseEvent) {
135✔
975
                clientX = ev.clientX;
131✔
976
                clientY = ev.clientY;
131✔
977
            } else {
135✔
978
                clientX = ev.touches[0].clientX;
4✔
979
                clientY = ev.touches[0].clientY;
4✔
980
            }
4✔
981
            if (ev.target === eventTarget && eventTarget !== null) {
135✔
982
                const bounds = eventTarget.getBoundingClientRect();
1✔
983
                if (clientX > bounds.right || clientY > bounds.bottom) return;
1!
984
            }
1✔
985

135✔
986
            const args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
135✔
987
            downPosition.current = args.location;
135✔
988

135✔
989
            if (args.isTouch) {
135✔
990
                downTime.current = Date.now();
4✔
991
            }
4✔
992
            if (lastWasTouchRef.current !== args.isTouch) {
135✔
993
                setLastWasTouch(args.isTouch);
4✔
994
            }
4✔
995

135✔
996
            if (
135✔
997
                args.kind === headerKind &&
135✔
998
                isOverHeaderMenu(canvas, args.location[0], clientX, clientY) !== undefined
20✔
999
            ) {
135✔
1000
                return;
2✔
1001
            } else if (args.kind === groupHeaderKind) {
135✔
1002
                const action = groupHeaderActionForEvent(args.group, args.bounds, args.localEventX, args.localEventY);
5✔
1003
                if (action !== undefined) {
5✔
1004
                    return;
1✔
1005
                }
1✔
1006
            }
5✔
1007

132✔
1008
            onMouseDown?.(args);
132✔
1009
            if (!args.isTouch && isDraggable !== true && isDraggable !== args.kind && args.button < 3) {
135✔
1010
                // preventing default in touch events stops scroll
126✔
1011
                ev.preventDefault();
126✔
1012
            }
126✔
1013
        },
135✔
1014
        [eventTargetRef, isDraggable, getMouseArgsForPosition, groupHeaderActionForEvent, isOverHeaderMenu, onMouseDown]
776✔
1015
    );
776✔
1016
    useEventListener("touchstart", onMouseDownImpl, window, false);
776✔
1017
    useEventListener("mousedown", onMouseDownImpl, window, false);
776✔
1018

776✔
1019
    const onMouseUpImpl = React.useCallback(
776✔
1020
        (ev: MouseEvent | TouchEvent) => {
776✔
1021
            const canvas = ref.current;
133✔
1022
            mouseDown.current = false;
133✔
1023
            if (onMouseUp === undefined || canvas === null) return;
133!
1024
            const eventTarget = eventTargetRef?.current;
133✔
1025

133✔
1026
            const isOutside = ev.target !== canvas && ev.target !== eventTarget;
133!
1027

133✔
1028
            let clientX: number;
133✔
1029
            let clientY: number;
133✔
1030
            let canCancel = true;
133✔
1031
            if (ev instanceof MouseEvent) {
133✔
1032
                clientX = ev.clientX;
129✔
1033
                clientY = ev.clientY;
129✔
1034
                canCancel = ev.button < 3;
129✔
1035
                if ((ev as any).pointerType === "touch") {
129!
1036
                    return;
×
1037
                }
×
1038
            } else {
133✔
1039
                clientX = ev.changedTouches[0].clientX;
4✔
1040
                clientY = ev.changedTouches[0].clientY;
4✔
1041
            }
4✔
1042

133✔
1043
            let args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
133✔
1044

133✔
1045
            if (args.isTouch && downTime.current !== 0 && Date.now() - downTime.current > 500) {
133!
1046
                args = {
×
1047
                    ...args,
×
1048
                    isLongTouch: true,
×
1049
                };
×
1050
            }
×
1051

133✔
1052
            if (lastWasTouchRef.current !== args.isTouch) {
133!
1053
                setLastWasTouch(args.isTouch);
×
1054
            }
×
1055

133✔
1056
            if (!isOutside && ev.cancelable && canCancel) {
133✔
1057
                ev.preventDefault();
132✔
1058
            }
132✔
1059

133✔
1060
            const [col] = args.location;
133✔
1061
            const headerBounds = isOverHeaderMenu(canvas, col, clientX, clientY);
133✔
1062
            if (args.kind === headerKind && headerBounds !== undefined) {
133✔
1063
                if (args.button !== 0 || downPosition.current?.[0] !== col || downPosition.current?.[1] !== -1) {
3✔
1064
                    // force outside so that click will not process
1✔
1065
                    onMouseUp(args, true);
1✔
1066
                }
1✔
1067
                return;
3✔
1068
            } else if (args.kind === groupHeaderKind) {
133✔
1069
                const action = groupHeaderActionForEvent(args.group, args.bounds, args.localEventX, args.localEventY);
5✔
1070
                if (action !== undefined) {
5✔
1071
                    if (args.button === 0) {
1✔
1072
                        action.onClick(args);
1✔
1073
                    }
1✔
1074
                    return;
1✔
1075
                }
1✔
1076
            }
5✔
1077

129✔
1078
            onMouseUp(args, isOutside);
129✔
1079
        },
133✔
1080
        [onMouseUp, eventTargetRef, getMouseArgsForPosition, isOverHeaderMenu, groupHeaderActionForEvent]
776✔
1081
    );
776✔
1082
    useEventListener("mouseup", onMouseUpImpl, window, false);
776✔
1083
    useEventListener("touchend", onMouseUpImpl, window, false);
776✔
1084

776✔
1085
    const onClickImpl = React.useCallback(
776✔
1086
        (ev: MouseEvent | TouchEvent) => {
776✔
1087
            const canvas = ref.current;
115✔
1088
            if (canvas === null) return;
115!
1089
            const eventTarget = eventTargetRef?.current;
115✔
1090

115✔
1091
            const isOutside = ev.target !== canvas && ev.target !== eventTarget;
115✔
1092

115✔
1093
            let clientX: number;
115✔
1094
            let clientY: number;
115✔
1095
            let canCancel = true;
115✔
1096
            if (ev instanceof MouseEvent) {
115✔
1097
                clientX = ev.clientX;
115✔
1098
                clientY = ev.clientY;
115✔
1099
                canCancel = ev.button < 3;
115✔
1100
            } else {
115!
1101
                clientX = ev.changedTouches[0].clientX;
×
1102
                clientY = ev.changedTouches[0].clientY;
×
1103
            }
×
1104

115✔
1105
            const args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
115✔
1106

115✔
1107
            if (lastWasTouchRef.current !== args.isTouch) {
115✔
1108
                setLastWasTouch(args.isTouch);
4✔
1109
            }
4✔
1110

115✔
1111
            if (!isOutside && ev.cancelable && canCancel) {
115✔
1112
                ev.preventDefault();
113✔
1113
            }
113✔
1114

115✔
1115
            const [col] = args.location;
115✔
1116
            const headerBounds = isOverHeaderMenu(canvas, col, clientX, clientY);
115✔
1117
            if (args.kind === headerKind && headerBounds !== undefined) {
115✔
1118
                if (args.button === 0 && downPosition.current?.[0] === col && downPosition.current?.[1] === -1) {
1✔
1119
                    onHeaderMenuClick?.(col, headerBounds);
1✔
1120
                }
1✔
1121
            } else if (args.kind === groupHeaderKind) {
115✔
1122
                const action = groupHeaderActionForEvent(args.group, args.bounds, args.localEventX, args.localEventY);
5✔
1123
                if (action !== undefined && args.button === 0) {
5✔
1124
                    action.onClick(args);
1✔
1125
                }
1✔
1126
            }
5✔
1127
        },
115✔
1128
        [eventTargetRef, getMouseArgsForPosition, isOverHeaderMenu, onHeaderMenuClick, groupHeaderActionForEvent]
776✔
1129
    );
776✔
1130
    useEventListener("click", onClickImpl, window, false);
776✔
1131

776✔
1132
    const onContextMenuImpl = React.useCallback(
776✔
1133
        (ev: MouseEvent) => {
776✔
1134
            const canvas = ref.current;
7✔
1135
            if (canvas === null || onContextMenu === undefined) return;
7!
1136
            const args = getMouseArgsForPosition(canvas, ev.clientX, ev.clientY, ev);
7✔
1137
            onContextMenu(args, () => {
7✔
1138
                if (ev.cancelable) ev.preventDefault();
×
1139
            });
7✔
1140
        },
7✔
1141
        [getMouseArgsForPosition, onContextMenu]
776✔
1142
    );
776✔
1143
    useEventListener("contextmenu", onContextMenuImpl, eventTargetRef?.current ?? null, false);
777✔
1144

777✔
1145
    const onAnimationFrame = React.useCallback<StepCallback>(values => {
777✔
1146
        damageRegion.current = new CellSet(values.map(x => x.item));
90✔
1147
        hoverValues.current = values;
90✔
1148
        lastDrawRef.current();
90✔
1149
        damageRegion.current = undefined;
90✔
1150
    }, []);
777✔
1151

777✔
1152
    const animManagerValue = React.useMemo(() => new AnimationManager(onAnimationFrame), [onAnimationFrame]);
777✔
1153
    const animationManager = React.useRef(animManagerValue);
777✔
1154
    animationManager.current = animManagerValue;
777✔
1155
    React.useLayoutEffect(() => {
777✔
1156
        const am = animationManager.current;
207✔
1157
        if (hoveredItem === undefined || hoveredItem[1] < 0) {
207✔
1158
            am.setHovered(hoveredItem);
188✔
1159
            return;
188✔
1160
        }
188✔
1161
        const cell = getCellContent(hoveredItem as [number, number]);
19✔
1162
        const r = getCellRenderer(cell);
19✔
1163
        am.setHovered(
19✔
1164
            (r === undefined && cell.kind === GridCellKind.Custom) || r?.needsHover === true ? hoveredItem : undefined
207!
1165
        );
207✔
1166
    }, [getCellContent, getCellRenderer, hoveredItem]);
777✔
1167

777✔
1168
    const hoveredRef = React.useRef<GridMouseEventArgs>();
777✔
1169
    const onMouseMoveImpl = React.useCallback(
777✔
1170
        (ev: MouseEvent) => {
777✔
1171
            const canvas = ref.current;
43✔
1172
            if (canvas === null) return;
43!
1173

43✔
1174
            const eventTarget = eventTargetRef?.current;
43✔
1175
            const isIndirect = ev.target !== canvas && ev.target !== eventTarget;
43✔
1176

43✔
1177
            const args = getMouseArgsForPosition(canvas, ev.clientX, ev.clientY, ev);
43✔
1178
            if (args.kind !== "out-of-bounds" && isIndirect && !mouseDown.current && !args.isTouch) {
43✔
1179
                // we are obscured by something else, so we want to not register events if we are not doing anything
1✔
1180
                // important already
1✔
1181
                return;
1✔
1182
            }
1✔
1183

42✔
1184
            // the point here is not to trigger re-renders every time the mouse moves over a cell
42✔
1185
            // that doesn't care about the mouse positon.
42✔
1186
            const maybeSetHoveredInfo = (newVal: typeof hoveredItemInfo, needPosition: boolean) => {
42✔
1187
                setHoveredItemInfo(cv => {
42✔
1188
                    if (cv === newVal) return cv;
42✔
1189
                    if (
38✔
1190
                        cv?.[0][0] === newVal?.[0][0] &&
42✔
1191
                        cv?.[0][1] === newVal?.[0][1] &&
3✔
1192
                        ((cv?.[1][0] === newVal?.[1][0] && cv?.[1][1] === newVal?.[1][1]) || !needPosition)
2!
1193
                    ) {
42!
NEW
1194
                        return cv;
×
NEW
1195
                    }
✔
1196
                    return newVal;
38✔
1197
                });
42✔
1198
            };
42✔
1199

42✔
1200
            if (!isSameItem(args, hoveredRef.current)) {
42✔
1201
                onItemHovered?.(args);
40✔
1202
                maybeSetHoveredInfo(
40✔
1203
                    args.kind === outOfBoundsKind ? undefined : [args.location, [args.localEventX, args.localEventY]],
40✔
1204
                    true
40✔
1205
                );
40✔
1206
                hoveredRef.current = args;
40✔
1207
            } else if (args.kind === "cell" || args.kind === headerKind || args.kind === groupHeaderKind) {
43!
1208
                let needsDamageCell = false;
2✔
1209
                let needsHoverPosition = true;
2✔
1210

2✔
1211
                if (args.kind === "cell") {
2!
1212
                    const toCheck = getCellContent(args.location);
×
NEW
1213
                    const rendererNeeds = getCellRenderer(toCheck)?.needsHoverPosition;
×
NEW
1214
                    // custom cells we will assume need the position if they don't explicitly say they don't, everything
×
NEW
1215
                    // else we will assume doesn't need it.
×
NEW
1216
                    needsHoverPosition = rendererNeeds ?? toCheck.kind === GridCellKind.Custom;
×
NEW
1217
                    needsDamageCell = needsHoverPosition;
×
1218
                } else if (args.kind === groupHeaderKind) {
2!
NEW
1219
                    needsDamageCell = true;
×
NEW
1220
                }
×
1221

2✔
1222
                const newInfo: typeof hoverInfoRef.current = [args.location, [args.localEventX, args.localEventY]];
2✔
1223
                maybeSetHoveredInfo(newInfo, needsHoverPosition);
2✔
1224
                hoverInfoRef.current = newInfo;
2✔
1225
                if (needsDamageCell) {
2!
NEW
1226
                    damageInternal(new CellSet([args.location]));
×
1227
                }
×
1228
            }
2✔
1229

42✔
1230
            const notRowMarkerCol = args.location[0] >= (firstColAccessible ? 0 : 1);
43✔
1231
            setHoveredOnEdge(args.kind === headerKind && args.isEdge && notRowMarkerCol && allowResize === true);
43✔
1232

43✔
1233
            if (fillHandle && selection.current !== undefined) {
43✔
1234
                const targetItem = [
5✔
1235
                    selection.current.range.x + selection.current.range.width - 1,
5✔
1236
                    selection.current.range.y + selection.current.range.height - 1,
5✔
1237
                ];
5✔
1238
                const [col, row] = targetItem;
5✔
1239
                const sb = getBoundsForItem(canvas, col, row);
5✔
1240
                const x = ev.clientX;
5✔
1241
                const y = ev.clientY;
5✔
1242
                assert(sb !== undefined);
5✔
1243
                setOverFill(
5✔
1244
                    x >= sb.x + sb.width - fillHandleClickSize &&
5✔
1245
                        x <= sb.x + sb.width &&
5✔
1246
                        y >= sb.y + sb.height - fillHandleClickSize &&
4✔
1247
                        y <= sb.y + sb.height
4✔
1248
                );
5✔
1249
            } else {
43✔
1250
                setOverFill(false);
37✔
1251
            }
37✔
1252

42✔
1253
            onMouseMoveRaw?.(ev);
42✔
1254
            onMouseMove(args);
43✔
1255
        },
43✔
1256
        [
777✔
1257
            eventTargetRef,
777✔
1258
            getMouseArgsForPosition,
777✔
1259
            firstColAccessible,
777✔
1260
            allowResize,
777✔
1261
            fillHandle,
777✔
1262
            selection,
777✔
1263
            onMouseMoveRaw,
777✔
1264
            onMouseMove,
777✔
1265
            onItemHovered,
777✔
1266
            getCellContent,
777✔
1267
            getCellRenderer,
777✔
1268
            damageInternal,
777✔
1269
            getBoundsForItem,
777✔
1270
        ]
777✔
1271
    );
777✔
1272
    useEventListener("mousemove", onMouseMoveImpl, window, true);
777✔
1273

777✔
1274
    const onKeyDownImpl = React.useCallback(
777✔
1275
        (event: React.KeyboardEvent<HTMLCanvasElement>) => {
777✔
1276
            const canvas = ref.current;
57✔
1277
            if (canvas === null) return;
57!
1278

57✔
1279
            let bounds: Rectangle | undefined;
57✔
1280
            let location: Item | undefined = undefined;
57✔
1281
            if (selection.current !== undefined) {
57✔
1282
                bounds = getBoundsForItem(canvas, selection.current.cell[0], selection.current.cell[1]);
54✔
1283
                location = selection.current.cell;
54✔
1284
            }
54✔
1285

57✔
1286
            onKeyDown?.({
57✔
1287
                bounds,
57✔
1288
                stopPropagation: () => event.stopPropagation(),
57✔
1289
                preventDefault: () => event.preventDefault(),
57✔
1290
                cancel: () => undefined,
57✔
1291
                ctrlKey: event.ctrlKey,
57✔
1292
                metaKey: event.metaKey,
57✔
1293
                shiftKey: event.shiftKey,
57✔
1294
                altKey: event.altKey,
57✔
1295
                key: event.key,
57✔
1296
                keyCode: event.keyCode,
57✔
1297
                rawEvent: event,
57✔
1298
                location,
57✔
1299
            });
57✔
1300
        },
57✔
1301
        [onKeyDown, selection, getBoundsForItem]
777✔
1302
    );
777✔
1303

777✔
1304
    const onKeyUpImpl = React.useCallback(
777✔
1305
        (event: React.KeyboardEvent<HTMLCanvasElement>) => {
777✔
1306
            const canvas = ref.current;
7✔
1307
            if (canvas === null) return;
7!
1308

7✔
1309
            let bounds: Rectangle | undefined;
7✔
1310
            let location: Item | undefined = undefined;
7✔
1311
            if (selection.current !== undefined) {
7✔
1312
                bounds = getBoundsForItem(canvas, selection.current.cell[0], selection.current.cell[1]);
7✔
1313
                location = selection.current.cell;
7✔
1314
            }
7✔
1315

7✔
1316
            onKeyUp?.({
7✔
1317
                bounds,
1✔
1318
                stopPropagation: () => event.stopPropagation(),
1✔
1319
                preventDefault: () => event.preventDefault(),
1✔
1320
                cancel: () => undefined,
1✔
1321
                ctrlKey: event.ctrlKey,
1✔
1322
                metaKey: event.metaKey,
1✔
1323
                shiftKey: event.shiftKey,
1✔
1324
                altKey: event.altKey,
1✔
1325
                key: event.key,
1✔
1326
                keyCode: event.keyCode,
1✔
1327
                rawEvent: event,
1✔
1328
                location,
1✔
1329
            });
1✔
1330
        },
7✔
1331
        [onKeyUp, selection, getBoundsForItem]
777✔
1332
    );
777✔
1333

777✔
1334
    const refImpl = React.useCallback(
777✔
1335
        (instance: HTMLCanvasElement | null) => {
777✔
1336
            ref.current = instance;
290✔
1337
            if (canvasRef !== undefined) {
290✔
1338
                canvasRef.current = instance;
266✔
1339
            }
266✔
1340
        },
290✔
1341
        [canvasRef]
777✔
1342
    );
777✔
1343

777✔
1344
    const onDragStartImpl = React.useCallback(
777✔
1345
        (event: DragEvent) => {
777✔
1346
            const canvas = ref.current;
1✔
1347
            if (canvas === null || isDraggable === false || isResizing) {
1!
1348
                event.preventDefault();
×
1349
                return;
×
1350
            }
×
1351

1✔
1352
            let dragMime: string | undefined;
1✔
1353
            let dragData: string | undefined;
1✔
1354

1✔
1355
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
1✔
1356

1✔
1357
            if (isDraggable !== true && args.kind !== isDraggable) {
1!
1358
                event.preventDefault();
×
1359
                return;
×
1360
            }
×
1361

1✔
1362
            const setData = (mime: string, payload: string) => {
1✔
1363
                dragMime = mime;
1✔
1364
                dragData = payload;
1✔
1365
            };
1✔
1366

1✔
1367
            let dragImage: Element | undefined;
1✔
1368
            let dragImageX: number | undefined;
1✔
1369
            let dragImageY: number | undefined;
1✔
1370
            const setDragImage = (image: Element, x: number, y: number) => {
1✔
1371
                dragImage = image;
×
1372
                dragImageX = x;
×
1373
                dragImageY = y;
×
1374
            };
×
1375

1✔
1376
            let prevented = false;
1✔
1377

1✔
1378
            onDragStart?.({
1✔
1379
                ...args,
1✔
1380
                setData,
1✔
1381
                setDragImage,
1✔
1382
                preventDefault: () => (prevented = true),
1✔
1383
                defaultPrevented: () => prevented,
1✔
1384
            });
1✔
1385
            if (!prevented && dragMime !== undefined && dragData !== undefined && event.dataTransfer !== null) {
1✔
1386
                event.dataTransfer.setData(dragMime, dragData);
1✔
1387
                event.dataTransfer.effectAllowed = "copyLink";
1✔
1388

1✔
1389
                if (dragImage !== undefined && dragImageX !== undefined && dragImageY !== undefined) {
1!
1390
                    event.dataTransfer.setDragImage(dragImage, dragImageX, dragImageY);
×
1391
                } else {
1✔
1392
                    const [col, row] = args.location;
1✔
1393
                    if (row !== undefined) {
1✔
1394
                        const offscreen = document.createElement("canvas");
1✔
1395
                        const boundsForDragTarget = getBoundsForItem(canvas, col, row);
1✔
1396

1✔
1397
                        assert(boundsForDragTarget !== undefined);
1✔
1398
                        const dpr = Math.ceil(window.devicePixelRatio ?? 1);
1!
1399
                        offscreen.width = boundsForDragTarget.width * dpr;
1✔
1400
                        offscreen.height = boundsForDragTarget.height * dpr;
1✔
1401

1✔
1402
                        const ctx = offscreen.getContext("2d");
1✔
1403
                        if (ctx !== null) {
1✔
1404
                            ctx.scale(dpr, dpr);
1✔
1405
                            ctx.textBaseline = "middle";
1✔
1406
                            if (row === -1) {
1!
NEW
1407
                                ctx.font = theme.headerFontFull;
×
1408
                                ctx.fillStyle = theme.bgHeader;
×
1409
                                ctx.fillRect(0, 0, offscreen.width, offscreen.height);
×
1410
                                drawHeader(
×
1411
                                    ctx,
×
1412
                                    0,
×
1413
                                    0,
×
1414
                                    boundsForDragTarget.width,
×
1415
                                    boundsForDragTarget.height,
×
1416
                                    mappedColumns[col],
×
1417
                                    false,
×
1418
                                    theme,
×
1419
                                    false,
×
1420
                                    false,
×
1421
                                    0,
×
1422
                                    spriteManager,
×
1423
                                    drawHeaderCallback,
×
1424
                                    false
×
1425
                                );
×
1426
                            } else {
1✔
1427
                                ctx.font = theme.baseFontFull;
1✔
1428
                                ctx.fillStyle = theme.bgCell;
1✔
1429
                                ctx.fillRect(0, 0, offscreen.width, offscreen.height);
1✔
1430
                                drawCell(
1✔
1431
                                    ctx,
1✔
1432
                                    row,
1✔
1433
                                    getCellContent([col, row]),
1✔
1434
                                    0,
1✔
1435
                                    0,
1✔
1436
                                    0,
1✔
1437
                                    boundsForDragTarget.width,
1✔
1438
                                    boundsForDragTarget.height,
1✔
1439
                                    false,
1✔
1440
                                    theme,
1✔
1441
                                    imageLoader,
1✔
1442
                                    spriteManager,
1✔
1443
                                    1,
1✔
1444
                                    undefined,
1✔
1445
                                    false,
1✔
1446
                                    0,
1✔
1447
                                    undefined,
1✔
1448
                                    undefined,
1✔
1449
                                    undefined,
1✔
1450
                                    renderStateProvider,
1✔
1451
                                    getCellRenderer,
1✔
1452
                                    () => undefined
1✔
1453
                                );
1✔
1454
                            }
1✔
1455
                        }
1✔
1456

1✔
1457
                        offscreen.style.left = "-100%";
1✔
1458
                        offscreen.style.position = "absolute";
1✔
1459
                        offscreen.style.width = `${boundsForDragTarget.width}px`;
1✔
1460
                        offscreen.style.height = `${boundsForDragTarget.height}px`;
1✔
1461

1✔
1462
                        document.body.append(offscreen);
1✔
1463

1✔
1464
                        event.dataTransfer.setDragImage(
1✔
1465
                            offscreen,
1✔
1466
                            boundsForDragTarget.width / 2,
1✔
1467
                            boundsForDragTarget.height / 2
1✔
1468
                        );
1✔
1469

1✔
1470
                        window.setTimeout(() => {
1✔
1471
                            offscreen.remove();
1✔
1472
                        }, 0);
1✔
1473
                    }
1✔
1474
                }
1✔
1475
            } else {
1!
1476
                event.preventDefault();
×
1477
            }
×
1478
        },
1✔
1479
        [
777✔
1480
            isDraggable,
777✔
1481
            isResizing,
777✔
1482
            getMouseArgsForPosition,
777✔
1483
            onDragStart,
777✔
1484
            getBoundsForItem,
777✔
1485
            theme,
777✔
1486
            mappedColumns,
777✔
1487
            spriteManager,
777✔
1488
            drawHeaderCallback,
777✔
1489
            getCellContent,
777✔
1490
            imageLoader,
777✔
1491
            renderStateProvider,
777✔
1492
            getCellRenderer,
777✔
1493
        ]
777✔
1494
    );
777✔
1495
    useEventListener("dragstart", onDragStartImpl, eventTargetRef?.current ?? null, false, false);
777✔
1496

777✔
1497
    const activeDropTarget = React.useRef<Item | undefined>();
777✔
1498

777✔
1499
    const onDragOverImpl = React.useCallback(
777✔
1500
        (event: DragEvent) => {
777✔
1501
            const canvas = ref.current;
×
1502
            if (onDrop !== undefined) {
×
1503
                // Need to preventDefault to allow drop
×
1504
                event.preventDefault();
×
1505
            }
×
1506

×
1507
            if (canvas === null || onDragOverCell === undefined) {
×
1508
                return;
×
1509
            }
×
1510

×
1511
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
×
1512

×
1513
            const [rawCol, row] = args.location;
×
1514
            const col = rawCol - (firstColAccessible ? 0 : 1);
×
1515
            const [activeCol, activeRow] = activeDropTarget.current ?? [];
×
1516

×
1517
            if (activeCol !== col || activeRow !== row) {
×
1518
                activeDropTarget.current = [col, row];
×
1519
                onDragOverCell([col, row], event.dataTransfer);
×
1520
            }
×
1521
        },
×
1522
        [firstColAccessible, getMouseArgsForPosition, onDragOverCell, onDrop]
777✔
1523
    );
777✔
1524
    useEventListener("dragover", onDragOverImpl, eventTargetRef?.current ?? null, false, false);
777✔
1525

777✔
1526
    const onDragEndImpl = React.useCallback(() => {
777✔
1527
        activeDropTarget.current = undefined;
×
1528
        onDragEnd?.();
×
1529
    }, [onDragEnd]);
777✔
1530
    useEventListener("dragend", onDragEndImpl, eventTargetRef?.current ?? null, false, false);
777✔
1531

777✔
1532
    const onDropImpl = React.useCallback(
777✔
1533
        (event: DragEvent) => {
777✔
1534
            const canvas = ref.current;
×
1535
            if (canvas === null || onDrop === undefined) {
×
1536
                return;
×
1537
            }
×
1538

×
1539
            // Default can mess up sometimes.
×
1540
            event.preventDefault();
×
1541

×
1542
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
×
1543

×
1544
            const [rawCol, row] = args.location;
×
1545
            const col = rawCol - (firstColAccessible ? 0 : 1);
×
1546

×
1547
            onDrop([col, row], event.dataTransfer);
×
1548
        },
×
1549
        [firstColAccessible, getMouseArgsForPosition, onDrop]
777✔
1550
    );
777✔
1551
    useEventListener("drop", onDropImpl, eventTargetRef?.current ?? null, false, false);
777✔
1552

777✔
1553
    const onDragLeaveImpl = React.useCallback(() => {
777✔
1554
        onDragLeave?.();
×
1555
    }, [onDragLeave]);
777✔
1556
    useEventListener("dragleave", onDragLeaveImpl, eventTargetRef?.current ?? null, false, false);
777✔
1557

777✔
1558
    const selectionRef = React.useRef(selection);
777✔
1559
    selectionRef.current = selection;
777✔
1560
    const focusRef = React.useRef<HTMLElement | null>(null);
777✔
1561
    const focusElement = React.useCallback(
777✔
1562
        (el: HTMLElement | null) => {
777✔
1563
            // We don't want to steal the focus if we don't currently own the focus.
59✔
1564
            if (ref.current === null || !ref.current.contains(document.activeElement)) return;
59✔
1565
            if (el === null && selectionRef.current.current !== undefined) {
59✔
1566
                canvasRef?.current?.focus({
7✔
1567
                    preventScroll: true,
7✔
1568
                });
7✔
1569
            } else if (el !== null) {
59✔
1570
                el.focus({
23✔
1571
                    preventScroll: true,
23✔
1572
                });
23✔
1573
            }
23✔
1574
            focusRef.current = el;
30✔
1575
        },
59✔
1576
        [canvasRef]
777✔
1577
    );
777✔
1578

777✔
1579
    React.useImperativeHandle(
777✔
1580
        forwardedRef,
777✔
1581
        () => ({
777✔
1582
            focus: () => {
287✔
1583
                const el = focusRef.current;
35✔
1584
                // The element in the ref may have been removed however our callback method ref
35✔
1585
                // won't see the removal so bad things happen. Checking to see if the element is
35✔
1586
                // no longer attached is enough to resolve the problem. In the future this
35✔
1587
                // should be replaced with something much more robust.
35✔
1588
                if (el === null || !document.contains(el)) {
35✔
1589
                    canvasRef?.current?.focus({
29✔
1590
                        preventScroll: true,
29✔
1591
                    });
29✔
1592
                } else {
35✔
1593
                    el.focus({
6✔
1594
                        preventScroll: true,
6✔
1595
                    });
6✔
1596
                }
6✔
1597
            },
35✔
1598
            getBounds: (col?: number, row?: number) => {
287✔
1599
                if (canvasRef === undefined || canvasRef.current === null) {
46!
1600
                    return undefined;
×
1601
                }
×
1602

46✔
1603
                return getBoundsForItem(canvasRef.current, col ?? 0, row ?? -1);
46!
1604
            },
46✔
1605
            damage,
287✔
1606
        }),
287✔
1607
        [canvasRef, damage, getBoundsForItem]
777✔
1608
    );
777✔
1609

777✔
1610
    const lastFocusedSubdomNode = React.useRef<Item>();
777✔
1611

777✔
1612
    const accessibilityTree = useDebouncedMemo(
777✔
1613
        () => {
777✔
1614
            if (width < 50 || experimental?.disableAccessibilityTree === true) return null;
347✔
1615
            let effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX);
81✔
1616
            const colOffset = firstColAccessible ? 0 : -1;
347✔
1617
            if (!firstColAccessible && effectiveCols[0]?.sourceIndex === 0) {
347✔
1618
                effectiveCols = effectiveCols.slice(1);
6✔
1619
            }
6✔
1620

81✔
1621
            const [fCol, fRow] = selection.current?.cell ?? [];
347✔
1622
            const range = selection.current?.range;
347✔
1623

347✔
1624
            const visibleCols = effectiveCols.map(c => c.sourceIndex);
347✔
1625
            const visibleRows = makeRange(cellYOffset, Math.min(rows, cellYOffset + accessibilityHeight));
347✔
1626

347✔
1627
            // Maintain focus within grid if we own it but focused cell is outside visible viewport
347✔
1628
            // and not rendered.
347✔
1629
            if (
347✔
1630
                fCol !== undefined &&
347✔
1631
                fRow !== undefined &&
36✔
1632
                !(visibleCols.includes(fCol) && visibleRows.includes(fRow))
36✔
1633
            ) {
347✔
1634
                focusElement(null);
1✔
1635
            }
1✔
1636

81✔
1637
            return (
81✔
1638
                <table
81✔
1639
                    key="access-tree"
81✔
1640
                    role="grid"
81✔
1641
                    aria-rowcount={rows + 1}
81✔
1642
                    aria-multiselectable="true"
81✔
1643
                    aria-colcount={mappedColumns.length + colOffset}>
81✔
1644
                    <thead role="rowgroup">
81✔
1645
                        <tr role="row" aria-rowindex={1}>
81✔
1646
                            {effectiveCols.map(c => (
81✔
1647
                                <th
748✔
1648
                                    role="columnheader"
748✔
1649
                                    aria-selected={selection.columns.hasIndex(c.sourceIndex)}
748✔
1650
                                    aria-colindex={c.sourceIndex + 1 + colOffset}
748✔
1651
                                    tabIndex={-1}
748✔
1652
                                    onFocus={e => {
748✔
1653
                                        if (e.target === focusRef.current) return;
×
1654
                                        return onCellFocused?.([c.sourceIndex, -1]);
×
1655
                                    }}
×
1656
                                    key={c.sourceIndex}>
748✔
1657
                                    {c.title}
748✔
1658
                                </th>
748✔
1659
                            ))}
81✔
1660
                        </tr>
81✔
1661
                    </thead>
81✔
1662
                    <tbody role="rowgroup">
81✔
1663
                        {visibleRows.map(row => (
81✔
1664
                            <tr
2,678✔
1665
                                role="row"
2,678✔
1666
                                aria-selected={selection.rows.hasIndex(row)}
2,678✔
1667
                                key={row}
2,678✔
1668
                                aria-rowindex={row + 2}>
2,678✔
1669
                                {effectiveCols.map(c => {
2,678✔
1670
                                    const col = c.sourceIndex;
23,680✔
1671
                                    const key = `${col},${row}`;
23,680✔
1672
                                    const focused = fCol === col && fRow === row;
23,680✔
1673
                                    const selected =
23,680✔
1674
                                        range !== undefined &&
23,680✔
1675
                                        col >= range.x &&
11,020✔
1676
                                        col < range.x + range.width &&
9,566✔
1677
                                        row >= range.y &&
1,166✔
1678
                                        row < range.y + range.height;
1,012✔
1679
                                    const id = `glide-cell-${col}-${row}`;
23,680✔
1680
                                    const location: Item = [col, row];
23,680✔
1681
                                    const cellContent = getCellContent(location, true);
23,680✔
1682
                                    return (
23,680✔
1683
                                        <td
23,680✔
1684
                                            key={key}
23,680✔
1685
                                            role="gridcell"
23,680✔
1686
                                            aria-colindex={col + 1 + colOffset}
23,680✔
1687
                                            aria-selected={selected}
23,680✔
1688
                                            aria-readonly={
23,680✔
1689
                                                isInnerOnlyCell(cellContent) || !isReadWriteCell(cellContent)
23,680✔
1690
                                            }
23,680✔
1691
                                            id={id}
23,680✔
1692
                                            data-testid={id}
23,680✔
1693
                                            onClick={() => {
23,680✔
1694
                                                const canvas = canvasRef?.current;
1✔
1695
                                                if (canvas === null || canvas === undefined) return;
1!
1696
                                                return onKeyDown?.({
1✔
1697
                                                    bounds: getBoundsForItem(canvas, col, row),
1✔
1698
                                                    cancel: () => undefined,
1✔
1699
                                                    preventDefault: () => undefined,
1✔
1700
                                                    stopPropagation: () => undefined,
1✔
1701
                                                    ctrlKey: false,
1✔
1702
                                                    key: "Enter",
1✔
1703
                                                    keyCode: 13,
1✔
1704
                                                    metaKey: false,
1✔
1705
                                                    shiftKey: false,
1✔
1706
                                                    altKey: false,
1✔
1707
                                                    rawEvent: undefined,
1✔
1708
                                                    location,
1✔
1709
                                                });
1✔
1710
                                            }}
1✔
1711
                                            onFocusCapture={e => {
23,680✔
1712
                                                if (
27✔
1713
                                                    e.target === focusRef.current ||
27✔
1714
                                                    (lastFocusedSubdomNode.current?.[0] === col &&
24✔
1715
                                                        lastFocusedSubdomNode.current?.[1] === row)
4✔
1716
                                                )
27✔
1717
                                                    return;
27✔
1718
                                                lastFocusedSubdomNode.current = location;
24✔
1719
                                                return onCellFocused?.(location);
24✔
1720
                                            }}
27✔
1721
                                            ref={focused ? focusElement : undefined}
23,680✔
1722
                                            tabIndex={-1}>
23,680✔
1723
                                            {getRowData(cellContent, getCellRenderer)}
23,680✔
1724
                                        </td>
23,680✔
1725
                                    );
23,680✔
1726
                                })}
2,678✔
1727
                            </tr>
2,678✔
1728
                        ))}
81✔
1729
                    </tbody>
81✔
1730
                </table>
81✔
1731
            );
347✔
1732
        },
347✔
1733
        [
777✔
1734
            width,
777✔
1735
            mappedColumns,
777✔
1736
            cellXOffset,
777✔
1737
            dragAndDropState,
777✔
1738
            translateX,
777✔
1739
            rows,
777✔
1740
            cellYOffset,
777✔
1741
            accessibilityHeight,
777✔
1742
            selection,
777✔
1743
            focusElement,
777✔
1744
            getCellContent,
777✔
1745
            canvasRef,
777✔
1746
            onKeyDown,
777✔
1747
            getBoundsForItem,
777✔
1748
            onCellFocused,
777✔
1749
        ],
777✔
1750
        200
777✔
1751
    );
777✔
1752

777✔
1753
    const stickyX = fixedShadowX ? getStickyWidth(mappedColumns, dragAndDropState) : 0;
777!
1754
    const opacityX =
777✔
1755
        freezeColumns === 0 || !fixedShadowX ? 0 : cellXOffset > freezeColumns ? 1 : clamp(-translateX / 100, 0, 1);
777✔
1756

777✔
1757
    const absoluteOffsetY = -cellYOffset * 32 + translateY;
777✔
1758
    const opacityY = !fixedShadowY ? 0 : clamp(-absoluteOffsetY / 100, 0, 1);
777!
1759

777✔
1760
    const stickyShadow = React.useMemo(() => {
777✔
1761
        if (!opacityX && !opacityY) {
280✔
1762
            return null;
277✔
1763
        }
277✔
1764

3✔
1765
        const styleX: React.CSSProperties = {
3✔
1766
            position: "absolute",
3✔
1767
            top: 0,
3✔
1768
            left: stickyX,
3✔
1769
            width: width - stickyX,
3✔
1770
            height: height,
3✔
1771
            opacity: opacityX,
3✔
1772
            pointerEvents: "none",
3✔
1773
            transition: !smoothScrollX ? "opacity 0.2s" : undefined,
280!
1774
            boxShadow: "inset 13px 0 10px -13px rgba(0, 0, 0, 0.2)",
280✔
1775
        };
280✔
1776

280✔
1777
        const styleY: React.CSSProperties = {
280✔
1778
            position: "absolute",
280✔
1779
            top: totalHeaderHeight,
280✔
1780
            left: 0,
280✔
1781
            width: width,
280✔
1782
            height: height,
280✔
1783
            opacity: opacityY,
280✔
1784
            pointerEvents: "none",
280✔
1785
            transition: !smoothScrollY ? "opacity 0.2s" : undefined,
280!
1786
            boxShadow: "inset 0 13px 10px -13px rgba(0, 0, 0, 0.2)",
280✔
1787
        };
280✔
1788

280✔
1789
        return (
280✔
1790
            <>
280✔
1791
                {opacityX > 0 && <div id="shadow-x" style={styleX} />}
280✔
1792
                {opacityY > 0 && <div id="shadow-y" style={styleY} />}
280✔
1793
            </>
280✔
1794
        );
280✔
1795
    }, [opacityX, opacityY, stickyX, width, smoothScrollX, totalHeaderHeight, height, smoothScrollY]);
777✔
1796

777✔
1797
    const overlayStyle = React.useMemo<React.CSSProperties>(
777✔
1798
        () => ({
777✔
1799
            position: "absolute",
145✔
1800
            top: 0,
145✔
1801
            left: 0,
145✔
1802
        }),
145✔
1803
        []
777✔
1804
    );
777✔
1805

777✔
1806
    return (
777✔
1807
        <>
777✔
1808
            <canvas
777✔
1809
                data-testid="data-grid-canvas"
777✔
1810
                tabIndex={0}
777✔
1811
                onKeyDown={onKeyDownImpl}
777✔
1812
                onKeyUp={onKeyUpImpl}
777✔
1813
                onFocus={onCanvasFocused}
777✔
1814
                onBlur={onCanvasBlur}
777✔
1815
                ref={refImpl}
777✔
1816
                style={style}>
777✔
1817
                {accessibilityTree}
777✔
1818
            </canvas>
777✔
1819
            <canvas ref={overlayRef} style={overlayStyle} />
777✔
1820
            {stickyShadow}
777✔
1821
        </>
777✔
1822
    );
777✔
1823
};
777✔
1824

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