@lotics/ui 2.6.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +1 -15
  2. package/src/react_native.d.ts +2 -2
  3. package/src/cell_date.tsx +0 -30
  4. package/src/cell_date_format.test.ts +0 -32
  5. package/src/cell_date_format.ts +0 -73
  6. package/src/cell_number.test.ts +0 -42
  7. package/src/cell_number.tsx +0 -25
  8. package/src/cell_number_format.ts +0 -42
  9. package/src/cell_select.tsx +0 -68
  10. package/src/cell_text.tsx +0 -45
  11. package/src/grid/data_grid.tsx +0 -2003
  12. package/src/grid/data_grid_columns.test.ts +0 -72
  13. package/src/grid/data_grid_columns.ts +0 -30
  14. package/src/grid/data_grid_context.ts +0 -119
  15. package/src/grid/dispatch_safely.ts +0 -39
  16. package/src/grid/engine.module.css +0 -114
  17. package/src/grid/engine.tsx +0 -1042
  18. package/src/grid/helpers.ts +0 -205
  19. package/src/grid/layout.test.ts +0 -515
  20. package/src/grid/layout.ts +0 -425
  21. package/src/grid/recycling.test.ts +0 -236
  22. package/src/grid/recycling.ts +0 -172
  23. package/src/grid/row_cell.module.css +0 -105
  24. package/src/grid/row_cell.tsx +0 -313
  25. package/src/grid/search_highlight.ts +0 -71
  26. package/src/grid/select_cell.tsx +0 -58
  27. package/src/grid/select_group_summary_cell.tsx +0 -76
  28. package/src/grid/select_header_cell.tsx +0 -32
  29. package/src/grid/skeleton_row.module.css +0 -34
  30. package/src/grid/skeleton_row.tsx +0 -20
  31. package/src/grid/use_grid_groups.ts +0 -311
  32. package/src/grid/use_scroll_to_cell.ts +0 -135
  33. package/src/grid/use_virtual_grid.ts +0 -383
  34. package/src/grid/visibility.test.ts +0 -208
  35. package/src/grid/visibility.ts +0 -77
  36. package/src/kanban/constants.ts +0 -18
  37. package/src/kanban/default_renderers.tsx +0 -160
  38. package/src/kanban/drag_preview.tsx +0 -157
  39. package/src/kanban/index.ts +0 -13
  40. package/src/kanban/insert_card_zone.tsx +0 -135
  41. package/src/kanban/kanban_board.tsx +0 -635
  42. package/src/kanban/kanban_card.tsx +0 -321
  43. package/src/kanban/kanban_column.tsx +0 -499
  44. package/src/kanban/placeholders.tsx +0 -54
  45. package/src/kanban/types.ts +0 -116
@@ -1,1042 +0,0 @@
1
- /// <reference path="../react_native.d.ts" />
2
- // Force-load the react-native type augmentation file so consumers picking up
3
- // the grid via subpath imports (e.g. `@lotics/ui/grid/data_grid`) see the
4
- // web-only `hovered`/`outline`/`cursor` types — they otherwise wouldn't,
5
- // because TypeScript doesn't auto-include `.d.ts` files inside dependency
6
- // directories. The triple-slash reference is the documented mechanism for
7
- // "include this file as part of the compilation when this one is loaded".
8
- import styles from "./engine.module.css";
9
- import React, { memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
10
- import { GridGroup, GroupPathKey, RowPathKey, groupPathToKey, rowPathToKey } from "./layout";
11
- import { RecycledColumn } from "./recycling";
12
- import { useVirtualGrid, GridRef } from "./use_virtual_grid";
13
- import { SkeletonRow } from "./skeleton_row";
14
- import { StyleProp, ViewStyle } from "react-native";
15
-
16
- export interface GridProps<TRow = unknown> {
17
- selectedRows?: Set<RowPathKey>;
18
- collapsedRows?: GroupPathKey[] | null;
19
- /** Editing cell. Only passed if the row is being edited. */
20
- editingCell?: {
21
- rowKey: RowPathKey;
22
- column: number;
23
- } | null;
24
- height: number;
25
- width: number;
26
- footerHeight?: number;
27
- renderFooterCell?: (props: GridRenderFooterCellProps) => React.ReactNode;
28
- renderRowCell: (props: GridRenderRowCellProps<TRow>) => React.ReactNode;
29
- renderRow: (props: GridRenderRowProps<TRow>) => React.ReactNode;
30
- renderHeader?: (props: GridRenderHeaderProps) => React.ReactNode;
31
- renderFooter?: (props: GridRenderFooterProps) => React.ReactNode;
32
- headerHeight?: number;
33
- renderHeaderCell?: (props: GridRenderHeaderCellProps) => React.ReactNode;
34
- leafRowHeight: number;
35
- groups: GridGroup[];
36
- groupHeadingHeight?: number;
37
- groupSummaryHeight?: number;
38
- spacerHeight?: number;
39
- renderGroupSummaryCell?: (props: GridRenderGroupSummaryCellProps) => React.ReactNode;
40
- renderGroupSummaryRow?: (props: GridRenderGroupSummaryRowProps) => React.ReactNode;
41
- renderGroupHeading?: (props: GridRenderGroupHeadingProps) => React.ReactNode;
42
- /** Length of the array determines number of columns. Array values correspond to their width. */
43
- columns: number[];
44
- /** Number of columns to freeze. Default is 0. */
45
- frozenColumnCount?: number;
46
- /** Maximum width for frozen columns. If frozen columns exceed this, they will be scaled down proportionally. */
47
- maxFrozenColumnsWidth?: number;
48
- /** Map of rowKey to row data. Used to pass row data to renderRowCell. */
49
- rowData?: Map<RowPathKey, TRow>;
50
- /** Map of rowKey to row index for selection bounds checking. */
51
- rowKeyToIndex?: Map<RowPathKey, number>;
52
- /** Selection bounds - min row index (primitive for optimization) */
53
- selectionMinRowIndex?: number | null;
54
- /** Selection bounds - max row index (primitive for optimization) */
55
- selectionMaxRowIndex?: number | null;
56
- /** Selection bounds - min column (primitive for optimization) */
57
- selectionMinColumn?: number | null;
58
- /** Selection bounds - max column (primitive for optimization) */
59
- selectionMaxColumn?: number | null;
60
- /** Active cell row index (primitive for optimization) */
61
- selectionActiveRowIndex?: number | null;
62
- /** Active cell column (primitive for optimization) */
63
- selectionActiveColumn?: number | null;
64
- /** Fill drag source row index (for fill handle) */
65
- fillSourceRowIndex?: number | null;
66
- /** Fill drag target row index (for fill handle) */
67
- fillTargetRowIndex?: number | null;
68
- /** Fill drag column (for fill handle) */
69
- fillColumn?: number | null;
70
- /** Optional function to get background color for a row (used for conditional row coloring) */
71
- rowBackgroundColorGetter?: (row: TRow) => string | undefined;
72
- /** Fires when scroll reaches near the bottom of the content. Used for infinite scroll pagination. */
73
- onEndReached?: () => void;
74
- /** Fires when the visible row range changes. Used for viewport-driven page loading. */
75
- onVisibleRangeChange?: (rowRange: [number, number]) => void;
76
- /** Number of rows to render outside the visible area for smoother scrolling. Default is 0. */
77
- rowOverscan?: number;
78
- /** Number of columns to render outside the visible area for smoother scrolling. Default is 0. */
79
- columnOverscan?: number;
80
- style?: StyleProp<ViewStyle>;
81
- ref?: React.Ref<GridRef>;
82
- }
83
-
84
- export interface GridRenderGroupSummaryCellProps {
85
- groupKey: GroupPathKey;
86
- column: number;
87
- width: number;
88
- height: number;
89
- }
90
-
91
- export interface GridRenderGroupSummaryRowProps {
92
- groupKey: GroupPathKey;
93
- collapsed: boolean;
94
- children: React.ReactNode;
95
- }
96
-
97
- export interface GridRenderHeaderCellProps {
98
- column: number;
99
- width: number;
100
- height: number;
101
- }
102
-
103
- export interface GridRenderFooterCellProps {
104
- column: number;
105
- width: number;
106
- height: number;
107
- }
108
-
109
- export interface GridRenderRowCellProps<TRow = unknown> {
110
- rowKey: RowPathKey;
111
- row: TRow;
112
- column: number;
113
- frozen: boolean;
114
- editing: boolean;
115
- active: boolean;
116
- selected: boolean;
117
- inSelectedRow: boolean;
118
- /** Row index for fill operations */
119
- rowIndex: number;
120
- /** Whether this cell is the source of a fill operation */
121
- isFillSource: boolean;
122
- /** Whether this cell is in the fill range (target cells during drag) */
123
- inFillRange: boolean;
124
- /** Optional background color for the row (from conditional coloring) */
125
- rowBackgroundColor?: string;
126
- }
127
-
128
- export interface GridRenderGroupHeadingProps {
129
- groupKey: GroupPathKey;
130
- collapsed: boolean;
131
- children: React.ReactNode;
132
- }
133
-
134
- export interface GridRenderRowProps<TRow = unknown> {
135
- rowKey: RowPathKey;
136
- row: TRow;
137
- selected: boolean;
138
- children: React.ReactNode;
139
- }
140
-
141
- export interface GridRenderHeaderProps {
142
- children: React.ReactNode;
143
- }
144
-
145
- export interface GridRenderFooterProps {
146
- children: React.ReactNode;
147
- }
148
-
149
- export const Grid = memo(function Grid<TRow = unknown>(props: GridProps<TRow>) {
150
- const {
151
- selectedRows,
152
- collapsedRows = null,
153
- columns,
154
- frozenColumnCount = 0,
155
- maxFrozenColumnsWidth,
156
- groups,
157
- spacerHeight = 0,
158
- groupHeadingHeight = 0,
159
- groupSummaryHeight = 0,
160
- footerHeight = 0,
161
- headerHeight = 0,
162
- leafRowHeight,
163
- height,
164
- width,
165
- renderFooterCell,
166
- renderGroupHeading,
167
- renderGroupSummaryCell,
168
- renderGroupSummaryRow,
169
- renderRowCell,
170
- renderRow,
171
- renderFooter,
172
- renderHeader,
173
- renderHeaderCell,
174
- rowData,
175
- rowKeyToIndex,
176
- editingCell,
177
- selectionMinRowIndex = null,
178
- selectionMaxRowIndex = null,
179
- selectionMinColumn = null,
180
- selectionMaxColumn = null,
181
- selectionActiveRowIndex = null,
182
- selectionActiveColumn = null,
183
- fillSourceRowIndex = null,
184
- fillTargetRowIndex = null,
185
- fillColumn = null,
186
- rowBackgroundColorGetter,
187
- onEndReached,
188
- onVisibleRangeChange,
189
- rowOverscan = 5,
190
- columnOverscan = 2,
191
- ref,
192
- style,
193
- } = props;
194
-
195
- const {
196
- api,
197
- frozenColumns,
198
- scrollableColumns,
199
- recycledRows,
200
- layout,
201
- scrollViewRef,
202
- handleScroll,
203
- isGroupCollapsed,
204
- isScrolledX,
205
- } = useVirtualGrid({
206
- columns,
207
- frozenColumnCount,
208
- maxFrozenColumnsWidth,
209
- groups,
210
- groupHeadingHeight,
211
- groupSummaryHeight,
212
- leafRowHeight,
213
- spacerHeight,
214
- collapsedRows,
215
- width,
216
- height,
217
- headerHeight,
218
- footerHeight,
219
- onEndReached,
220
- onVisibleRangeChange,
221
- rowOverscan,
222
- columnOverscan,
223
- });
224
-
225
- useImperativeHandle(ref, () => api, [api]);
226
-
227
- const [scrollbarHeight, setScrollbarHeight] = useState(0);
228
-
229
- useEffect(() => {
230
- const element = scrollViewRef.current;
231
- if (!element) return;
232
-
233
- const measureScrollbar = () => {
234
- const height = element.offsetHeight - element.clientHeight;
235
- setScrollbarHeight(height);
236
- };
237
-
238
- measureScrollbar();
239
-
240
- const resizeObserver = new ResizeObserver(measureScrollbar);
241
- resizeObserver.observe(element);
242
-
243
- return () => resizeObserver.disconnect();
244
- }, [scrollViewRef]);
245
-
246
- const footerOffset = useMemo(
247
- () => layout.scrollViewHeight + layout.headerHeight - scrollbarHeight,
248
- [layout.scrollViewHeight, layout.headerHeight, scrollbarHeight],
249
- );
250
-
251
- return (
252
- <div
253
- ref={scrollViewRef}
254
- className={styles.root}
255
- onScroll={handleScroll}
256
- data-testid="grid-scroll-container"
257
- data-scrolled-x={isScrolledX || undefined}
258
- style={style as React.CSSProperties}
259
- >
260
- <div
261
- className={styles.content}
262
- style={{ width: layout.contentWidth, height: layout.totalHeight }}
263
- >
264
- {headerHeight > 0 && renderHeaderCell !== undefined && renderHeader !== undefined && (
265
- <Header
266
- height={headerHeight}
267
- width={layout.contentWidth}
268
- frozenColumns={frozenColumns.columns}
269
- frozenColumnsWidth={frozenColumns.width}
270
- scrollableColumns={scrollableColumns.columns}
271
- scrollableColumnsWidth={scrollableColumns.width}
272
- renderHeader={renderHeader}
273
- renderHeaderCell={renderHeaderCell}
274
- />
275
- )}
276
- <div className={styles.rows_wrapper} style={{ top: headerHeight }}>
277
- {recycledRows.map((recycledRow) => {
278
- switch (recycledRow.type) {
279
- case "row": {
280
- const rowKey = rowPathToKey(recycledRow.rowPath);
281
- const row = rowData?.get(rowKey);
282
- if (row === undefined) {
283
- return (
284
- <SkeletonRow
285
- key={recycledRow.key}
286
- y={recycledRow.y}
287
- height={recycledRow.height}
288
- width={layout.contentWidth}
289
- />
290
- );
291
- }
292
- const rowIndex = rowKeyToIndex?.get(rowKey);
293
- const selected = selectedRows?.has(rowKey) ?? false;
294
-
295
- // Check if this row is within selection bounds
296
- const rowInSelection =
297
- rowIndex !== undefined &&
298
- selectionMinRowIndex !== null &&
299
- selectionMaxRowIndex !== null &&
300
- rowIndex >= selectionMinRowIndex &&
301
- rowIndex <= selectionMaxRowIndex;
302
-
303
- // Check if this row contains the active cell
304
- const isActiveRow =
305
- rowIndex !== undefined &&
306
- selectionActiveRowIndex !== null &&
307
- rowIndex === selectionActiveRowIndex;
308
-
309
- // Only pass column selection props if this row needs them
310
- // This prevents re-renders of rows outside selection when selection changes
311
- const needsSelectionProps = rowInSelection || isActiveRow;
312
-
313
- // Compute fill range to check if this row is affected
314
- const fillMinRow =
315
- fillSourceRowIndex !== null && fillTargetRowIndex !== null
316
- ? Math.min(fillSourceRowIndex, fillTargetRowIndex)
317
- : null;
318
- const fillMaxRow =
319
- fillSourceRowIndex !== null && fillTargetRowIndex !== null
320
- ? Math.max(fillSourceRowIndex, fillTargetRowIndex)
321
- : null;
322
-
323
- // Check if this row needs fill props
324
- const needsFillProps =
325
- rowIndex !== undefined &&
326
- fillMinRow !== null &&
327
- fillMaxRow !== null &&
328
- rowIndex >= fillMinRow &&
329
- rowIndex <= fillMaxRow;
330
-
331
- return (
332
- <Row
333
- key={recycledRow.key}
334
- y={recycledRow.y}
335
- rowKey={rowKey}
336
- row={row}
337
- rowIndex={rowIndex ?? -1}
338
- selected={selected}
339
- editingCell={editingCell?.rowKey === rowKey ? editingCell : null}
340
- height={recycledRow.height}
341
- width={layout.contentWidth}
342
- frozenColumns={frozenColumns.columns}
343
- frozenColumnsWidth={frozenColumns.width}
344
- scrollableColumns={scrollableColumns.columns}
345
- scrollableColumnsWidth={scrollableColumns.width}
346
- rowInSelection={rowInSelection}
347
- isActiveRow={isActiveRow}
348
- selectionMinColumn={needsSelectionProps ? selectionMinColumn : null}
349
- selectionMaxColumn={needsSelectionProps ? selectionMaxColumn : null}
350
- selectionActiveColumn={needsSelectionProps ? selectionActiveColumn : null}
351
- fillSourceRowIndex={needsFillProps ? fillSourceRowIndex : null}
352
- fillTargetRowIndex={needsFillProps ? fillTargetRowIndex : null}
353
- fillColumn={needsFillProps ? fillColumn : null}
354
- rowBackgroundColor={row ? rowBackgroundColorGetter?.(row) : undefined}
355
- renderRow={renderRow}
356
- renderRowCell={renderRowCell}
357
- />
358
- );
359
- }
360
- case "group_heading": {
361
- if (renderGroupHeading === undefined) {
362
- return null;
363
- }
364
-
365
- return (
366
- <GroupHeadingRow
367
- key={recycledRow.key}
368
- y={recycledRow.y}
369
- groupKey={groupPathToKey(recycledRow.groupPath)}
370
- height={recycledRow.height}
371
- width={layout.contentWidth}
372
- renderGroupHeading={renderGroupHeading}
373
- collapsed={isGroupCollapsed(groupPathToKey(recycledRow.groupPath))}
374
- />
375
- );
376
- }
377
- case "group_summary": {
378
- if (renderGroupSummaryCell === undefined) {
379
- return null;
380
- }
381
-
382
- const groupKey = groupPathToKey(recycledRow.groupPath);
383
-
384
- return (
385
- <GroupSummaryRow
386
- key={recycledRow.key}
387
- y={recycledRow.y}
388
- groupKey={groupKey}
389
- height={recycledRow.height}
390
- width={layout.contentWidth}
391
- frozenColumns={frozenColumns.columns}
392
- frozenColumnsWidth={frozenColumns.width}
393
- scrollableColumns={scrollableColumns.columns}
394
- scrollableColumnsWidth={scrollableColumns.width}
395
- renderGroupSummaryCell={renderGroupSummaryCell}
396
- renderGroupSummaryRow={renderGroupSummaryRow}
397
- collapsed={isGroupCollapsed(groupKey)}
398
- />
399
- );
400
- }
401
- case "spacer":
402
- return (
403
- <SpacerRow key={recycledRow.key} y={recycledRow.y} height={recycledRow.height} />
404
- );
405
- }
406
- })}
407
- </div>
408
- {footerHeight > 0 && renderFooterCell !== undefined && renderFooter !== undefined && (
409
- <Footer
410
- y={footerOffset}
411
- height={footerHeight}
412
- width={layout.contentWidth}
413
- frozenColumns={frozenColumns.columns}
414
- frozenColumnsWidth={frozenColumns.width}
415
- scrollableColumns={scrollableColumns.columns}
416
- scrollableColumnsWidth={scrollableColumns.width}
417
- renderFooter={renderFooter}
418
- renderFooterCell={renderFooterCell}
419
- />
420
- )}
421
- </div>
422
- </div>
423
- );
424
- }) as <TRow = unknown>(props: GridProps<TRow>) => React.ReactNode;
425
-
426
- interface HeaderProps {
427
- height: number;
428
- width: number;
429
- frozenColumns: RecycledColumn[];
430
- frozenColumnsWidth: number;
431
- scrollableColumns: RecycledColumn[];
432
- scrollableColumnsWidth: number;
433
- renderHeader: (props: GridRenderHeaderProps) => React.ReactNode;
434
- renderHeaderCell: (props: GridRenderHeaderCellProps) => React.ReactNode;
435
- }
436
-
437
- const Header = memo(function Header(props: HeaderProps) {
438
- const {
439
- height,
440
- width,
441
- frozenColumns,
442
- frozenColumnsWidth,
443
- scrollableColumns,
444
- scrollableColumnsWidth,
445
- renderHeader,
446
- renderHeaderCell,
447
- } = props;
448
-
449
- const children = useMemo(
450
- () => (
451
- <>
452
- <div className={styles.frozen_cells} style={{ width: frozenColumnsWidth, height }}>
453
- {frozenColumns.map((columnData) => (
454
- <Cell key={columnData.key} height={height} width={columnData.width}>
455
- {renderHeaderCell({
456
- column: columnData.column,
457
- width: columnData.width,
458
- height,
459
- })}
460
- </Cell>
461
- ))}
462
- </div>
463
- <div className={styles.scrollable_cells} style={{ width: scrollableColumnsWidth, height }}>
464
- {scrollableColumns.map((columnData) => (
465
- <ScrollableCell
466
- key={columnData.key}
467
- x={columnData.x}
468
- height={height}
469
- width={columnData.width}
470
- >
471
- {renderHeaderCell({
472
- column: columnData.column,
473
- width: columnData.width,
474
- height,
475
- })}
476
- </ScrollableCell>
477
- ))}
478
- </div>
479
- </>
480
- ),
481
- [
482
- frozenColumns,
483
- frozenColumnsWidth,
484
- scrollableColumns,
485
- scrollableColumnsWidth,
486
- renderHeaderCell,
487
- height,
488
- ],
489
- );
490
-
491
- return (
492
- <div className={styles.header_wrapper} style={{ width, height }}>
493
- {renderHeader({ children })}
494
- </div>
495
- );
496
- });
497
-
498
- interface FooterProps {
499
- height: number;
500
- width: number;
501
- y: number;
502
- frozenColumns: RecycledColumn[];
503
- frozenColumnsWidth: number;
504
- scrollableColumns: RecycledColumn[];
505
- scrollableColumnsWidth: number;
506
- renderFooter: (props: GridRenderFooterProps) => React.ReactNode;
507
- renderFooterCell: (props: GridRenderFooterCellProps) => React.ReactNode;
508
- }
509
-
510
- const Footer = memo(function Footer(props: FooterProps) {
511
- const {
512
- y,
513
- height,
514
- width,
515
- frozenColumns,
516
- frozenColumnsWidth,
517
- scrollableColumns,
518
- scrollableColumnsWidth,
519
- renderFooterCell,
520
- renderFooter,
521
- } = props;
522
-
523
- const children = useMemo(
524
- () => (
525
- <>
526
- <div className={styles.frozen_cells} style={{ width: frozenColumnsWidth, height }}>
527
- {frozenColumns.map((columnData) => (
528
- <Cell key={columnData.key} height={height} width={columnData.width}>
529
- {renderFooterCell({
530
- column: columnData.column,
531
- width: columnData.width,
532
- height,
533
- })}
534
- </Cell>
535
- ))}
536
- </div>
537
- <div className={styles.scrollable_cells} style={{ width: scrollableColumnsWidth, height }}>
538
- {scrollableColumns.map((columnData) => (
539
- <ScrollableCell
540
- key={columnData.key}
541
- x={columnData.x}
542
- height={height}
543
- width={columnData.width}
544
- >
545
- {renderFooterCell({
546
- column: columnData.column,
547
- width: columnData.width,
548
- height,
549
- })}
550
- </ScrollableCell>
551
- ))}
552
- </div>
553
- </>
554
- ),
555
- [
556
- frozenColumns,
557
- frozenColumnsWidth,
558
- scrollableColumns,
559
- scrollableColumnsWidth,
560
- renderFooterCell,
561
- height,
562
- ],
563
- );
564
-
565
- return (
566
- <div data-testid="footer" className={styles.footer_wrapper} style={{ top: y, width, height }}>
567
- {renderFooter({ children })}
568
- </div>
569
- );
570
- });
571
-
572
- interface RowProps<TRow = unknown> {
573
- height: number;
574
- width: number;
575
- y: number;
576
- rowKey: RowPathKey;
577
- row: TRow | undefined;
578
- rowIndex: number;
579
- selected: boolean;
580
- /** Editing cell. Only passed if the row is being edited. */
581
- editingCell?: {
582
- rowKey: RowPathKey;
583
- column: number;
584
- } | null;
585
- frozenColumns: RecycledColumn[];
586
- frozenColumnsWidth: number;
587
- scrollableColumns: RecycledColumn[];
588
- scrollableColumnsWidth: number;
589
- /** Whether this row is within the selection range */
590
- rowInSelection: boolean;
591
- /** Whether this row contains the active cell */
592
- isActiveRow: boolean;
593
- /** Selection min column (for computing cell selection). Only passed if row is in selection. */
594
- selectionMinColumn: number | null;
595
- /** Selection max column (for computing cell selection). Only passed if row is in selection. */
596
- selectionMaxColumn: number | null;
597
- /** Active cell column. Only passed if row contains active cell. */
598
- selectionActiveColumn: number | null;
599
- /** Fill source row index. Only passed if this row is affected by fill. */
600
- fillSourceRowIndex: number | null;
601
- /** Fill target row index. Only passed if this row is affected by fill. */
602
- fillTargetRowIndex: number | null;
603
- /** Fill column. Only passed if this row is affected by fill. */
604
- fillColumn: number | null;
605
- /** Background color for the row (from conditional coloring) */
606
- rowBackgroundColor?: string;
607
- renderRowCell: (props: GridRenderRowCellProps<TRow>) => React.ReactNode;
608
- renderRow: (props: GridRenderRowProps<TRow>) => React.ReactNode;
609
- }
610
-
611
- const Row = memo(function Row<TRow = unknown>(props: RowProps<TRow>) {
612
- const {
613
- width,
614
- height,
615
- y,
616
- rowKey,
617
- row,
618
- rowIndex,
619
- selected,
620
- editingCell,
621
- frozenColumns,
622
- frozenColumnsWidth,
623
- scrollableColumns,
624
- scrollableColumnsWidth,
625
- rowInSelection,
626
- isActiveRow,
627
- selectionMinColumn,
628
- selectionMaxColumn,
629
- selectionActiveColumn,
630
- fillSourceRowIndex,
631
- fillTargetRowIndex,
632
- fillColumn,
633
- rowBackgroundColor,
634
- renderRow,
635
- renderRowCell,
636
- } = props;
637
-
638
- // Compute fill range for this row
639
- const fillMinRow =
640
- fillSourceRowIndex !== null && fillTargetRowIndex !== null
641
- ? Math.min(fillSourceRowIndex, fillTargetRowIndex)
642
- : null;
643
- const fillMaxRow =
644
- fillSourceRowIndex !== null && fillTargetRowIndex !== null
645
- ? Math.max(fillSourceRowIndex, fillTargetRowIndex)
646
- : null;
647
-
648
- const children = (
649
- <>
650
- <div className={styles.frozen_cells} style={{ width: frozenColumnsWidth, height }}>
651
- {frozenColumns.map((columnData) => {
652
- const col = columnData.column;
653
- const selectedCell =
654
- rowInSelection &&
655
- selectionMinColumn !== null &&
656
- selectionMaxColumn !== null &&
657
- col >= selectionMinColumn &&
658
- col <= selectionMaxColumn;
659
- const active = isActiveRow && selectionActiveColumn === col;
660
-
661
- const isEditing = editingCell?.rowKey === rowKey && editingCell.column === col;
662
-
663
- // Fill source: this cell is the origin of the fill
664
- const isFillSource = fillSourceRowIndex === rowIndex && fillColumn === col;
665
-
666
- // In fill range: this cell is being filled (excluding source)
667
- const inFillRange =
668
- fillColumn === col &&
669
- fillMinRow !== null &&
670
- fillMaxRow !== null &&
671
- rowIndex >= fillMinRow &&
672
- rowIndex <= fillMaxRow &&
673
- rowIndex !== fillSourceRowIndex;
674
-
675
- return (
676
- <FrozenCellWrapper
677
- key={columnData.key}
678
- rowKey={rowKey}
679
- row={row}
680
- rowIndex={rowIndex}
681
- column={col}
682
- editing={isEditing}
683
- cellWidth={columnData.width}
684
- cellHeight={height}
685
- active={active}
686
- selected={selectedCell}
687
- inSelectedRow={selected}
688
- isFillSource={isFillSource}
689
- inFillRange={inFillRange}
690
- rowBackgroundColor={rowBackgroundColor}
691
- renderRowCell={renderRowCell}
692
- />
693
- );
694
- })}
695
- </div>
696
- <div className={styles.scrollable_cells} style={{ width: scrollableColumnsWidth, height }}>
697
- {scrollableColumns.map((columnData) => {
698
- const col = columnData.column;
699
- const selectedCell =
700
- rowInSelection &&
701
- selectionMinColumn !== null &&
702
- selectionMaxColumn !== null &&
703
- col >= selectionMinColumn &&
704
- col <= selectionMaxColumn;
705
- const active = isActiveRow && selectionActiveColumn === col;
706
-
707
- const isEditing = editingCell?.rowKey === rowKey && editingCell.column === col;
708
-
709
- // Fill source: this cell is the origin of the fill
710
- const isFillSource = fillSourceRowIndex === rowIndex && fillColumn === col;
711
-
712
- // In fill range: this cell is being filled (excluding source)
713
- const inFillRange =
714
- fillColumn === col &&
715
- fillMinRow !== null &&
716
- fillMaxRow !== null &&
717
- rowIndex >= fillMinRow &&
718
- rowIndex <= fillMaxRow &&
719
- rowIndex !== fillSourceRowIndex;
720
-
721
- return (
722
- <ScrollableCellWrapper
723
- key={columnData.key}
724
- rowKey={rowKey}
725
- row={row}
726
- rowIndex={rowIndex}
727
- column={col}
728
- editing={isEditing}
729
- x={columnData.x}
730
- cellWidth={columnData.width}
731
- cellHeight={height}
732
- active={active}
733
- selected={selectedCell}
734
- inSelectedRow={selected}
735
- isFillSource={isFillSource}
736
- inFillRange={inFillRange}
737
- rowBackgroundColor={rowBackgroundColor}
738
- renderRowCell={renderRowCell}
739
- />
740
- );
741
- })}
742
- </div>
743
- </>
744
- );
745
-
746
- return (
747
- <div className={styles.row_wrapper} style={{ height, width, transform: `translateY(${y}px)` }}>
748
- {renderRow({ children, rowKey, row: row as TRow, selected })}
749
- </div>
750
- );
751
- }) as <TRow = unknown>(props: RowProps<TRow>) => React.ReactNode;
752
-
753
- /** Memoized cell wrapper - only re-renders when active/selected change */
754
- interface FrozenCellWrapperProps<TRow = unknown> {
755
- rowKey: RowPathKey;
756
- row: TRow | undefined;
757
- rowIndex: number;
758
- column: number;
759
- cellWidth: number;
760
- cellHeight: number;
761
- active: boolean;
762
- selected: boolean;
763
- inSelectedRow: boolean;
764
- editing: boolean;
765
- isFillSource: boolean;
766
- inFillRange: boolean;
767
- rowBackgroundColor?: string;
768
- renderRowCell: (props: GridRenderRowCellProps<TRow>) => React.ReactNode;
769
- }
770
-
771
- const FrozenCellWrapper = memo(function FrozenCellWrapper<TRow = unknown>(
772
- props: FrozenCellWrapperProps<TRow>,
773
- ) {
774
- const {
775
- rowKey,
776
- row,
777
- rowIndex,
778
- column,
779
- cellWidth,
780
- cellHeight,
781
- active,
782
- selected,
783
- inSelectedRow,
784
- editing,
785
- isFillSource,
786
- inFillRange,
787
- rowBackgroundColor,
788
- renderRowCell,
789
- } = props;
790
-
791
- return (
792
- <Cell height={cellHeight} width={cellWidth}>
793
- {renderRowCell({
794
- rowKey,
795
- row: row as TRow,
796
- rowIndex,
797
- column,
798
- frozen: true,
799
- inSelectedRow,
800
- active,
801
- selected,
802
- editing,
803
- isFillSource,
804
- inFillRange,
805
- rowBackgroundColor,
806
- })}
807
- </Cell>
808
- );
809
- }) as <TRow = unknown>(props: FrozenCellWrapperProps<TRow>) => React.ReactNode;
810
-
811
- interface ScrollableCellWrapperProps<TRow = unknown> {
812
- rowKey: RowPathKey;
813
- row: TRow | undefined;
814
- rowIndex: number;
815
- column: number;
816
- x: number;
817
- cellWidth: number;
818
- cellHeight: number;
819
- active: boolean;
820
- selected: boolean;
821
- inSelectedRow: boolean;
822
- editing: boolean;
823
- isFillSource: boolean;
824
- inFillRange: boolean;
825
- rowBackgroundColor?: string;
826
- renderRowCell: (props: GridRenderRowCellProps<TRow>) => React.ReactNode;
827
- }
828
-
829
- const ScrollableCellWrapper = memo(function ScrollableCellWrapper<TRow = unknown>(
830
- props: ScrollableCellWrapperProps<TRow>,
831
- ) {
832
- const {
833
- rowKey,
834
- row,
835
- rowIndex,
836
- column,
837
- x,
838
- cellWidth,
839
- cellHeight,
840
- active,
841
- selected,
842
- inSelectedRow,
843
- editing,
844
- isFillSource,
845
- inFillRange,
846
- rowBackgroundColor,
847
- renderRowCell,
848
- } = props;
849
-
850
- return (
851
- <ScrollableCell x={x} height={cellHeight} width={cellWidth}>
852
- {renderRowCell({
853
- rowKey,
854
- row: row as TRow,
855
- rowIndex,
856
- column,
857
- frozen: false,
858
- inSelectedRow,
859
- active,
860
- selected,
861
- editing,
862
- isFillSource,
863
- inFillRange,
864
- rowBackgroundColor,
865
- })}
866
- </ScrollableCell>
867
- );
868
- }) as <TRow = unknown>(props: ScrollableCellWrapperProps<TRow>) => React.ReactNode;
869
-
870
- interface GroupHeadingRowProps {
871
- height: number;
872
- width: number;
873
- y: number;
874
- groupKey: GroupPathKey;
875
- collapsed: boolean;
876
- renderGroupHeading: (props: GridRenderGroupHeadingProps) => React.ReactNode;
877
- }
878
-
879
- const GroupHeadingRow = memo(function GroupHeadingRow(props: GroupHeadingRowProps) {
880
- const { width, height, y, groupKey, collapsed, renderGroupHeading } = props;
881
-
882
- return (
883
- <div
884
- className={styles.group_heading_wrapper}
885
- style={{ height, width, transform: `translateY(${y}px)` }}
886
- >
887
- <div className={styles.group_heading_content} style={{ height }}>
888
- {renderGroupHeading({ groupKey, collapsed, children: null })}
889
- </div>
890
- </div>
891
- );
892
- }) as (props: GroupHeadingRowProps) => React.ReactNode;
893
-
894
- interface GroupSummaryRowProps {
895
- height: number;
896
- width: number;
897
- y: number;
898
- groupKey: GroupPathKey;
899
- collapsed: boolean;
900
- frozenColumns: RecycledColumn[];
901
- frozenColumnsWidth: number;
902
- scrollableColumns: RecycledColumn[];
903
- scrollableColumnsWidth: number;
904
- renderGroupSummaryCell: (props: GridRenderGroupSummaryCellProps) => React.ReactNode;
905
- renderGroupSummaryRow?: (props: GridRenderGroupSummaryRowProps) => React.ReactNode;
906
- }
907
-
908
- const GroupSummaryRow = memo(function GroupSummary(props: GroupSummaryRowProps) {
909
- const {
910
- width,
911
- height,
912
- y,
913
- groupKey,
914
- collapsed,
915
- frozenColumns,
916
- frozenColumnsWidth,
917
- scrollableColumns,
918
- scrollableColumnsWidth,
919
- renderGroupSummaryCell,
920
- renderGroupSummaryRow,
921
- } = props;
922
-
923
- const children = useMemo(
924
- () => (
925
- <>
926
- <div className={styles.frozen_cells} style={{ width: frozenColumnsWidth, height }}>
927
- {frozenColumns.map((columnData) => (
928
- <Cell key={columnData.key} height={height} width={columnData.width}>
929
- {renderGroupSummaryCell({
930
- groupKey,
931
- column: columnData.column,
932
- width: columnData.width,
933
- height,
934
- })}
935
- </Cell>
936
- ))}
937
- </div>
938
- <div className={styles.scrollable_cells} style={{ width: scrollableColumnsWidth, height }}>
939
- {scrollableColumns.map((columnData) => (
940
- <ScrollableCell
941
- key={columnData.key}
942
- x={columnData.x}
943
- height={height}
944
- width={columnData.width}
945
- >
946
- {renderGroupSummaryCell({
947
- groupKey,
948
- column: columnData.column,
949
- width: columnData.width,
950
- height,
951
- })}
952
- </ScrollableCell>
953
- ))}
954
- </div>
955
- </>
956
- ),
957
- [
958
- frozenColumns,
959
- frozenColumnsWidth,
960
- scrollableColumns,
961
- scrollableColumnsWidth,
962
- groupKey,
963
- height,
964
- renderGroupSummaryCell,
965
- ],
966
- );
967
-
968
- const rowContent = renderGroupSummaryRow ? (
969
- renderGroupSummaryRow({ groupKey, collapsed, children })
970
- ) : (
971
- <div
972
- style={{
973
- display: "flex",
974
- alignItems: "center",
975
- height: "100%",
976
- }}
977
- >
978
- {children}
979
- </div>
980
- );
981
-
982
- return (
983
- <div className={styles.row_wrapper} style={{ height, width, transform: `translateY(${y}px)` }}>
984
- {rowContent}
985
- </div>
986
- );
987
- }) as (props: GroupSummaryRowProps) => React.ReactNode;
988
-
989
- interface SpacerRowProps {
990
- y: number;
991
- height: number;
992
- }
993
-
994
- const SpacerRow = memo(function SpacerRow(props: SpacerRowProps) {
995
- const { height, y } = props;
996
-
997
- return <div className={styles.spacer} style={{ height, transform: `translateY(${y}px)` }} />;
998
- }) as (props: SpacerRowProps) => React.ReactNode;
999
-
1000
- interface CellProps {
1001
- width: number;
1002
- height: number;
1003
- children: React.ReactNode;
1004
- }
1005
-
1006
- const Cell = memo(function Cell(props: CellProps) {
1007
- const { width, height, children } = props;
1008
-
1009
- return (
1010
- <div className={styles.cell} style={{ width, height }}>
1011
- {children}
1012
- </div>
1013
- );
1014
- });
1015
-
1016
- interface ScrollableCellProps {
1017
- x: number;
1018
- width: number;
1019
- height: number;
1020
- children: React.ReactNode;
1021
- }
1022
-
1023
- const ScrollableCell = memo(function ScrollableCell(props: ScrollableCellProps) {
1024
- const { x, width, height, children } = props;
1025
-
1026
- return (
1027
- <div
1028
- className={styles.cell}
1029
- style={{
1030
- position: "absolute",
1031
- top: 0,
1032
- left: 0,
1033
- transform: `translateX(${x}px)`,
1034
- width,
1035
- height,
1036
- overflow: "hidden",
1037
- }}
1038
- >
1039
- {children}
1040
- </div>
1041
- );
1042
- });