@jielga/tmdatagrid 2.0.0-beta.8 → 2.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 (155) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1323 -796
  3. package/dist/index.js +4719 -3193
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +268 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +69 -78
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +83 -50
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +31 -23
  52. package/skills/editing/references/editors-and-validation.md +24 -17
  53. package/skills/filtering/SKILL.md +148 -40
  54. package/skills/getting-started/SKILL.md +17 -15
  55. package/skills/grouping/SKILL.md +31 -16
  56. package/skills/options/SKILL.md +8 -8
  57. package/skills/rows/SKILL.md +22 -18
  58. package/skills/server-side/SKILL.md +170 -17
  59. package/skills/testing/SKILL.md +150 -32
  60. package/skills/testing-components/SKILL.md +230 -0
  61. package/skills/testing-editing/SKILL.md +240 -0
  62. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  63. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  64. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  65. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
  66. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  67. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  68. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
  69. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  70. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
  71. package/src/components/TMDataGridEntryRows.tsx +354 -0
  72. package/src/components/TMDataGridExportPicker.module.css +77 -0
  73. package/src/components/TMDataGridExportPicker.tsx +234 -0
  74. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  75. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  76. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  77. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  78. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  80. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
  81. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  82. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  83. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  84. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  85. package/src/components/TMDataGridMenu.tsx +357 -0
  86. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
  87. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
  88. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
  89. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  90. package/src/components/TMDataGridToolbar.tsx +181 -0
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  96. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  97. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  98. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  99. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  100. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  101. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  102. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  103. package/src/components/filters/controlLayout.ts +32 -0
  104. package/src/components/filters/filterControlFor.ts +65 -0
  105. package/src/components/generatedColumns.tsx +187 -0
  106. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  107. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  108. package/src/components/useHideableColumns.ts +52 -0
  109. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  110. package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
  111. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  112. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  113. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  114. package/src/core/controlledStateSync.ts +108 -0
  115. package/src/core/deletedRows.ts +34 -0
  116. package/src/core/dom.ts +74 -0
  117. package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
  118. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  119. package/src/core/export.ts +704 -0
  120. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  121. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  122. package/src/core/filterSurface.ts +99 -0
  123. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  124. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  125. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  126. package/src/core/pageReset.ts +120 -0
  127. package/src/core/pagination.ts +81 -0
  128. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  129. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  130. package/src/{tmdatagrid/index.ts → index.ts} +70 -36
  131. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
  132. package/src/useTMDataGridExport.ts +78 -0
  133. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
  134. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  135. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  136. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  137. package/src/tmdatagrid/core/cellExport.ts +0 -320
  138. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  141. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  142. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  143. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  144. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  155. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -1,9 +1,9 @@
1
1
  import { ActionIcon, Group, Loader, Tooltip } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
- import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
3
+ import type { Row, RowData } from "@tanstack/react-table";
4
4
  import classes from "./TMDataGridTable.module.css";
5
5
  import {
6
- useCellControlTabIndex,
6
+ useBodyControlTabIndex,
7
7
  useTMDataGridContext,
8
8
  } from "../TMDataGridContext";
9
9
  import type { TMDataGridFeatures } from "../useTMDataGrid";
@@ -65,18 +65,22 @@ function RowStateIndicator({
65
65
  * | open | the mode's own controls: Save and Cancel | `editing.mode` |
66
66
  * | neither | the pencil (row mode) and the trash | `editing.mode` |
67
67
  *
68
- * A row error turns the Save red with the message in its tooltip - a
69
- * pathless `.refine()` has nowhere else to land, and a field error outlives
70
- * the editor that found it. A parked row never offers a save: it has had its
71
- * submit, and `TMDataGrid.DraftActions` is what sends it.
68
+ * A row error turns the Save - or an entry row's ✓ - red with the message in
69
+ * its tooltip: a pathless `.refine()` has nowhere else to land, and a field
70
+ * error outlives the editor that found it. A parked row never offers a save:
71
+ * it has had its submit, and `TMDataGrid.DraftActions` is what sends it.
72
+ *
73
+ * The lane's controls take the tab index every body control does - see
74
+ * useBodyControlTabIndex. The open row's save and cancel are the exception,
75
+ * see where they are built.
72
76
  */
73
- function EditLaneCell<TData extends RowData>({
77
+ export function EditLaneCell<TData extends RowData>({
74
78
  row,
75
79
  }: {
76
80
  row: Row<TMDataGridFeatures, TData>;
77
81
  }) {
78
82
  const { edit, features, labels } = useTMDataGridContext();
79
- const tabIndex = useCellControlTabIndex();
83
+ const tabIndex = useBodyControlTabIndex();
80
84
  const rowId = row.id;
81
85
  const isOpen = useSelector(edit.store, (state) =>
82
86
  state.openRowIds.includes(rowId),
@@ -182,10 +186,10 @@ function EditLaneCell<TData extends RowData>({
182
186
  }
183
187
  return (
184
188
  <Group gap={0} wrap="nowrap">
185
- <Tooltip label={labels.confirmNewRow} withArrow>
189
+ <Tooltip label={rowError ?? labels.confirmNewRow} withArrow>
186
190
  <ActionIcon
187
191
  variant="subtle"
188
- color="green"
192
+ color={hasErrors ? "red" : "green"}
189
193
  size="sm"
190
194
  tabIndex={tabIndex}
191
195
  aria-label={labels.confirmNewRow}
@@ -235,7 +239,7 @@ function EditLaneCell<TData extends RowData>({
235
239
  data-row-id={rowId}
236
240
  onClick={(event) => {
237
241
  event.stopPropagation();
238
- edit.deleteRow(rowId);
242
+ edit.restoreRow(rowId);
239
243
  }}
240
244
  >
241
245
  <RestoreIcon size={16} stroke={1.6} />
@@ -312,12 +316,15 @@ function EditLaneCell<TData extends RowData>({
312
316
  );
313
317
  }
314
318
 
319
+ // An open row is a form, and its save and cancel are the form's last two
320
+ // fields: Tab walks into them from the last editor. The closed lane's
321
+ // controls stay out of the tab order like every other control in a cell.
315
322
  const save = (
316
323
  <ActionIcon
317
324
  variant="subtle"
318
325
  color={hasErrors ? "red" : "green"}
319
326
  size="sm"
320
- tabIndex={tabIndex}
327
+ tabIndex={0}
321
328
  aria-label={labels.saveRow}
322
329
  data-dg-part="save-row"
323
330
  data-row-id={rowId}
@@ -332,21 +339,15 @@ function EditLaneCell<TData extends RowData>({
332
339
 
333
340
  return (
334
341
  <Group gap={0} wrap="nowrap">
335
- {rowError === null ? (
336
- <Tooltip label={labels.saveRow} withArrow>
337
- {save}
338
- </Tooltip>
339
- ) : (
340
- <Tooltip label={rowError} withArrow>
341
- {save}
342
- </Tooltip>
343
- )}
342
+ <Tooltip label={rowError ?? labels.saveRow} withArrow>
343
+ {save}
344
+ </Tooltip>
344
345
  <Tooltip label={labels.cancelRowEdit} withArrow>
345
346
  <ActionIcon
346
347
  variant="subtle"
347
348
  color="gray"
348
349
  size="sm"
349
- tabIndex={tabIndex}
350
+ tabIndex={0}
350
351
  aria-label={labels.cancelRowEdit}
351
352
  data-dg-part="cancel-row"
352
353
  data-row-id={rowId}
@@ -361,44 +362,3 @@ function EditLaneCell<TData extends RowData>({
361
362
  </Group>
362
363
  );
363
364
  }
364
-
365
- /**
366
- * The generated edit lane, appended and pinned right - the row's Save at the
367
- * end of the row under `mode: "row"`, the state marker and revert under
368
- * `editing.draft`, mirroring the checkbox lane's build on the left.
369
- */
370
- export function createEditColumn<TData extends RowData>(
371
- label = "Edit",
372
- /** A draft lane holds three controls where the rest hold two. */
373
- wide = false,
374
- ): ColumnDef<TMDataGridFeatures, TData, unknown> {
375
- const width = wide ? 88 : 64;
376
- return {
377
- id: EDIT_COLUMN_ID,
378
- meta: {
379
- label,
380
- align: "center",
381
- enableOrdering: false,
382
- },
383
- // Wide enough for the pair (or draft's trio) it holds while editing.
384
- size: width,
385
- minSize: width,
386
- maxSize: width,
387
- enableResizing: false,
388
- enableSorting: false,
389
- enableColumnFilter: false,
390
- enableGlobalFilter: false,
391
- // The row's Save, Cancel and Delete live here, so hiding the lane would
392
- // strand an open row with no way to commit or discard it. Same rule as the
393
- // checkbox lane: chrome the grid generates is not a user setting.
394
- enableHiding: false,
395
- // Structurally pinned to the right; not movable.
396
- enablePinning: false,
397
- header: () => null,
398
- cell: ({ row }) => <EditLaneCell row={row} />,
399
- // Group rows: same reasoning as the checkbox lane - without this the
400
- // cell renders blank on aggregated rows, but here blank is also correct,
401
- // so the aggregated cell renders the same (null for groups).
402
- aggregatedCell: ({ row }) => <EditLaneCell row={row} />,
403
- };
404
- }
@@ -0,0 +1,354 @@
1
+ import {
2
+ flexRender,
3
+ useTable,
4
+ type Cell,
5
+ type Column,
6
+ type Row,
7
+ } from "@tanstack/react-table";
8
+ import { useSelector } from "@tanstack/react-store";
9
+ import { shallow } from "@tanstack/store";
10
+ import { useLayoutEffect, useMemo, useRef } from "react";
11
+ import classes from "./TMDataGridTable.module.css";
12
+ import sticky from "./sticky.module.css";
13
+ import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
14
+ import {
15
+ getColumnAlign,
16
+ isColumnEditableForRow,
17
+ isControlColumn,
18
+ } from "../core/columnUtils";
19
+ import { draftCellContext } from "../core/draftCellContext";
20
+ import { getEditFieldName, type TMDataGridEditApi } from "../core/editEngine";
21
+ import { focusEditorContent } from "../core/editorFocus";
22
+ import {
23
+ tmDataGridFeatures,
24
+ type TMDataGridFeatures,
25
+ } from "../useTMDataGrid";
26
+ import { TMDataGridCellEditor } from "./TMDataGridCellEditor";
27
+ import { EDIT_COLUMN_ID } from "./TMDataGridEditColumn";
28
+ import type { TMDataGridColumnLayout } from "./TMDataGridTable";
29
+
30
+ type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
31
+ type EntryRow = Row<TMDataGridFeatures, TMDataGridRowData>;
32
+ type EntryCellApi = Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
33
+
34
+ /**
35
+ * Whether an entry cell edits. `edit.canEditCell` asks the main table for
36
+ * the row, and an entry row is precisely not there yet - so the structural
37
+ * half of the same rule is applied to the entry table's own row.
38
+ */
39
+ function isEntryCellEditable(
40
+ row: Row<TMDataGridFeatures, TMDataGridRowData>,
41
+ column: ErasedColumn,
42
+ edit: TMDataGridEditApi,
43
+ ): boolean {
44
+ if (!edit.isColumnEditable(column)) return false;
45
+ return isColumnEditableForRow(column, row);
46
+ }
47
+
48
+ /**
49
+ * One cell of an entry row: the lane's controls, an editor for a cell the
50
+ * open row edits, or a value through the column's own renderer. The invalid
51
+ * marker subscribes here, per cell, as a body cell's does - a refused ✓
52
+ * paints the cells its issues name and nothing else. No dirty marker:
53
+ * everything in an entry row is new.
54
+ */
55
+ function EntryCell({
56
+ entryRow,
57
+ column,
58
+ cell,
59
+ committed,
60
+ layout,
61
+ rowHeight,
62
+ pinnedZ,
63
+ }: {
64
+ entryRow: EntryRow;
65
+ column: ErasedColumn;
66
+ cell: EntryCellApi | undefined;
67
+ committed: boolean;
68
+ layout: TMDataGridColumnLayout;
69
+ rowHeight: number;
70
+ pinnedZ: string;
71
+ }) {
72
+ const { edit } = useTMDataGridContext();
73
+ const fieldName = getEditFieldName(column);
74
+ const isInvalid = useSelector(edit.store, (state) => {
75
+ if (fieldName === null) return false;
76
+ const projection = state.rows[entryRow.id];
77
+ return projection !== undefined && projection.errorFields.includes(fieldName);
78
+ });
79
+ const editable =
80
+ cell !== undefined && isEntryCellEditable(entryRow, column, edit);
81
+ // An open row's cells outside the editors follow the form as it is typed,
82
+ // as a body row's do: the column's renderer over a context reading the
83
+ // form's values - the whole row, so a computed cell repaints with any
84
+ // field. `undefined` for a committed row, whose values the entry table
85
+ // already holds as data, and for the lanes.
86
+ const draftValues = useSelector(edit.store, (state) =>
87
+ committed || isControlColumn(column.id)
88
+ ? undefined
89
+ : state.rows[entryRow.id]?.values,
90
+ );
91
+ return (
92
+ <div
93
+ role="cell"
94
+ data-column-id={column.id}
95
+ data-align={getColumnAlign(column)}
96
+ data-control-column={isControlColumn(column.id) || undefined}
97
+ data-invalid={isInvalid || undefined}
98
+ // A committed row re-opens where it is double-clicked, the same
99
+ // gesture a body cell answers - and, like a body cell, a cell that
100
+ // takes no edit answers nothing.
101
+ onDoubleClick={
102
+ committed && editable
103
+ ? () => edit.begin({ rowId: entryRow.id, columnId: column.id })
104
+ : undefined
105
+ }
106
+ className={[
107
+ classes.entryCell,
108
+ layout.isBoundary && layout.pinnedAt === "left" ? sticky.stickyLeft : "",
109
+ layout.isBoundary && layout.pinnedAt === "right"
110
+ ? sticky.stickyRight
111
+ : "",
112
+ ]
113
+ .filter(Boolean)
114
+ .join(" ")}
115
+ style={{
116
+ minHeight: rowHeight,
117
+ left: layout.pinnedAt === "left" ? layout.offset : undefined,
118
+ right: layout.pinnedAt === "right" ? layout.offset : undefined,
119
+ position: layout.pinnedAt ? "sticky" : undefined,
120
+ zIndex: layout.pinnedAt ? pinnedZ : undefined,
121
+ }}
122
+ >
123
+ {cell === undefined ? null : column.id === EDIT_COLUMN_ID ? (
124
+ // The lane's cell - the entry row's controls.
125
+ flexRender(cell.column.columnDef.cell, cell.getContext())
126
+ ) : !committed && editable ? (
127
+ <TMDataGridCellEditor
128
+ cell={cell}
129
+ row={entryRow}
130
+ takeSeedText={() => undefined}
131
+ onClose={() => {}}
132
+ inEntryBlock
133
+ />
134
+ ) : committed || !isControlColumn(column.id) ? (
135
+ // A value through the column's own renderer. Every cell of a row
136
+ // entered and awaiting Save all, over the draft the entry table was
137
+ // fed - and, while the row is open, every cell it does not open (a
138
+ // display column, `meta.edit.enabled` off), over the form as it is
139
+ // typed, as a body row in row mode shows them. Blank was how a
140
+ // calculated column or a button cell vanished the moment a
141
+ // committed row was reopened. The generated lanes stay blank while
142
+ // the row is open: a checkbox or a chevron there would act on the
143
+ // entry table's own state, which reaches nothing.
144
+ <span className={classes.cellContent}>
145
+ {flexRender(
146
+ cell.column.columnDef.cell,
147
+ draftValues !== undefined
148
+ ? draftCellContext(cell, draftValues)
149
+ : cell.getContext(),
150
+ )}
151
+ </span>
152
+ ) : null}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ /**
158
+ * The sticky entry block - one row of open editors per `edit.addRow()`,
159
+ * pinned under the header. The one place stickiness is genuinely required:
160
+ * an existing row that scrolls away has a place to scroll back to, a row
161
+ * being typed into does not exist anywhere else.
162
+ *
163
+ * A second, tiny table instance over the new rows' seed values - same
164
+ * columns, its own `useTable`, no sorting or filtering ever exercised - so
165
+ * these are real `Cell`s and the editor host, the typed editors and the
166
+ * validators all apply unchanged. The forms live in the same engine map as
167
+ * every other draft, keyed by their temporary ids.
168
+ *
169
+ * Enter commits the entry - `onRowAdd`, or a park under `editing.draft`,
170
+ * where `saveDrafts` reports it in `created`; Escape discards it. The edit
171
+ * lane, when present, offers the same pair as buttons.
172
+ */
173
+ export function TMDataGridEntryRows({
174
+ orderedColumns,
175
+ layoutFor,
176
+ rowHeight,
177
+ }: {
178
+ orderedColumns: ReadonlyArray<ErasedColumn>;
179
+ layoutFor: (columnId: string) => TMDataGridColumnLayout;
180
+ rowHeight: number;
181
+ }) {
182
+ const { table, edit, features } = useTMDataGridContext();
183
+ const sticky = features.editNewRowsSticky;
184
+ // A row being *typed* into is always here - it exists nowhere else to
185
+ // scroll back to. A committed row is a body row, sorted and filtered with
186
+ // the rest (the hook feeds it to the table as `data`), unless
187
+ // `newRowsSticky` keeps it pinned here until the save. Only the rows the
188
+ // block renders go into its table: after an import `newRows` runs to
189
+ // thousands, every one of them committed and in the body.
190
+ const newRows = useSelector(
191
+ edit.store,
192
+ (state) =>
193
+ sticky
194
+ ? state.newRows
195
+ : state.newRows.filter((newRow) => !newRow.committed),
196
+ { compare: shallow },
197
+ );
198
+ // What a committed row renders from. It has no form - its values are the
199
+ // draft store's - and this is the same snapshot the table feeds a
200
+ // committed row in the body.
201
+ const committedValues = useSelector(
202
+ edit.store,
203
+ (state) => state.committedValues,
204
+ );
205
+ // The reopen gesture: `begin` on a committed entry row flips it back to
206
+ // editors and names the cell double-clicked - where the caret goes.
207
+ const activeEntry = useSelector(edit.store, (state) =>
208
+ state.active !== null &&
209
+ state.newRows.some(
210
+ (newRow) => newRow.tempId === state.active?.rowId && !newRow.committed,
211
+ )
212
+ ? state.active
213
+ : null,
214
+ );
215
+ const blockRef = useRef<HTMLDivElement>(null);
216
+ /** The entry row the caret has already been placed in. */
217
+ const focusedTempIdRef = useRef<string | null>(null);
218
+ /** The reopen target (`rowId:columnId`) the caret has already landed in. */
219
+ const focusedActiveRef = useRef<string | null>(null);
220
+
221
+ /**
222
+ * The caret goes into a row the moment `edit.addRow()` opens it, landing in
223
+ * its first editable cell - the same placement the main table makes for a
224
+ * row opened by a gesture, and made here for the same reason: an editor is
225
+ * free not to focus itself, and then a new row appeared with the caret
226
+ * still outside it.
227
+ */
228
+ useLayoutEffect(() => {
229
+ const newest = newRows.at(-1)?.tempId;
230
+ if (newest === undefined) {
231
+ focusedTempIdRef.current = null;
232
+ return;
233
+ }
234
+ if (focusedTempIdRef.current === newest) return;
235
+ const block = blockRef.current;
236
+ if (block === null) return;
237
+ const editor = block.querySelector<HTMLElement>(
238
+ `[data-dg-part="editor"][data-row-id="${CSS.escape(newest)}"]`,
239
+ );
240
+ // Not mounted yet; the next render tries again, as the table's does.
241
+ if (editor === null) return;
242
+ focusedTempIdRef.current = newest;
243
+ focusEditorContent(editor);
244
+ });
245
+
246
+ /**
247
+ * The caret for a reopen: `begin` on a committed row re-arms its editors
248
+ * and the double-clicked cell's editor should hold the caret, not the
249
+ * row's first. Keyed separately from the add effect above - the row was
250
+ * already focused once when it was added.
251
+ */
252
+ useLayoutEffect(() => {
253
+ if (activeEntry === null) {
254
+ focusedActiveRef.current = null;
255
+ return;
256
+ }
257
+ const key = `${activeEntry.rowId}:${activeEntry.columnId ?? ""}`;
258
+ if (focusedActiveRef.current === key) return;
259
+ const block = blockRef.current;
260
+ if (block === null) return;
261
+ const editor = block.querySelector<HTMLElement>(
262
+ activeEntry.columnId !== null
263
+ ? `[data-dg-part="editor"][data-row-id="${CSS.escape(activeEntry.rowId)}"][data-column-id="${CSS.escape(activeEntry.columnId)}"]`
264
+ : `[data-dg-part="editor"][data-row-id="${CSS.escape(activeEntry.rowId)}"]`,
265
+ );
266
+ // Not mounted yet; the next render tries again, as the add effect does.
267
+ if (editor === null) return;
268
+ focusedActiveRef.current = key;
269
+ focusEditorContent(editor);
270
+ });
271
+
272
+ // What the rows render over. A committed row is data: its values are the
273
+ // draft store's, which stand through a reopen. An open row's cells read
274
+ // the form itself - the editors directly, the rest through EntryCell's
275
+ // draft overlay - so its entry here gives the row an identity and a
276
+ // fallback: the committed values it was reopened from, else the seed
277
+ // frozen at addRow (the form's `defaultValues`, which a reopen keeps at
278
+ // the seed and writes the committed values over).
279
+ const data = useMemo(
280
+ () =>
281
+ newRows.map(
282
+ ({ tempId }) =>
283
+ (committedValues[tempId] ??
284
+ edit.getForm(tempId)?.options.defaultValues ??
285
+ {}) as TMDataGridRowData,
286
+ ),
287
+ [newRows, committedValues, edit],
288
+ );
289
+
290
+ const entryTable = useTable({
291
+ features: tmDataGridFeatures,
292
+ columns: table.options.columns,
293
+ data,
294
+ getRowId: (_row, index) => newRows[index]?.tempId ?? String(index),
295
+ });
296
+
297
+ if (newRows.length === 0) return null;
298
+
299
+ const stickyRows = entryTable.getCoreRowModel().rows;
300
+ const committedById = new Map(
301
+ newRows.map((newRow) => [newRow.tempId, newRow.committed]),
302
+ );
303
+
304
+ const renderEntryRow = (entryRow: EntryRow, pinnedZ: string) => {
305
+ const cellsById = new Map(
306
+ entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
307
+ );
308
+ const committed = committedById.get(entryRow.id) === true;
309
+ return (
310
+ <div
311
+ key={entryRow.id}
312
+ role="row"
313
+ data-dg-part="entry-row"
314
+ data-row-id={entryRow.id}
315
+ data-new
316
+ data-committed={committed || undefined}
317
+ // The same marker body rows carry once committed, so one selector
318
+ // reaches everything parked in the draft store.
319
+ data-draft={committed || undefined}
320
+ className={classes.entryRow}
321
+ >
322
+ {orderedColumns.map((column) => (
323
+ <EntryCell
324
+ key={column.id}
325
+ entryRow={entryRow}
326
+ column={column}
327
+ cell={cellsById.get(column.id)}
328
+ committed={committed}
329
+ layout={layoutFor(column.id)}
330
+ rowHeight={rowHeight}
331
+ pinnedZ={pinnedZ}
332
+ />
333
+ ))}
334
+ </div>
335
+ );
336
+ };
337
+
338
+ return (
339
+ <>
340
+ {stickyRows.length > 0 && (
341
+ <div
342
+ ref={blockRef}
343
+ role="rowgroup"
344
+ data-dg-entry-block
345
+ className={classes.entryBlock}
346
+ >
347
+ {stickyRows.map((entryRow) =>
348
+ renderEntryRow(entryRow, "var(--dg-z-pinned-row-pinned-cell, 5)"),
349
+ )}
350
+ </div>
351
+ )}
352
+ </>
353
+ );
354
+ }
@@ -0,0 +1,77 @@
1
+ .title {
2
+ font-weight: 500;
3
+ }
4
+
5
+ .list {
6
+ display: flex;
7
+ flex-direction: column;
8
+ border: 1px solid var(--mantine-color-default-border);
9
+ border-radius: var(--mantine-radius-default);
10
+ overflow: hidden;
11
+ }
12
+
13
+ .listHeader {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ gap: var(--mantine-spacing-sm);
18
+ padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
19
+ background-color: var(--mantine-color-default-hover);
20
+ border-bottom: 1px solid var(--mantine-color-default-border);
21
+ }
22
+
23
+ .headerLabel {
24
+ font-weight: 500;
25
+ cursor: pointer;
26
+ }
27
+
28
+ .rows {
29
+ display: flex;
30
+ flex-direction: column;
31
+ padding: calc(var(--mantine-spacing-xs) / 2) 0;
32
+ }
33
+
34
+ /* Each column is one full-width row: the label stretches over the free space
35
+ so the whole row toggles the box, and the row highlights on hover like a
36
+ menu item. */
37
+ .row {
38
+ padding: 0 var(--mantine-spacing-sm);
39
+ }
40
+
41
+ .row:hover {
42
+ background-color: var(--mantine-color-default-hover);
43
+ }
44
+
45
+ .rowBody {
46
+ align-items: center;
47
+ }
48
+
49
+ .rowLabelWrapper {
50
+ flex: 1;
51
+ min-width: 0;
52
+ }
53
+
54
+ .rowLabel {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: space-between;
58
+ gap: var(--mantine-spacing-sm);
59
+ padding-block: calc(var(--mantine-spacing-xs) * 0.75);
60
+ cursor: pointer;
61
+ }
62
+
63
+ .rowText {
64
+ overflow: hidden;
65
+ text-overflow: ellipsis;
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .rowHint {
70
+ flex: none;
71
+ font-size: var(--mantine-font-size-xs);
72
+ color: var(--mantine-color-dimmed);
73
+ }
74
+
75
+ .empty {
76
+ padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
77
+ }