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

glideapps / glide-data-grid / 7312551980

24 Dec 2023 04:45AM UTC coverage: 90.577% (+4.2%) from 86.42%
7312551980

Pull #810

github

web-flow
Allow control of cell activation behavior (#829)

* Allow control of cell activation behavior

* Add unit tests

* Cleanup

* Fix double click with touch

* Improve comment
Pull Request #810: 6.0.0

2590 of 3242 branches covered (0.0%)

3348 of 3816 new or added lines in 62 files covered. (87.74%)

265 existing lines in 12 files now uncovered.

15745 of 17383 relevant lines covered (90.58%)

3077.86 hits per line

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

91.91
/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
    rectBottomRight,
1✔
10
    useMappedColumns,
1✔
11
} from "./data-grid-lib.js";
1✔
12
import {
1✔
13
    GridCellKind,
1✔
14
    type Rectangle,
1✔
15
    type GridSelection,
1✔
16
    type InnerGridCell,
1✔
17
    InnerGridCellKind,
1✔
18
    CompactSelection,
1✔
19
    type Item,
1✔
20
    type DrawHeaderCallback,
1✔
21
    isReadWriteCell,
1✔
22
    isInnerOnlyCell,
1✔
23
    booleanCellIsEditable,
1✔
24
    type InnerGridColumn,
1✔
25
    type TrailingRowType,
1✔
26
    type DrawCellCallback,
1✔
27
} from "./data-grid-types.js";
1✔
28
import { CellSet } from "./cell-set.js";
1✔
29
import { SpriteManager, type SpriteMap } from "./data-grid-sprites.js";
1✔
30
import { direction, getScrollBarWidth, useDebouncedMemo, useEventListener } from "../../common/utils.js";
1✔
31
import clamp from "lodash/clamp.js";
1✔
32
import makeRange from "lodash/range.js";
1✔
33
import {
1✔
34
    type BlitData,
1✔
35
    drawCell,
1✔
36
    drawGrid,
1✔
37
    drawHeader,
1✔
38
    getActionBoundsForGroup,
1✔
39
    getHeaderMenuBounds,
1✔
40
    type GetRowThemeCallback,
1✔
41
    type GroupDetailsCallback,
1✔
42
    type Highlight,
1✔
43
    pointInRect,
1✔
44
} from "./data-grid-render.js";
1✔
45
import { AnimationManager, type StepCallback } from "./animation-manager.js";
1✔
46
import { RenderStateProvider } from "../../common/render-state-provider.js";
1✔
47
import { browserIsFirefox, browserIsSafari } from "../../common/browser-detect.js";
1✔
48
import { type EnqueueCallback, useAnimationQueue } from "./use-animation-queue.js";
1✔
49
import { assert } from "../../common/support.js";
1✔
50
import type { CellRenderer, GetCellRendererCallback } from "../../cells/cell-types.js";
1✔
51
import type { DrawGridArg } from "./draw-grid-arg.js";
1✔
52
import type { ImageWindowLoader } from "./image-window-loader-interface.js";
1✔
53
import {
1✔
54
    type GridMouseEventArgs,
1✔
55
    type GridKeyEventArgs,
1✔
56
    type GridDragEventArgs,
1✔
57
    OutOfBoundsRegionAxis,
1✔
58
    outOfBoundsKind,
1✔
59
    groupHeaderKind,
1✔
60
    headerKind,
1✔
61
    mouseEventArgsAreEqual,
1✔
62
} from "./event-args.js";
1✔
63

1✔
64
export interface DataGridProps {
1✔
65
    readonly width: number;
1✔
66
    readonly height: number;
1✔
67

1✔
68
    readonly cellXOffset: number;
1✔
69
    readonly cellYOffset: number;
1✔
70

1✔
71
    readonly translateX: number | undefined;
1✔
72
    readonly translateY: number | undefined;
1✔
73

1✔
74
    readonly accessibilityHeight: number;
1✔
75

1✔
76
    readonly freezeColumns: number;
1✔
77
    readonly trailingRowType: TrailingRowType;
1✔
78
    readonly firstColAccessible: boolean;
1✔
79

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

1✔
91
    readonly allowResize: boolean | undefined;
1✔
92
    readonly isResizing: boolean;
1✔
93
    readonly resizeColumn: number | undefined;
1✔
94
    readonly isDragging: boolean;
1✔
95
    readonly isFilling: boolean;
1✔
96
    readonly isFocused: boolean;
1✔
97

1✔
98
    readonly columns: readonly InnerGridColumn[];
1✔
99
    /**
1✔
100
     * The number of rows in the grid.
1✔
101
     * @group Data
1✔
102
     */
1✔
103
    readonly rows: number;
1✔
104

1✔
105
    readonly headerHeight: number;
1✔
106
    readonly groupHeaderHeight: number;
1✔
107
    readonly enableGroups: boolean;
1✔
108
    readonly rowHeight: number | ((index: number) => number);
1✔
109

1✔
110
    readonly canvasRef: React.MutableRefObject<HTMLCanvasElement | null> | undefined;
1✔
111

1✔
112
    readonly eventTargetRef: React.MutableRefObject<HTMLDivElement | null> | undefined;
1✔
113

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

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

1✔
139
    /**
1✔
140
     * Enabled/disables the fill handle.
1✔
141
     * @defaultValue false
1✔
142
     * @group Editing
1✔
143
     */
1✔
144
    readonly fillHandle: boolean | undefined;
1✔
145

1✔
146
    readonly disabledRows: CompactSelection | undefined;
1✔
147
    /**
1✔
148
     * Allows passing a custom image window loader.
1✔
149
     * @group Advanced
1✔
150
     */
1✔
151
    readonly imageWindowLoader: ImageWindowLoader;
1✔
152

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

1✔
163
    readonly onCanvasFocused: () => void;
1✔
164
    readonly onCanvasBlur: () => void;
1✔
165
    readonly onCellFocused: (args: Item) => void;
1✔
166

1✔
167
    readonly onMouseMoveRaw: (event: MouseEvent) => void;
1✔
168

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

1✔
180
    readonly verticalBorder: (col: number) => boolean;
1✔
181

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

1✔
196
    /** @group Drag and Drop */
1✔
197
    readonly onDragOverCell: ((cell: Item, dataTransfer: DataTransfer | null) => void) | undefined;
1✔
198
    /** @group Drag and Drop */
1✔
199
    readonly onDragLeave: (() => void) | undefined;
1✔
200

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

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

1✔
219
    readonly drawCell: DrawCellCallback | undefined;
1✔
220

1✔
221
    /**
1✔
222
     * Controls the drawing of the focus ring.
1✔
223
     * @defaultValue true
1✔
224
     * @group Style
1✔
225
     */
1✔
226
    readonly drawFocusRing: boolean | undefined;
1✔
227

1✔
228
    readonly dragAndDropState:
1✔
229
        | {
1✔
230
              src: number;
1✔
231
              dest: number;
1✔
232
          }
1✔
233
        | undefined;
1✔
234

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

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

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

1✔
285
    readonly theme: FullTheme;
1✔
286

1✔
287
    readonly getCellRenderer: <T extends InnerGridCell>(cell: T) => CellRenderer<T> | undefined;
1✔
288
}
1✔
289

1✔
290
type DamageUpdateList = readonly {
1✔
291
    cell: Item;
1✔
292
    // newValue: GridCell,
1✔
293
}[];
1✔
294

1✔
295
const fillHandleClickSize = 6;
1✔
296

1✔
297
export interface DataGridRef {
1✔
298
    focus: () => void;
1✔
299
    getBounds: (col?: number, row?: number) => Rectangle | undefined;
1✔
300
    damage: (cells: DamageUpdateList) => void;
1✔
301
}
1✔
302

1✔
303
const getRowData = (cell: InnerGridCell, getCellRenderer?: GetCellRendererCallback) => {
1✔
304
    if (cell.kind === GridCellKind.Custom) return cell.copyData;
26,560✔
305
    const r = getCellRenderer?.(cell);
24,640✔
306
    return r?.getAccessibilityString(cell) ?? "";
26,560!
307
};
26,560✔
308

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

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

812✔
389
    const [lastWasTouch, setLastWasTouch] = React.useState(false);
812✔
390
    const lastWasTouchRef = React.useRef(lastWasTouch);
812✔
391
    lastWasTouchRef.current = lastWasTouch;
812✔
392

812✔
393
    const spriteManager = React.useMemo(
812✔
394
        () =>
812✔
395
            new SpriteManager(headerIcons, () => {
149✔
396
                lastArgsRef.current = undefined;
×
397
                lastDrawRef.current();
×
398
            }),
149✔
399
        [headerIcons]
812✔
400
    );
812✔
401
    const totalHeaderHeight = enableGroups ? groupHeaderHeight + headerHeight : headerHeight;
812✔
402

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

812✔
418
    const mappedColumns = useMappedColumns(columns, freezeColumns);
812✔
419
    const stickyX = fixedShadowX ? getStickyWidth(mappedColumns, dragAndDropState) : 0;
812!
420

812✔
421
    // row: -1 === columnHeader, -2 === groupHeader
812✔
422
    const getBoundsForItem = React.useCallback(
812✔
423
        (canvas: HTMLCanvasElement, col: number, row: number): Rectangle | undefined => {
812✔
424
            const rect = canvas.getBoundingClientRect();
581✔
425

581✔
426
            if (col >= mappedColumns.length || row >= rows) {
581!
427
                return undefined;
×
428
            }
×
429

581✔
430
            const scale = rect.width / width;
581✔
431

581✔
432
            const result = computeBounds(
581✔
433
                col,
581✔
434
                row,
581✔
435
                width,
581✔
436
                height,
581✔
437
                groupHeaderHeight,
581✔
438
                totalHeaderHeight,
581✔
439
                cellXOffset,
581✔
440
                cellYOffset,
581✔
441
                translateX,
581✔
442
                translateY,
581✔
443
                rows,
581✔
444
                freezeColumns,
581✔
445
                trailingRowType === "sticky",
581✔
446
                mappedColumns,
581✔
447
                rowHeight
581✔
448
            );
581✔
449

581✔
450
            if (scale !== 1) {
581!
451
                result.x *= scale;
×
452
                result.y *= scale;
×
453
                result.width *= scale;
×
454
                result.height *= scale;
×
455
            }
×
456

581✔
457
            result.x += rect.x;
581✔
458
            result.y += rect.y;
581✔
459

581✔
460
            return result;
581✔
461
        },
581✔
462
        [
812✔
463
            width,
812✔
464
            height,
812✔
465
            groupHeaderHeight,
812✔
466
            totalHeaderHeight,
812✔
467
            cellXOffset,
812✔
468
            cellYOffset,
812✔
469
            translateX,
812✔
470
            translateY,
812✔
471
            rows,
812✔
472
            freezeColumns,
812✔
473
            trailingRowType,
812✔
474
            mappedColumns,
812✔
475
            rowHeight,
812✔
476
        ]
812✔
477
    );
812✔
478

812✔
479
    const getMouseArgsForPosition = React.useCallback(
812✔
480
        (canvas: HTMLCanvasElement, posX: number, posY: number, ev?: MouseEvent | TouchEvent): GridMouseEventArgs => {
812✔
481
            const rect = canvas.getBoundingClientRect();
455✔
482
            const scale = rect.width / width;
455✔
483
            const x = (posX - rect.left) / scale;
455✔
484
            const y = (posY - rect.top) / scale;
455✔
485
            const edgeDetectionBuffer = 5;
455✔
486

455✔
487
            const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, undefined, translateX);
455✔
488

455✔
489
            let button = 0;
455✔
490
            if (ev instanceof MouseEvent) {
455✔
491
                button = ev.button;
446✔
492
            }
446✔
493

455✔
494
            // -1 === off right edge
455✔
495
            const col = getColumnIndexForX(x, effectiveCols, translateX);
455✔
496

455✔
497
            // -1: header or above
455✔
498
            // undefined: offbottom
455✔
499
            const row = getRowIndexForY(
455✔
500
                y,
455✔
501
                height,
455✔
502
                enableGroups,
455✔
503
                headerHeight,
455✔
504
                groupHeaderHeight,
455✔
505
                rows,
455✔
506
                rowHeight,
455✔
507
                cellYOffset,
455✔
508
                translateY,
455✔
509
                trailingRowType === "sticky"
455✔
510
            );
455✔
511

455✔
512
            const shiftKey = ev?.shiftKey === true;
455✔
513
            const ctrlKey = ev?.ctrlKey === true;
455✔
514
            const metaKey = ev?.metaKey === true;
455✔
515
            const isTouch = (ev !== undefined && !(ev instanceof MouseEvent)) || (ev as any)?.pointerType === "touch";
455✔
516

455✔
517
            const scrollEdge: GridMouseEventArgs["scrollEdge"] = [
455✔
518
                x < stickyX ? -1 : rect.width < x ? 1 : 0,
455!
519
                y < totalHeaderHeight ? -1 : rect.height < y ? 1 : 0,
455!
520
            ];
455✔
521

455✔
522
            let result: GridMouseEventArgs;
455✔
523
            if (col === -1 || y < 0 || x < 0 || row === undefined || x > width || y > height) {
455✔
524
                const horizontal = x > width ? 1 : x < 0 ? -1 : 0;
14!
525
                const vertical = y > height ? 1 : y < 0 ? -1 : 0;
14!
526

14✔
527
                let innerHorizontal: OutOfBoundsRegionAxis = horizontal * 2;
14✔
528
                let innerVertical: OutOfBoundsRegionAxis = vertical * 2;
14✔
529
                if (horizontal === 0)
14✔
530
                    innerHorizontal = col === -1 ? OutOfBoundsRegionAxis.EndPadding : OutOfBoundsRegionAxis.Center;
14✔
531
                if (vertical === 0)
14✔
532
                    innerVertical = row === undefined ? OutOfBoundsRegionAxis.EndPadding : OutOfBoundsRegionAxis.Center;
14✔
533

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

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

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

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

352✔
607
                let isFillHandle = false;
352✔
608
                if (fillHandle && selection.current !== undefined) {
352✔
609
                    const fillHandleLocation = rectBottomRight(selection.current.range);
21✔
610
                    const fillHandleCellBounds = getBoundsForItem(canvas, fillHandleLocation[0], fillHandleLocation[1]);
21✔
611

21✔
612
                    if (fillHandleCellBounds !== undefined) {
21✔
613
                        const handleLogicalCenterX = fillHandleCellBounds.x + fillHandleCellBounds.width - 2;
21✔
614
                        const handleLogicalCenterY = fillHandleCellBounds.y + fillHandleCellBounds.height - 2;
21✔
615

21✔
616
                        //check if posX and posY are within fillHandleClickSize from handleLogicalCenter
21✔
617
                        isFillHandle =
21✔
618
                            Math.abs(handleLogicalCenterX - posX) < fillHandleClickSize &&
21✔
619
                            Math.abs(handleLogicalCenterY - posY) < fillHandleClickSize;
10✔
620
                    }
21✔
621
                }
21✔
622

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

812✔
663
    const [hoveredItem] = hoveredItemInfo ?? [];
812✔
664

812✔
665
    const enqueueRef = React.useRef<EnqueueCallback>(() => {
812✔
666
        // do nothing
×
667
    });
812✔
668
    const hoverInfoRef = React.useRef(hoveredItemInfo);
812✔
669
    hoverInfoRef.current = hoveredItemInfo;
812✔
670

812✔
671
    const [bufferA, bufferB] = React.useMemo(() => {
812✔
672
        const a = document.createElement("canvas");
149✔
673
        const b = document.createElement("canvas");
149✔
674
        a.style["display"] = "none";
149✔
675
        a.style["opacity"] = "0";
149✔
676
        a.style["position"] = "fixed";
149✔
677
        b.style["display"] = "none";
149✔
678
        b.style["opacity"] = "0";
149✔
679
        b.style["position"] = "fixed";
149✔
680
        return [a, b];
149✔
681
    }, []);
812✔
682

812✔
683
    React.useLayoutEffect(() => {
812✔
684
        document.documentElement.append(bufferA);
149✔
685
        document.documentElement.append(bufferB);
149✔
686
        return () => {
149✔
687
            bufferA.remove();
149✔
688
            bufferB.remove();
149✔
689
        };
149✔
690
    }, [bufferA, bufferB]);
812✔
691

812✔
692
    const renderStateProvider = React.useMemo(() => new RenderStateProvider(), []);
812✔
693

812✔
694
    const minimumCellWidth = experimental?.disableMinimumCellWidth === true ? 1 : 10;
812!
695
    const lastArgsRef = React.useRef<DrawGridArg>();
812✔
696
    const draw = React.useCallback(() => {
812✔
697
        const canvas = ref.current;
654✔
698
        const overlay = overlayRef.current;
654✔
699
        if (canvas === null || overlay === null) return;
654✔
700

588✔
701
        let didOverride = false;
588✔
702
        const overrideCursor = (cursor: React.CSSProperties["cursor"]) => {
588✔
NEW
703
            didOverride = true;
×
NEW
704
            setDrawCursorOverride(cursor);
×
NEW
705
        };
×
706

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

654✔
761
        // This confusing bit of code due to some poor design. Long story short, the damage property is only used
654✔
762
        // with what is effectively the "last args" for the last normal draw anyway. We don't want the drawing code
654✔
763
        // to look at this and go "shit dawg, nothing changed" so we force it to draw frash, but the damage restricts
654✔
764
        // the draw anyway.
654✔
765
        //
654✔
766
        // Dear future Jason, I'm sorry. It was expedient, it worked, and had almost zero perf overhead. THe universe
654✔
767
        // basically made me do it. What choice did I have?
654✔
768
        if (current.damage === undefined) {
654✔
769
            lastArgsRef.current = current;
533✔
770
            drawGrid(current, last);
533✔
771
        } else {
639✔
772
            drawGrid(current, undefined);
55✔
773
        }
55✔
774

588✔
775
        // don't reset on damage events
588✔
776
        if (!didOverride && (current.damage === undefined || current.damage.has(hoverInfoRef?.current?.[0]))) {
654✔
777
            setDrawCursorOverride(undefined);
558✔
778
        }
558✔
779
    }, [
812✔
780
        bufferA,
812✔
781
        bufferB,
812✔
782
        width,
812✔
783
        height,
812✔
784
        cellXOffset,
812✔
785
        cellYOffset,
812✔
786
        translateX,
812✔
787
        translateY,
812✔
788
        mappedColumns,
812✔
789
        enableGroups,
812✔
790
        freezeColumns,
812✔
791
        dragAndDropState,
812✔
792
        theme,
812✔
793
        headerHeight,
812✔
794
        groupHeaderHeight,
812✔
795
        disabledRows,
812✔
796
        rowHeight,
812✔
797
        verticalBorder,
812✔
798
        isResizing,
812✔
799
        resizeCol,
812✔
800
        isFocused,
812✔
801
        selection,
812✔
802
        fillHandle,
812✔
803
        trailingRowType,
812✔
804
        rows,
812✔
805
        drawFocusRing,
812✔
806
        getCellContent,
812✔
807
        getGroupDetails,
812✔
808
        getRowThemeOverride,
812✔
809
        drawCellCallback,
812✔
810
        drawHeaderCallback,
812✔
811
        prelightCells,
812✔
812
        highlightRegions,
812✔
813
        imageLoader,
812✔
814
        spriteManager,
812✔
815
        scrolling,
812✔
816
        experimental?.hyperWrapping,
812✔
817
        experimental?.renderStrategy,
812✔
818
        lastWasTouch,
812✔
819
        renderStateProvider,
812✔
820
        getCellRenderer,
812✔
821
        minimumCellWidth,
812✔
822
    ]);
812✔
823

812✔
824
    const lastDrawRef = React.useRef(draw);
812✔
825
    React.useLayoutEffect(() => {
812✔
826
        draw();
533✔
827
        lastDrawRef.current = draw;
533✔
828
    }, [draw]);
812✔
829

812✔
830
    React.useLayoutEffect(() => {
812✔
831
        const fn = async () => {
149✔
832
            if (document?.fonts?.ready === undefined) return;
149!
833
            await document.fonts.ready;
×
834
            lastArgsRef.current = undefined;
×
835
            lastDrawRef.current();
×
836
        };
149✔
837
        void fn();
149✔
838
    }, []);
812✔
839

812✔
840
    const damageInternal = React.useCallback((locations: CellSet) => {
812✔
841
        damageRegion.current = locations;
31✔
842
        lastDrawRef.current();
31✔
843
        damageRegion.current = undefined;
31✔
844
    }, []);
812✔
845

812✔
846
    const enqueue = useAnimationQueue(damageInternal);
812✔
847
    enqueueRef.current = enqueue;
812✔
848

812✔
849
    const damage = React.useCallback(
812✔
850
        (cells: DamageUpdateList) => {
812✔
851
            damageInternal(new CellSet(cells.map(x => x.cell)));
31✔
852
        },
31✔
853
        [damageInternal]
812✔
854
    );
812✔
855

812✔
856
    imageLoader.setCallback(damageInternal);
812✔
857

812✔
858
    const [overFill, setOverFill] = React.useState(false);
812✔
859

812✔
860
    const [hCol, hRow] = hoveredItem ?? [];
812✔
861
    const headerHovered = hCol !== undefined && hRow === -1;
812✔
862
    const groupHeaderHovered = hCol !== undefined && hRow === -2;
812✔
863
    let clickableInnerCellHovered = false;
812✔
864
    let editableBoolHovered = false;
812✔
865
    let cursorOverride: React.CSSProperties["cursor"] | undefined = drawCursorOverride;
812✔
866
    if (cursorOverride === undefined && hCol !== undefined && hRow !== undefined && hRow > -1 && hRow < rows) {
812✔
867
        const cell = getCellContent([hCol, hRow], true);
48✔
868
        clickableInnerCellHovered =
48✔
869
            cell.kind === InnerGridCellKind.NewRow ||
48✔
870
            (cell.kind === InnerGridCellKind.Marker && cell.markerKind !== "number");
45✔
871
        editableBoolHovered = cell.kind === GridCellKind.Boolean && booleanCellIsEditable(cell);
48!
872
        cursorOverride = cell.cursor;
48✔
873
    }
48✔
874
    const canDrag = hoveredOnEdge ?? false;
812✔
875
    const cursor = isDragging
812✔
876
        ? "grabbing"
5✔
877
        : canDrag || isResizing
806✔
878
        ? "col-resize"
12✔
879
        : overFill || isFilling
794✔
880
        ? "crosshair"
14✔
881
        : cursorOverride !== undefined
780!
882
        ? cursorOverride
×
883
        : headerHovered || clickableInnerCellHovered || editableBoolHovered || groupHeaderHovered
780✔
884
        ? "pointer"
40✔
885
        : "default";
740✔
886
    const style = React.useMemo(
812✔
887
        () => ({
812✔
888
            // width,
188✔
889
            // height,
188✔
890
            contain: "strict",
188✔
891
            display: "block",
188✔
892
            cursor,
188✔
893
        }),
188✔
894
        [cursor]
812✔
895
    );
812✔
896

812✔
897
    const lastSetCursor = React.useRef<typeof cursor>("default");
812✔
898
    const target = eventTargetRef?.current;
812✔
899
    if (target !== null && target !== undefined && lastSetCursor.current !== style.cursor) {
812✔
900
        // because we have an event target we need to set its cursor instead.
36✔
901
        target.style.cursor = lastSetCursor.current = style.cursor;
36✔
902
    }
36✔
903

811✔
904
    const groupHeaderActionForEvent = React.useCallback(
811✔
905
        (group: string, bounds: Rectangle, localEventX: number, localEventY: number) => {
811✔
906
            if (getGroupDetails === undefined) return undefined;
15!
907
            const groupDesc = getGroupDetails(group);
15✔
908
            if (groupDesc.actions !== undefined) {
15✔
909
                const boxes = getActionBoundsForGroup(bounds, groupDesc.actions);
3✔
910
                for (const [i, box] of boxes.entries()) {
3✔
911
                    if (pointInRect(box, localEventX + bounds.x, localEventY + box.y)) {
3✔
912
                        return groupDesc.actions[i];
3✔
913
                    }
3✔
914
                }
3!
915
            }
✔
916
            return undefined;
12✔
917
        },
15✔
918
        [getGroupDetails]
811✔
919
    );
811✔
920

811✔
921
    const isOverHeaderMenu = React.useCallback(
811✔
922
        (canvas: HTMLCanvasElement, col: number, clientX: number, clientY: number) => {
811✔
923
            const header = columns[col];
282✔
924

282✔
925
            if (!isDragging && !isResizing && header.hasMenu === true && !(hoveredOnEdge ?? false)) {
282✔
926
                const headerBounds = getBoundsForItem(canvas, col, -1);
6✔
927
                assert(headerBounds !== undefined);
6✔
928
                const menuBounds = getHeaderMenuBounds(
6✔
929
                    headerBounds.x,
6✔
930
                    headerBounds.y,
6✔
931
                    headerBounds.width,
6✔
932
                    headerBounds.height,
6✔
933
                    direction(header.title) === "rtl"
6✔
934
                );
6✔
935
                if (
6✔
936
                    clientX > menuBounds.x &&
6✔
937
                    clientX < menuBounds.x + menuBounds.width &&
6✔
938
                    clientY > menuBounds.y &&
6✔
939
                    clientY < menuBounds.y + menuBounds.height
6✔
940
                ) {
6✔
941
                    return headerBounds;
6✔
942
                }
6✔
943
            }
6✔
944
            return undefined;
276✔
945
        },
282✔
946
        [columns, getBoundsForItem, hoveredOnEdge, isDragging, isResizing]
811✔
947
    );
811✔
948

811✔
949
    const downTime = React.useRef(0);
811✔
950
    const downPosition = React.useRef<Item>();
811✔
951
    const mouseDown = React.useRef(false);
811✔
952
    const onMouseDownImpl = React.useCallback(
811✔
953
        (ev: MouseEvent | TouchEvent) => {
811✔
954
            const canvas = ref.current;
142✔
955
            const eventTarget = eventTargetRef?.current;
142✔
956
            if (canvas === null || (ev.target !== canvas && ev.target !== eventTarget)) return;
142!
957
            mouseDown.current = true;
142✔
958

142✔
959
            let clientX: number;
142✔
960
            let clientY: number;
142✔
961
            if (ev instanceof MouseEvent) {
142✔
962
                clientX = ev.clientX;
138✔
963
                clientY = ev.clientY;
138✔
964
            } else {
142✔
965
                clientX = ev.touches[0].clientX;
4✔
966
                clientY = ev.touches[0].clientY;
4✔
967
            }
4✔
968
            if (ev.target === eventTarget && eventTarget !== null) {
142✔
969
                const bounds = eventTarget.getBoundingClientRect();
1✔
970
                if (clientX > bounds.right || clientY > bounds.bottom) return;
1!
971
            }
1✔
972

142✔
973
            const args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
142✔
974
            downPosition.current = args.location;
142✔
975

142✔
976
            if (args.isTouch) {
142✔
977
                downTime.current = Date.now();
4✔
978
            }
4✔
979
            if (lastWasTouchRef.current !== args.isTouch) {
142✔
980
                setLastWasTouch(args.isTouch);
4✔
981
            }
4✔
982

142✔
983
            if (
142✔
984
                args.kind === headerKind &&
142✔
985
                isOverHeaderMenu(canvas, args.location[0], clientX, clientY) !== undefined
20✔
986
            ) {
142✔
987
                return;
2✔
988
            } else if (args.kind === groupHeaderKind) {
142✔
989
                const action = groupHeaderActionForEvent(args.group, args.bounds, args.localEventX, args.localEventY);
5✔
990
                if (action !== undefined) {
5✔
991
                    return;
1✔
992
                }
1✔
993
            }
5✔
994

139✔
995
            onMouseDown?.(args);
139✔
996
            if (!args.isTouch && isDraggable !== true && isDraggable !== args.kind && args.button < 3) {
142✔
997
                // preventing default in touch events stops scroll
133✔
998
                ev.preventDefault();
133✔
999
            }
133✔
1000
        },
142✔
1001
        [eventTargetRef, isDraggable, getMouseArgsForPosition, groupHeaderActionForEvent, isOverHeaderMenu, onMouseDown]
811✔
1002
    );
811✔
1003
    useEventListener("touchstart", onMouseDownImpl, window, false);
811✔
1004
    useEventListener("mousedown", onMouseDownImpl, window, false);
811✔
1005

811✔
1006
    const lastUpTime = React.useRef(0);
811✔
1007

811✔
1008
    const onMouseUpImpl = React.useCallback(
811✔
1009
        (ev: MouseEvent | TouchEvent) => {
811✔
1010
            const lastUpTimeValue = lastUpTime.current;
140✔
1011
            lastUpTime.current = Date.now();
140✔
1012
            const canvas = ref.current;
140✔
1013
            mouseDown.current = false;
140✔
1014
            if (onMouseUp === undefined || canvas === null) return;
140!
1015
            const eventTarget = eventTargetRef?.current;
140✔
1016

140✔
1017
            const isOutside = ev.target !== canvas && ev.target !== eventTarget;
140!
1018

140✔
1019
            let clientX: number;
140✔
1020
            let clientY: number;
140✔
1021
            let canCancel = true;
140✔
1022
            if (ev instanceof MouseEvent) {
140✔
1023
                clientX = ev.clientX;
136✔
1024
                clientY = ev.clientY;
136✔
1025
                canCancel = ev.button < 3;
136✔
1026
                if ((ev as any).pointerType === "touch") {
136!
1027
                    return;
×
1028
                }
×
1029
            } else {
140✔
1030
                clientX = ev.changedTouches[0].clientX;
4✔
1031
                clientY = ev.changedTouches[0].clientY;
4✔
1032
            }
4✔
1033

140✔
1034
            let args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
140✔
1035

140✔
1036
            if (args.isTouch && downTime.current !== 0 && Date.now() - downTime.current > 500) {
140!
1037
                args = {
×
1038
                    ...args,
×
1039
                    isLongTouch: true,
×
1040
                };
×
1041
            }
×
1042

140✔
1043
            if (lastUpTimeValue !== 0 && Date.now() - lastUpTimeValue < (args.isTouch ? 1000 : 500)) {
140✔
1044
                args = {
32✔
1045
                    ...args,
32✔
1046
                    isDoubleClick: true,
32✔
1047
                };
32✔
1048
            }
32✔
1049

140✔
1050
            if (lastWasTouchRef.current !== args.isTouch) {
140!
1051
                setLastWasTouch(args.isTouch);
×
1052
            }
×
1053

140✔
1054
            if (!isOutside && ev.cancelable && canCancel) {
140✔
1055
                ev.preventDefault();
139✔
1056
            }
139✔
1057

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

136✔
1076
            onMouseUp(args, isOutside);
136✔
1077
        },
140✔
1078
        [onMouseUp, eventTargetRef, getMouseArgsForPosition, isOverHeaderMenu, groupHeaderActionForEvent]
811✔
1079
    );
811✔
1080
    useEventListener("mouseup", onMouseUpImpl, window, false);
811✔
1081
    useEventListener("touchend", onMouseUpImpl, window, false);
811✔
1082

811✔
1083
    const onClickImpl = React.useCallback(
811✔
1084
        (ev: MouseEvent | TouchEvent) => {
811✔
1085
            const canvas = ref.current;
122✔
1086
            if (canvas === null) return;
122!
1087
            const eventTarget = eventTargetRef?.current;
122✔
1088

122✔
1089
            const isOutside = ev.target !== canvas && ev.target !== eventTarget;
122✔
1090

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

122✔
1103
            const args = getMouseArgsForPosition(canvas, clientX, clientY, ev);
122✔
1104

122✔
1105
            if (lastWasTouchRef.current !== args.isTouch) {
122✔
1106
                setLastWasTouch(args.isTouch);
4✔
1107
            }
4✔
1108

122✔
1109
            if (!isOutside && ev.cancelable && canCancel) {
122✔
1110
                ev.preventDefault();
120✔
1111
            }
120✔
1112

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

811✔
1130
    const onContextMenuImpl = React.useCallback(
811✔
1131
        (ev: MouseEvent) => {
811✔
1132
            const canvas = ref.current;
7✔
1133
            const eventTarget = eventTargetRef?.current;
7✔
1134
            if (canvas === null || (ev.target !== canvas && ev.target !== eventTarget) || onContextMenu === undefined)
7✔
1135
                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
        [eventTargetRef, getMouseArgsForPosition, onContextMenu]
811✔
1142
    );
811✔
1143
    useEventListener("contextmenu", onContextMenuImpl, eventTargetRef?.current ?? null, false);
812✔
1144

812✔
1145
    const onAnimationFrame = React.useCallback<StepCallback>(values => {
812✔
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
    }, []);
812✔
1151

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

812✔
1168
    const hoveredRef = React.useRef<GridMouseEventArgs>();
812✔
1169
    const onMouseMoveImpl = React.useCallback(
812✔
1170
        (ev: MouseEvent) => {
812✔
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 (!mouseEventArgsAreEqual(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
            setOverFill(args.kind === "cell" && args.isFillHandle);
43✔
1234

43✔
1235
            onMouseMoveRaw?.(ev);
43✔
1236
            onMouseMove(args);
43✔
1237
        },
43✔
1238
        [
812✔
1239
            eventTargetRef,
812✔
1240
            getMouseArgsForPosition,
812✔
1241
            firstColAccessible,
812✔
1242
            allowResize,
812✔
1243
            onMouseMoveRaw,
812✔
1244
            onMouseMove,
812✔
1245
            onItemHovered,
812✔
1246
            getCellContent,
812✔
1247
            getCellRenderer,
812✔
1248
            damageInternal,
812✔
1249
        ]
812✔
1250
    );
812✔
1251
    useEventListener("mousemove", onMouseMoveImpl, window, true);
812✔
1252

812✔
1253
    const onKeyDownImpl = React.useCallback(
812✔
1254
        (event: React.KeyboardEvent<HTMLCanvasElement>) => {
812✔
1255
            const canvas = ref.current;
57✔
1256
            if (canvas === null) return;
57!
1257

57✔
1258
            let bounds: Rectangle | undefined;
57✔
1259
            let location: Item | undefined = undefined;
57✔
1260
            if (selection.current !== undefined) {
57✔
1261
                bounds = getBoundsForItem(canvas, selection.current.cell[0], selection.current.cell[1]);
54✔
1262
                location = selection.current.cell;
54✔
1263
            }
54✔
1264

57✔
1265
            onKeyDown?.({
57✔
1266
                bounds,
57✔
1267
                stopPropagation: () => event.stopPropagation(),
57✔
1268
                preventDefault: () => event.preventDefault(),
57✔
1269
                cancel: () => undefined,
57✔
1270
                ctrlKey: event.ctrlKey,
57✔
1271
                metaKey: event.metaKey,
57✔
1272
                shiftKey: event.shiftKey,
57✔
1273
                altKey: event.altKey,
57✔
1274
                key: event.key,
57✔
1275
                keyCode: event.keyCode,
57✔
1276
                rawEvent: event,
57✔
1277
                location,
57✔
1278
            });
57✔
1279
        },
57✔
1280
        [onKeyDown, selection, getBoundsForItem]
812✔
1281
    );
812✔
1282

812✔
1283
    const onKeyUpImpl = React.useCallback(
812✔
1284
        (event: React.KeyboardEvent<HTMLCanvasElement>) => {
812✔
1285
            const canvas = ref.current;
7✔
1286
            if (canvas === null) return;
7!
1287

7✔
1288
            let bounds: Rectangle | undefined;
7✔
1289
            let location: Item | undefined = undefined;
7✔
1290
            if (selection.current !== undefined) {
7✔
1291
                bounds = getBoundsForItem(canvas, selection.current.cell[0], selection.current.cell[1]);
7✔
1292
                location = selection.current.cell;
7✔
1293
            }
7✔
1294

7✔
1295
            onKeyUp?.({
7✔
1296
                bounds,
1✔
1297
                stopPropagation: () => event.stopPropagation(),
1✔
1298
                preventDefault: () => event.preventDefault(),
1✔
1299
                cancel: () => undefined,
1✔
1300
                ctrlKey: event.ctrlKey,
1✔
1301
                metaKey: event.metaKey,
1✔
1302
                shiftKey: event.shiftKey,
1✔
1303
                altKey: event.altKey,
1✔
1304
                key: event.key,
1✔
1305
                keyCode: event.keyCode,
1✔
1306
                rawEvent: event,
1✔
1307
                location,
1✔
1308
            });
1✔
1309
        },
7✔
1310
        [onKeyUp, selection, getBoundsForItem]
812✔
1311
    );
812✔
1312

812✔
1313
    const refImpl = React.useCallback(
812✔
1314
        (instance: HTMLCanvasElement | null) => {
812✔
1315
            ref.current = instance;
298✔
1316
            if (canvasRef !== undefined) {
298✔
1317
                canvasRef.current = instance;
274✔
1318
            }
274✔
1319
        },
298✔
1320
        [canvasRef]
812✔
1321
    );
812✔
1322

812✔
1323
    const onDragStartImpl = React.useCallback(
812✔
1324
        (event: DragEvent) => {
812✔
1325
            const canvas = ref.current;
1✔
1326
            if (canvas === null || isDraggable === false || isResizing) {
1!
1327
                event.preventDefault();
×
1328
                return;
×
1329
            }
×
1330

1✔
1331
            let dragMime: string | undefined;
1✔
1332
            let dragData: string | undefined;
1✔
1333

1✔
1334
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
1✔
1335

1✔
1336
            if (isDraggable !== true && args.kind !== isDraggable) {
1!
1337
                event.preventDefault();
×
1338
                return;
×
1339
            }
×
1340

1✔
1341
            const setData = (mime: string, payload: string) => {
1✔
1342
                dragMime = mime;
1✔
1343
                dragData = payload;
1✔
1344
            };
1✔
1345

1✔
1346
            let dragImage: Element | undefined;
1✔
1347
            let dragImageX: number | undefined;
1✔
1348
            let dragImageY: number | undefined;
1✔
1349
            const setDragImage = (image: Element, x: number, y: number) => {
1✔
1350
                dragImage = image;
×
1351
                dragImageX = x;
×
1352
                dragImageY = y;
×
1353
            };
×
1354

1✔
1355
            let prevented = false;
1✔
1356

1✔
1357
            onDragStart?.({
1✔
1358
                ...args,
1✔
1359
                setData,
1✔
1360
                setDragImage,
1✔
1361
                preventDefault: () => (prevented = true),
1✔
1362
                defaultPrevented: () => prevented,
1✔
1363
            });
1✔
1364
            if (!prevented && dragMime !== undefined && dragData !== undefined && event.dataTransfer !== null) {
1✔
1365
                event.dataTransfer.setData(dragMime, dragData);
1✔
1366
                event.dataTransfer.effectAllowed = "copyLink";
1✔
1367

1✔
1368
                if (dragImage !== undefined && dragImageX !== undefined && dragImageY !== undefined) {
1!
1369
                    event.dataTransfer.setDragImage(dragImage, dragImageX, dragImageY);
×
1370
                } else {
1✔
1371
                    const [col, row] = args.location;
1✔
1372
                    if (row !== undefined) {
1✔
1373
                        const offscreen = document.createElement("canvas");
1✔
1374
                        const boundsForDragTarget = getBoundsForItem(canvas, col, row);
1✔
1375

1✔
1376
                        assert(boundsForDragTarget !== undefined);
1✔
1377
                        const dpr = Math.ceil(window.devicePixelRatio ?? 1);
1!
1378
                        offscreen.width = boundsForDragTarget.width * dpr;
1✔
1379
                        offscreen.height = boundsForDragTarget.height * dpr;
1✔
1380

1✔
1381
                        const ctx = offscreen.getContext("2d");
1✔
1382
                        if (ctx !== null) {
1✔
1383
                            ctx.scale(dpr, dpr);
1✔
1384
                            ctx.textBaseline = "middle";
1✔
1385
                            if (row === -1) {
1!
NEW
1386
                                ctx.font = theme.headerFontFull;
×
1387
                                ctx.fillStyle = theme.bgHeader;
×
1388
                                ctx.fillRect(0, 0, offscreen.width, offscreen.height);
×
1389
                                drawHeader(
×
1390
                                    ctx,
×
1391
                                    0,
×
1392
                                    0,
×
1393
                                    boundsForDragTarget.width,
×
1394
                                    boundsForDragTarget.height,
×
1395
                                    mappedColumns[col],
×
1396
                                    false,
×
1397
                                    theme,
×
1398
                                    false,
×
1399
                                    false,
×
1400
                                    0,
×
1401
                                    spriteManager,
×
1402
                                    drawHeaderCallback,
×
1403
                                    false
×
1404
                                );
×
1405
                            } else {
1✔
1406
                                ctx.font = theme.baseFontFull;
1✔
1407
                                ctx.fillStyle = theme.bgCell;
1✔
1408
                                ctx.fillRect(0, 0, offscreen.width, offscreen.height);
1✔
1409
                                drawCell(
1✔
1410
                                    ctx,
1✔
1411
                                    row,
1✔
1412
                                    getCellContent([col, row]),
1✔
1413
                                    0,
1✔
1414
                                    0,
1✔
1415
                                    0,
1✔
1416
                                    boundsForDragTarget.width,
1✔
1417
                                    boundsForDragTarget.height,
1✔
1418
                                    false,
1✔
1419
                                    theme,
1✔
1420
                                    theme.bgCell,
1✔
1421
                                    imageLoader,
1✔
1422
                                    spriteManager,
1✔
1423
                                    1,
1✔
1424
                                    undefined,
1✔
1425
                                    false,
1✔
1426
                                    0,
1✔
1427
                                    undefined,
1✔
1428
                                    undefined,
1✔
1429
                                    undefined,
1✔
1430
                                    renderStateProvider,
1✔
1431
                                    getCellRenderer,
1✔
1432
                                    () => undefined
1✔
1433
                                );
1✔
1434
                            }
1✔
1435
                        }
1✔
1436

1✔
1437
                        offscreen.style.left = "-100%";
1✔
1438
                        offscreen.style.position = "absolute";
1✔
1439
                        offscreen.style.width = `${boundsForDragTarget.width}px`;
1✔
1440
                        offscreen.style.height = `${boundsForDragTarget.height}px`;
1✔
1441

1✔
1442
                        document.body.append(offscreen);
1✔
1443

1✔
1444
                        event.dataTransfer.setDragImage(
1✔
1445
                            offscreen,
1✔
1446
                            boundsForDragTarget.width / 2,
1✔
1447
                            boundsForDragTarget.height / 2
1✔
1448
                        );
1✔
1449

1✔
1450
                        window.setTimeout(() => {
1✔
1451
                            offscreen.remove();
1✔
1452
                        }, 0);
1✔
1453
                    }
1✔
1454
                }
1✔
1455
            } else {
1!
1456
                event.preventDefault();
×
1457
            }
×
1458
        },
1✔
1459
        [
812✔
1460
            isDraggable,
812✔
1461
            isResizing,
812✔
1462
            getMouseArgsForPosition,
812✔
1463
            onDragStart,
812✔
1464
            getBoundsForItem,
812✔
1465
            theme,
812✔
1466
            mappedColumns,
812✔
1467
            spriteManager,
812✔
1468
            drawHeaderCallback,
812✔
1469
            getCellContent,
812✔
1470
            imageLoader,
812✔
1471
            renderStateProvider,
812✔
1472
            getCellRenderer,
812✔
1473
        ]
812✔
1474
    );
812✔
1475
    useEventListener("dragstart", onDragStartImpl, eventTargetRef?.current ?? null, false, false);
812✔
1476

812✔
1477
    const activeDropTarget = React.useRef<Item | undefined>();
812✔
1478

812✔
1479
    const onDragOverImpl = React.useCallback(
812✔
1480
        (event: DragEvent) => {
812✔
1481
            const canvas = ref.current;
×
1482
            if (onDrop !== undefined) {
×
1483
                // Need to preventDefault to allow drop
×
1484
                event.preventDefault();
×
1485
            }
×
1486

×
1487
            if (canvas === null || onDragOverCell === undefined) {
×
1488
                return;
×
1489
            }
×
1490

×
1491
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
×
1492

×
1493
            const [rawCol, row] = args.location;
×
1494
            const col = rawCol - (firstColAccessible ? 0 : 1);
×
1495
            const [activeCol, activeRow] = activeDropTarget.current ?? [];
×
1496

×
1497
            if (activeCol !== col || activeRow !== row) {
×
1498
                activeDropTarget.current = [col, row];
×
1499
                onDragOverCell([col, row], event.dataTransfer);
×
1500
            }
×
1501
        },
×
1502
        [firstColAccessible, getMouseArgsForPosition, onDragOverCell, onDrop]
812✔
1503
    );
812✔
1504
    useEventListener("dragover", onDragOverImpl, eventTargetRef?.current ?? null, false, false);
812✔
1505

812✔
1506
    const onDragEndImpl = React.useCallback(() => {
812✔
1507
        activeDropTarget.current = undefined;
×
1508
        onDragEnd?.();
×
1509
    }, [onDragEnd]);
812✔
1510
    useEventListener("dragend", onDragEndImpl, eventTargetRef?.current ?? null, false, false);
812✔
1511

812✔
1512
    const onDropImpl = React.useCallback(
812✔
1513
        (event: DragEvent) => {
812✔
1514
            const canvas = ref.current;
×
1515
            if (canvas === null || onDrop === undefined) {
×
1516
                return;
×
1517
            }
×
1518

×
1519
            // Default can mess up sometimes.
×
1520
            event.preventDefault();
×
1521

×
1522
            const args = getMouseArgsForPosition(canvas, event.clientX, event.clientY);
×
1523

×
1524
            const [rawCol, row] = args.location;
×
1525
            const col = rawCol - (firstColAccessible ? 0 : 1);
×
1526

×
1527
            onDrop([col, row], event.dataTransfer);
×
1528
        },
×
1529
        [firstColAccessible, getMouseArgsForPosition, onDrop]
812✔
1530
    );
812✔
1531
    useEventListener("drop", onDropImpl, eventTargetRef?.current ?? null, false, false);
812✔
1532

812✔
1533
    const onDragLeaveImpl = React.useCallback(() => {
812✔
1534
        onDragLeave?.();
×
1535
    }, [onDragLeave]);
812✔
1536
    useEventListener("dragleave", onDragLeaveImpl, eventTargetRef?.current ?? null, false, false);
812✔
1537

812✔
1538
    const selectionRef = React.useRef(selection);
812✔
1539
    selectionRef.current = selection;
812✔
1540
    const focusRef = React.useRef<HTMLElement | null>(null);
812✔
1541
    const focusElement = React.useCallback(
812✔
1542
        (el: HTMLElement | null) => {
812✔
1543
            // We don't want to steal the focus if we don't currently own the focus.
67✔
1544
            if (ref.current === null || !ref.current.contains(document.activeElement)) return;
67✔
1545
            if (el === null && selectionRef.current.current !== undefined) {
67✔
1546
                canvasRef?.current?.focus({
7✔
1547
                    preventScroll: true,
7✔
1548
                });
7✔
1549
            } else if (el !== null) {
67✔
1550
                el.focus({
27✔
1551
                    preventScroll: true,
27✔
1552
                });
27✔
1553
            }
27✔
1554
            focusRef.current = el;
34✔
1555
        },
67✔
1556
        [canvasRef]
812✔
1557
    );
812✔
1558

812✔
1559
    React.useImperativeHandle(
812✔
1560
        forwardedRef,
812✔
1561
        () => ({
812✔
1562
            focus: () => {
295✔
1563
                const el = focusRef.current;
39✔
1564
                // The element in the ref may have been removed however our callback method ref
39✔
1565
                // won't see the removal so bad things happen. Checking to see if the element is
39✔
1566
                // no longer attached is enough to resolve the problem. In the future this
39✔
1567
                // should be replaced with something much more robust.
39✔
1568
                if (el === null || !document.contains(el)) {
39✔
1569
                    canvasRef?.current?.focus({
33✔
1570
                        preventScroll: true,
33✔
1571
                    });
33✔
1572
                } else {
39✔
1573
                    el.focus({
6✔
1574
                        preventScroll: true,
6✔
1575
                    });
6✔
1576
                }
6✔
1577
            },
39✔
1578
            getBounds: (col?: number, row?: number) => {
295✔
1579
                if (canvasRef === undefined || canvasRef.current === null) {
46!
1580
                    return undefined;
×
1581
                }
×
1582

46✔
1583
                return getBoundsForItem(canvasRef.current, col ?? 0, row ?? -1);
46!
1584
            },
46✔
1585
            damage,
295✔
1586
        }),
295✔
1587
        [canvasRef, damage, getBoundsForItem]
812✔
1588
    );
812✔
1589

812✔
1590
    const lastFocusedSubdomNode = React.useRef<Item>();
812✔
1591

812✔
1592
    const accessibilityTree = useDebouncedMemo(
812✔
1593
        () => {
812✔
1594
            if (width < 50 || experimental?.disableAccessibilityTree === true) return null;
364✔
1595
            let effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX);
90✔
1596
            const colOffset = firstColAccessible ? 0 : -1;
364✔
1597
            if (!firstColAccessible && effectiveCols[0]?.sourceIndex === 0) {
364✔
1598
                effectiveCols = effectiveCols.slice(1);
6✔
1599
            }
6✔
1600

90✔
1601
            const [fCol, fRow] = selection.current?.cell ?? [];
364✔
1602
            const range = selection.current?.range;
364✔
1603

364✔
1604
            const visibleCols = effectiveCols.map(c => c.sourceIndex);
364✔
1605
            const visibleRows = makeRange(cellYOffset, Math.min(rows, cellYOffset + accessibilityHeight));
364✔
1606

364✔
1607
            // Maintain focus within grid if we own it but focused cell is outside visible viewport
364✔
1608
            // and not rendered.
364✔
1609
            if (
364✔
1610
                fCol !== undefined &&
364✔
1611
                fRow !== undefined &&
41✔
1612
                !(visibleCols.includes(fCol) && visibleRows.includes(fRow))
41✔
1613
            ) {
364✔
1614
                focusElement(null);
1✔
1615
            }
1✔
1616

90✔
1617
            return (
90✔
1618
                <table
90✔
1619
                    key="access-tree"
90✔
1620
                    role="grid"
90✔
1621
                    aria-rowcount={rows + 1}
90✔
1622
                    aria-multiselectable="true"
90✔
1623
                    aria-colcount={mappedColumns.length + colOffset}>
90✔
1624
                    <thead role="rowgroup">
90✔
1625
                        <tr role="row" aria-rowindex={1}>
90✔
1626
                            {effectiveCols.map(c => (
90✔
1627
                                <th
838✔
1628
                                    role="columnheader"
838✔
1629
                                    aria-selected={selection.columns.hasIndex(c.sourceIndex)}
838✔
1630
                                    aria-colindex={c.sourceIndex + 1 + colOffset}
838✔
1631
                                    tabIndex={-1}
838✔
1632
                                    onFocus={e => {
838✔
1633
                                        if (e.target === focusRef.current) return;
×
1634
                                        return onCellFocused?.([c.sourceIndex, -1]);
×
1635
                                    }}
×
1636
                                    key={c.sourceIndex}>
838✔
1637
                                    {c.title}
838✔
1638
                                </th>
838✔
1639
                            ))}
90✔
1640
                        </tr>
90✔
1641
                    </thead>
90✔
1642
                    <tbody role="rowgroup">
90✔
1643
                        {visibleRows.map(row => (
90✔
1644
                            <tr
2,966✔
1645
                                role="row"
2,966✔
1646
                                aria-selected={selection.rows.hasIndex(row)}
2,966✔
1647
                                key={row}
2,966✔
1648
                                aria-rowindex={row + 2}>
2,966✔
1649
                                {effectiveCols.map(c => {
2,966✔
1650
                                    const col = c.sourceIndex;
26,560✔
1651
                                    const key = `${col},${row}`;
26,560✔
1652
                                    const focused = fCol === col && fRow === row;
26,560✔
1653
                                    const selected =
26,560✔
1654
                                        range !== undefined &&
26,560✔
1655
                                        col >= range.x &&
12,620✔
1656
                                        col < range.x + range.width &&
11,006✔
1657
                                        row >= range.y &&
1,326✔
1658
                                        row < range.y + range.height;
1,167✔
1659
                                    const id = `glide-cell-${col}-${row}`;
26,560✔
1660
                                    const location: Item = [col, row];
26,560✔
1661
                                    const cellContent = getCellContent(location, true);
26,560✔
1662
                                    return (
26,560✔
1663
                                        <td
26,560✔
1664
                                            key={key}
26,560✔
1665
                                            role="gridcell"
26,560✔
1666
                                            aria-colindex={col + 1 + colOffset}
26,560✔
1667
                                            aria-selected={selected}
26,560✔
1668
                                            aria-readonly={
26,560✔
1669
                                                isInnerOnlyCell(cellContent) || !isReadWriteCell(cellContent)
26,560✔
1670
                                            }
26,560✔
1671
                                            id={id}
26,560✔
1672
                                            data-testid={id}
26,560✔
1673
                                            onClick={() => {
26,560✔
1674
                                                const canvas = canvasRef?.current;
1✔
1675
                                                if (canvas === null || canvas === undefined) return;
1!
1676
                                                return onKeyDown?.({
1✔
1677
                                                    bounds: getBoundsForItem(canvas, col, row),
1✔
1678
                                                    cancel: () => undefined,
1✔
1679
                                                    preventDefault: () => undefined,
1✔
1680
                                                    stopPropagation: () => undefined,
1✔
1681
                                                    ctrlKey: false,
1✔
1682
                                                    key: "Enter",
1✔
1683
                                                    keyCode: 13,
1✔
1684
                                                    metaKey: false,
1✔
1685
                                                    shiftKey: false,
1✔
1686
                                                    altKey: false,
1✔
1687
                                                    rawEvent: undefined,
1✔
1688
                                                    location,
1✔
1689
                                                });
1✔
1690
                                            }}
1✔
1691
                                            onFocusCapture={e => {
26,560✔
1692
                                                if (
31✔
1693
                                                    e.target === focusRef.current ||
31✔
1694
                                                    (lastFocusedSubdomNode.current?.[0] === col &&
28✔
1695
                                                        lastFocusedSubdomNode.current?.[1] === row)
4✔
1696
                                                )
31✔
1697
                                                    return;
31✔
1698
                                                lastFocusedSubdomNode.current = location;
28✔
1699
                                                return onCellFocused?.(location);
28✔
1700
                                            }}
31✔
1701
                                            ref={focused ? focusElement : undefined}
26,560✔
1702
                                            tabIndex={-1}>
26,560✔
1703
                                            {getRowData(cellContent, getCellRenderer)}
26,560✔
1704
                                        </td>
26,560✔
1705
                                    );
26,560✔
1706
                                })}
2,966✔
1707
                            </tr>
2,966✔
1708
                        ))}
90✔
1709
                    </tbody>
90✔
1710
                </table>
90✔
1711
            );
364✔
1712
        },
364✔
1713
        [
812✔
1714
            width,
812✔
1715
            mappedColumns,
812✔
1716
            cellXOffset,
812✔
1717
            dragAndDropState,
812✔
1718
            translateX,
812✔
1719
            rows,
812✔
1720
            cellYOffset,
812✔
1721
            accessibilityHeight,
812✔
1722
            selection,
812✔
1723
            focusElement,
812✔
1724
            getCellContent,
812✔
1725
            canvasRef,
812✔
1726
            onKeyDown,
812✔
1727
            getBoundsForItem,
812✔
1728
            onCellFocused,
812✔
1729
        ],
812✔
1730
        200
812✔
1731
    );
812✔
1732

812✔
1733
    const opacityX =
812✔
1734
        freezeColumns === 0 || !fixedShadowX ? 0 : cellXOffset > freezeColumns ? 1 : clamp(-translateX / 100, 0, 1);
812✔
1735

812✔
1736
    const absoluteOffsetY = -cellYOffset * 32 + translateY;
812✔
1737
    const opacityY = !fixedShadowY ? 0 : clamp(-absoluteOffsetY / 100, 0, 1);
812!
1738

812✔
1739
    const stickyShadow = React.useMemo(() => {
812✔
1740
        if (!opacityX && !opacityY) {
288✔
1741
            return null;
285✔
1742
        }
285✔
1743

3✔
1744
        const styleX: React.CSSProperties = {
3✔
1745
            position: "absolute",
3✔
1746
            top: 0,
3✔
1747
            left: stickyX,
3✔
1748
            width: width - stickyX,
3✔
1749
            height: height,
3✔
1750
            opacity: opacityX,
3✔
1751
            pointerEvents: "none",
3✔
1752
            transition: !smoothScrollX ? "opacity 0.2s" : undefined,
288!
1753
            boxShadow: "inset 13px 0 10px -13px rgba(0, 0, 0, 0.2)",
288✔
1754
        };
288✔
1755

288✔
1756
        const styleY: React.CSSProperties = {
288✔
1757
            position: "absolute",
288✔
1758
            top: totalHeaderHeight,
288✔
1759
            left: 0,
288✔
1760
            width: width,
288✔
1761
            height: height,
288✔
1762
            opacity: opacityY,
288✔
1763
            pointerEvents: "none",
288✔
1764
            transition: !smoothScrollY ? "opacity 0.2s" : undefined,
288!
1765
            boxShadow: "inset 0 13px 10px -13px rgba(0, 0, 0, 0.2)",
288✔
1766
        };
288✔
1767

288✔
1768
        return (
288✔
1769
            <>
288✔
1770
                {opacityX > 0 && <div id="shadow-x" style={styleX} />}
288✔
1771
                {opacityY > 0 && <div id="shadow-y" style={styleY} />}
288✔
1772
            </>
288✔
1773
        );
288✔
1774
    }, [opacityX, opacityY, stickyX, width, smoothScrollX, totalHeaderHeight, height, smoothScrollY]);
812✔
1775

812✔
1776
    const overlayStyle = React.useMemo<React.CSSProperties>(
812✔
1777
        () => ({
812✔
1778
            position: "absolute",
149✔
1779
            top: 0,
149✔
1780
            left: 0,
149✔
1781
        }),
149✔
1782
        []
812✔
1783
    );
812✔
1784

812✔
1785
    return (
812✔
1786
        <>
812✔
1787
            <canvas
812✔
1788
                data-testid="data-grid-canvas"
812✔
1789
                tabIndex={0}
812✔
1790
                onKeyDown={onKeyDownImpl}
812✔
1791
                onKeyUp={onKeyUpImpl}
812✔
1792
                onFocus={onCanvasFocused}
812✔
1793
                onBlur={onCanvasBlur}
812✔
1794
                ref={refImpl}
812✔
1795
                style={style}>
812✔
1796
                {accessibilityTree}
812✔
1797
            </canvas>
812✔
1798
            <canvas ref={overlayRef} style={overlayStyle} />
812✔
1799
            {stickyShadow}
812✔
1800
        </>
812✔
1801
    );
812✔
1802
};
812✔
1803

1✔
1804
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