@multiplatform.one/table 7.29.0 → 7.30.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 (44) hide show
  1. package/dist/cjs/components/DataTableCell/index.cjs +7 -0
  2. package/dist/cjs/components/DataTableCell/index.native.js +9 -0
  3. package/dist/cjs/components/DataTableCell/index.native.js.map +1 -1
  4. package/dist/cjs/components/KnobDialog.cjs +8 -9
  5. package/dist/cjs/components/KnobDialog.native.js +8 -9
  6. package/dist/cjs/components/KnobDialog.native.js.map +1 -1
  7. package/dist/cjs/components/TableInput/RowDetailEditor.cjs +79 -81
  8. package/dist/cjs/components/TableInput/index.cjs +139 -110
  9. package/dist/esm/components/DataTableCell/index.mjs +7 -0
  10. package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
  11. package/dist/esm/components/DataTableCell/index.native.js +9 -0
  12. package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
  13. package/dist/esm/components/KnobDialog.mjs +1 -2
  14. package/dist/esm/components/KnobDialog.mjs.map +1 -1
  15. package/dist/esm/components/KnobDialog.native.js +1 -2
  16. package/dist/esm/components/KnobDialog.native.js.map +1 -1
  17. package/dist/esm/components/TableInput/RowDetailEditor.mjs +79 -81
  18. package/dist/esm/components/TableInput/RowDetailEditor.mjs.map +1 -1
  19. package/dist/esm/components/TableInput/index.mjs +140 -111
  20. package/dist/esm/components/TableInput/index.mjs.map +1 -1
  21. package/dist/jsx/components/DataTableCell/index.mjs +7 -0
  22. package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
  23. package/dist/jsx/components/DataTableCell/index.native.js +9 -0
  24. package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
  25. package/dist/jsx/components/KnobDialog.mjs +1 -2
  26. package/dist/jsx/components/KnobDialog.mjs.map +1 -1
  27. package/dist/jsx/components/KnobDialog.native.js +8 -9
  28. package/dist/jsx/components/KnobDialog.native.js.map +1 -1
  29. package/dist/jsx/components/TableInput/RowDetailEditor.mjs +79 -81
  30. package/dist/jsx/components/TableInput/RowDetailEditor.mjs.map +1 -1
  31. package/dist/jsx/components/TableInput/index.mjs +140 -111
  32. package/dist/jsx/components/TableInput/index.mjs.map +1 -1
  33. package/package.json +12 -12
  34. package/src/components/ChildTable/childTable.rowDetail.spec.tsx +69 -0
  35. package/src/components/DataTableCell/cellDraftPolicy.spec.tsx +60 -0
  36. package/src/components/DataTableCell/index.tsx +10 -1
  37. package/src/components/KnobDialog.tsx +1 -2
  38. package/src/components/TableInput/RowDetailEditor.tsx +71 -70
  39. package/src/components/TableInput/index.tsx +136 -128
  40. package/src/components/TableInput/tableInput.cellIdentity.spec.tsx +196 -0
  41. package/types/components/KnobDialog.d.ts.map +1 -1
  42. package/types/components/TableInput/RowDetailEditor.d.ts +4 -0
  43. package/types/components/TableInput/RowDetailEditor.d.ts.map +1 -1
  44. package/types/components/TableInput/index.d.ts.map +1 -1
@@ -1107,12 +1107,19 @@ function DraftFieldEditor({
1107
1107
  const onCommitRef = useRef(onCommit);
1108
1108
  onCommitRef.current = onCommit;
1109
1109
  const blurTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
1110
+ const shownRef = useRef(committed);
1110
1111
 
1111
1112
  // External value updates (a commit landing, row edits) clear the draft so
1112
- // the field tracks the new committed value.
1113
+ // the field tracks the new committed value. A value the field did not emit
1114
+ // itself also remounts it: Stepper and PhoneInput only read defaultValue on
1115
+ // mount, and the grid keeps its cells mounted across row commits (MPO-475).
1113
1116
  if (!Object.is(prevCommitted, committed)) {
1114
1117
  setPrevCommitted(committed);
1115
1118
  if (draft) setDraft(null);
1119
+ if (!Object.is(shownRef.current, committed)) {
1120
+ shownRef.current = committed;
1121
+ setResetKey((key) => key + 1);
1122
+ }
1116
1123
  }
1117
1124
 
1118
1125
  const clearBlurTimer = useCallback(() => {
@@ -1144,6 +1151,7 @@ function DraftFieldEditor({
1144
1151
  setSessionActive(false);
1145
1152
  draftRef.current = null;
1146
1153
  setDraft(null);
1154
+ shownRef.current = committedRef.current;
1147
1155
  // Remount the field: uncontrolled editors and popover-internal state
1148
1156
  // (DateRange mid-pick) hold edits that no prop change can reset.
1149
1157
  setResetKey((key) => key + 1);
@@ -1151,6 +1159,7 @@ function DraftFieldEditor({
1151
1159
 
1152
1160
  const handleDraftChange = useCallback(
1153
1161
  (value: any) => {
1162
+ shownRef.current = value;
1154
1163
  if (mode === "change") {
1155
1164
  onCommitRef.current(value);
1156
1165
  return;
@@ -6,10 +6,9 @@
6
6
  * borderRadius, elevation, and sizing across the table package.
7
7
  */
8
8
 
9
- import { DialogShowFocus } from "@multiplatform.one/components";
9
+ import { Dialog, DialogShowFocus } from "@multiplatform.one/components";
10
10
  import { useResolvedKnobs } from "@multiplatform.one/theme";
11
11
  import type React from "react";
12
- import { Dialog } from "tamagui";
13
12
 
14
13
  export interface KnobDialogProps {
15
14
  open: boolean;
@@ -7,6 +7,10 @@
7
7
  * copy of the row seeded on open. "Done" and an outside click commit the
8
8
  * draft once; "Cancel" and Escape discard it. A read-only row opens for
9
9
  * viewing with a single "Close".
10
+ *
11
+ * The pencil is the dialog's `Dialog.Trigger`: tamagui's modal close returns
12
+ * focus to the trigger ref and nowhere else, so without it every close
13
+ * dropped focus on <body> (MPO-471).
10
14
  */
11
15
 
12
16
  import { DialogContent, DialogOverlay, ScrollView } from "@multiplatform.one/components";
@@ -99,75 +103,72 @@ export function RowDetailEditor<TData extends DataTableData>({
99
103
  const current = draft ?? row;
100
104
 
101
105
  return (
102
- <>
103
- <Button
104
- chromeless
105
- compact={compact}
106
- onPress={() => handleOpenChange(true)}
107
- hitSlop={knobProps.nestedControl.hitSlop}
108
- aria-label={triggerLabel}
109
- aria-haspopup="dialog"
110
- aria-expanded={draft !== null}
111
- >
112
- <PencilSimpleIcon size={16} />
113
- </Button>
114
- <Dialog open={draft !== null} onOpenChange={handleOpenChange} modal>
115
- <Dialog.Portal>
116
- <DialogOverlay opacity={0.5} />
117
- <DialogContent
118
- maxWidth={720}
119
- maxHeight="80vh"
120
- width="90vw"
121
- onEscapeKeyDown={() => {
122
- discardRef.current = true;
123
- }}
124
- >
125
- <Dialog.Title {...knobProps.heading}>
126
- {withInterp(t("Row {{row}}", { row: rowNumber }), { row: rowNumber })}
127
- </Dialog.Title>
128
- <ScrollView flexShrink={1} minHeight={0}>
129
- <TableCellContext.Provider value={{ inTableCell: false }}>
130
- <YStack {...knobProps.gapLg}>
131
- {columns.map((col) => {
132
- const columnId = columnKey(col);
133
- if (isCellHidden?.(current, columnId, rowIndex)) return null;
134
- const label = typeof col.header === "string" ? col.header : columnId;
135
- return (
136
- <YStack key={columnId} {...knobProps.gap}>
137
- <Text {...knobProps.label}>{label}</Text>
138
- <DataTableCell
139
- value={current[columnId as keyof TData]}
140
- onChange={(value) => setDraftValue(columnId, value)}
141
- field={(col.field || "input") as DataTableField}
142
- fieldProps={col.fieldProps as DataTableFieldProps | undefined}
143
- readOnly={readOnly}
144
- compact={false}
145
- columnId={columnId}
146
- columnLabel={label}
147
- rowIndex={rowIndex}
148
- rowData={current}
149
- />
150
- </YStack>
151
- );
152
- })}
153
- </YStack>
154
- </TableCellContext.Provider>
155
- </ScrollView>
156
- <XStack {...knobProps.gap} justifyContent="flex-end">
157
- {readOnly ? (
158
- <Button onPress={handleCancel}>{t("Close")}</Button>
159
- ) : (
160
- <>
161
- <Button onPress={handleCancel}>{t("Cancel")}</Button>
162
- <Button theme="accent" onPress={() => handleOpenChange(false)}>
163
- {t("Done")}
164
- </Button>
165
- </>
166
- )}
167
- </XStack>
168
- </DialogContent>
169
- </Dialog.Portal>
170
- </Dialog>
171
- </>
106
+ <Dialog open={draft !== null} onOpenChange={handleOpenChange} modal>
107
+ <Dialog.Trigger asChild>
108
+ <Button
109
+ chromeless
110
+ compact={compact}
111
+ hitSlop={knobProps.nestedControl.hitSlop}
112
+ aria-label={triggerLabel}
113
+ >
114
+ <PencilSimpleIcon size={16} />
115
+ </Button>
116
+ </Dialog.Trigger>
117
+ <Dialog.Portal>
118
+ <DialogOverlay opacity={0.5} />
119
+ <DialogContent
120
+ maxWidth={720}
121
+ maxHeight="80vh"
122
+ width="90vw"
123
+ onEscapeKeyDown={() => {
124
+ discardRef.current = true;
125
+ }}
126
+ >
127
+ <Dialog.Title {...knobProps.heading}>
128
+ {withInterp(t("Row {{row}}", { row: rowNumber }), { row: rowNumber })}
129
+ </Dialog.Title>
130
+ <ScrollView flexShrink={1} minHeight={0}>
131
+ <TableCellContext.Provider value={{ inTableCell: false }}>
132
+ <YStack {...knobProps.gapLg}>
133
+ {columns.map((col) => {
134
+ const columnId = columnKey(col);
135
+ if (isCellHidden?.(current, columnId, rowIndex)) return null;
136
+ const label = typeof col.header === "string" ? col.header : columnId;
137
+ return (
138
+ <YStack key={columnId} {...knobProps.gap}>
139
+ <Text {...knobProps.label}>{label}</Text>
140
+ <DataTableCell
141
+ value={current[columnId as keyof TData]}
142
+ onChange={(value) => setDraftValue(columnId, value)}
143
+ field={(col.field || "input") as DataTableField}
144
+ fieldProps={col.fieldProps as DataTableFieldProps | undefined}
145
+ readOnly={readOnly}
146
+ compact={false}
147
+ columnId={columnId}
148
+ columnLabel={label}
149
+ rowIndex={rowIndex}
150
+ rowData={current}
151
+ />
152
+ </YStack>
153
+ );
154
+ })}
155
+ </YStack>
156
+ </TableCellContext.Provider>
157
+ </ScrollView>
158
+ <XStack {...knobProps.gap} justifyContent="flex-end">
159
+ {readOnly ? (
160
+ <Button onPress={handleCancel}>{t("Close")}</Button>
161
+ ) : (
162
+ <>
163
+ <Button onPress={handleCancel}>{t("Cancel")}</Button>
164
+ <Button theme="accent" onPress={() => handleOpenChange(false)}>
165
+ {t("Done")}
166
+ </Button>
167
+ </>
168
+ )}
169
+ </XStack>
170
+ </DialogContent>
171
+ </Dialog.Portal>
172
+ </Dialog>
172
173
  );
173
174
  }
@@ -35,7 +35,13 @@ import {
35
35
  useResolvedValidators,
36
36
  type AnyFormApi,
37
37
  } from "@multiplatform.one/forms";
38
- import { flexRender, getCoreRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
38
+ import {
39
+ flexRender,
40
+ getCoreRowModel,
41
+ useReactTable,
42
+ type CellContext,
43
+ type ColumnDef,
44
+ } from "@tanstack/react-table";
39
45
  import { TrashIcon, PlusIcon } from "@phosphor-icons/react";
40
46
  import { useTranslation } from "react-i18next";
41
47
  import { Table, getTableCellPadding } from "@multiplatform.one/table-primitives";
@@ -53,6 +59,7 @@ import { RowDetailEditor } from "./RowDetailEditor";
53
59
  import {
54
60
  childRowAttrs,
55
61
  childTableKeyAction,
62
+ columnKey,
56
63
  generateRowId,
57
64
  isEmptyRows,
58
65
  SKELETON_ROW_COUNT,
@@ -61,6 +68,113 @@ import {
61
68
 
62
69
  const chromeColumnIds = new Set(["_reorder", "_rowNumber", "_detail", "_actions"]);
63
70
 
71
+ /**
72
+ * What the cell renderers read at render time. It rides on TanStack's table
73
+ * meta so the column definitions depend on the schema alone.
74
+ */
75
+ interface GridMeta<TData extends DataTableData> {
76
+ readOnly: boolean;
77
+ compact: boolean;
78
+ canDelete: boolean;
79
+ deleteColor: string;
80
+ knobProps: ReturnType<typeof useResolvedKnobs>["knobProps"];
81
+ sortable: ReturnType<typeof useSortableRows<TData>>;
82
+ detailColumns: DataTableColumn<TData>[];
83
+ isCellHidden?: ChildCellHidden<TData>;
84
+ t: (key: string, options?: Record<string, unknown>) => string;
85
+ changeCell: (rowIndex: number, columnId: string, value: unknown) => void;
86
+ deleteRow: (rowIndex: number) => void;
87
+ commitRowDetail: (rowIndex: number, next: TData, changed: string[]) => void;
88
+ }
89
+
90
+ type GridCellProps<TData extends DataTableData> = CellContext<TData, unknown>;
91
+
92
+ const gridMeta = <TData extends DataTableData>({ table }: GridCellProps<TData>) =>
93
+ table.options.meta as GridMeta<TData>;
94
+
95
+ // flexRender mounts each column's `cell` as a component, so these renderers
96
+ // live at module scope: a renderer created per render remounts every cell on
97
+ // a row commit, and the focused control goes with it (MPO-471, MPO-475).
98
+ function ReorderCell<TData extends DataTableData>(props: GridCellProps<TData>) {
99
+ const { row } = props;
100
+ const { sortable, t, compact } = gridMeta(props);
101
+ return (
102
+ <SortableHandle
103
+ label={sortable.labels[row.index] ?? t("Row {{row}}", { row: row.index + 1 })}
104
+ itemId={String(row.original.id ?? row.id)}
105
+ compact={compact}
106
+ onPointerDown={(e) => sortable.onHandlePointerDown(row.index, e)}
107
+ onKeyDown={(e) => sortable.onHandleKeyDown(row.index, e)}
108
+ />
109
+ );
110
+ }
111
+
112
+ function RowNumberCell<TData extends DataTableData>(props: GridCellProps<TData>) {
113
+ const { knobProps } = gridMeta(props);
114
+ return (
115
+ <Text color="$color10" fontSize={knobProps.label.fontSize}>
116
+ {props.row.index + 1}
117
+ </Text>
118
+ );
119
+ }
120
+
121
+ function DataCell<TData extends DataTableData>(props: GridCellProps<TData>) {
122
+ const { row, column } = props;
123
+ const { isCellHidden, readOnly, compact, changeCell } = gridMeta(props);
124
+ const columnId = column.id;
125
+ if (isCellHidden?.(row.original, columnId, row.index)) return null;
126
+ const col = column.columnDef as DataTableColumn<TData>;
127
+ return (
128
+ <DataTableCell
129
+ value={row.original[columnId as keyof TData]}
130
+ onChange={(newValue) => changeCell(row.index, columnId, newValue)}
131
+ field={(col.field || "input") as DataTableField}
132
+ fieldProps={col.fieldProps as DataTableFieldProps | undefined}
133
+ readOnly={readOnly}
134
+ compact={compact}
135
+ columnId={columnId}
136
+ columnLabel={typeof col.header === "string" ? col.header : undefined}
137
+ rowIndex={row.index}
138
+ rowData={row.original}
139
+ />
140
+ );
141
+ }
142
+
143
+ function DetailCell<TData extends DataTableData>(props: GridCellProps<TData>) {
144
+ const { row } = props;
145
+ const { detailColumns, readOnly, compact, isCellHidden, commitRowDetail } = gridMeta(props);
146
+ return (
147
+ <RowDetailEditor
148
+ row={row.original}
149
+ rowIndex={row.index}
150
+ columns={detailColumns}
151
+ readOnly={readOnly}
152
+ compact={compact}
153
+ isCellHidden={isCellHidden}
154
+ onCommit={(next, changed) => commitRowDetail(row.index, next, changed)}
155
+ />
156
+ );
157
+ }
158
+
159
+ function DeleteCell<TData extends DataTableData>(props: GridCellProps<TData>) {
160
+ const { row } = props;
161
+ const { canDelete, compact, knobProps, deleteColor, deleteRow, t } = gridMeta(props);
162
+ return (
163
+ <Button
164
+ chromeless
165
+ compact={compact}
166
+ onPress={() => deleteRow(row.index)}
167
+ disabled={!canDelete}
168
+ hitSlop={knobProps.nestedControl.hitSlop}
169
+ aria-label={withInterp(t("Delete row {{row}}", { row: row.index + 1 }), {
170
+ row: row.index + 1,
171
+ })}
172
+ >
173
+ <TrashIcon size={16} color={deleteColor} />
174
+ </Button>
175
+ );
176
+ }
177
+
64
178
  export interface TableInputProps<TData extends DataTableData = DataTableData> {
65
179
  /** Data array for the table (ignored in favor of form state in the form branch) */
66
180
  data?: TData[];
@@ -272,7 +386,6 @@ function TableInputInner<TData extends DataTableData>({
272
386
  const pendingFocusRef = useRef<string | null>(null);
273
387
  const gridRef = useRef<HTMLElement | null>(null);
274
388
  const reorderEnabled = enableReorder && !readOnly && !skeleton;
275
- const Detail = RowDetailEditor<TData>;
276
389
 
277
390
  const firstDataKey = useMemo(() => {
278
391
  const first = columns[0];
@@ -307,13 +420,6 @@ function TableInputInner<TData extends DataTableData>({
307
420
  rowSelector: `[${childRowAttrs.row}]`,
308
421
  });
309
422
 
310
- const handlePointerRef = useRef(sortable.onHandlePointerDown);
311
- handlePointerRef.current = sortable.onHandlePointerDown;
312
- const handleKeyRef = useRef(sortable.onHandleKeyDown);
313
- handleKeyRef.current = sortable.onHandleKeyDown;
314
- const labelsRef = useRef(sortable.labels);
315
- labelsRef.current = sortable.labels;
316
-
317
423
  const lastDataColumnId = useMemo(() => {
318
424
  const last = columns[columns.length - 1];
319
425
  if (!last) return null;
@@ -428,133 +534,34 @@ function TableInputInner<TData extends DataTableData>({
428
534
 
429
535
  const tableColumns = useMemo(() => {
430
536
  const cols: ColumnDef<TData, unknown>[] = [];
537
+ const chrome = (id: string, header: string, size: number, cell: ColumnDef<TData>["cell"]) =>
538
+ cols.push({ id, header, size, cell });
431
539
 
432
- if (reorderEnabled) {
433
- cols.push({
434
- id: "_reorder",
435
- header: "",
436
- size: chromePx + 16,
437
- cell: ({ row }) => (
438
- <SortableHandle
439
- label={labelsRef.current[row.index] ?? t("Row {{row}}", { row: row.index + 1 })}
440
- itemId={String((row.original as DataTableData).id ?? row.id)}
441
- compact={compact}
442
- onPointerDown={(e) => handlePointerRef.current(row.index, e)}
443
- onKeyDown={(e) => handleKeyRef.current(row.index, e)}
444
- />
445
- ),
446
- });
447
- }
448
-
449
- if (showRowNumbers) {
450
- cols.push({
451
- id: "_rowNumber",
452
- header: "#",
453
- size: Math.max(36, chromePx),
454
- cell: ({ row }) => (
455
- <Text color="$color10" fontSize={knobProps.label.fontSize}>
456
- {row.index + 1}
457
- </Text>
458
- ),
459
- });
460
- }
461
-
462
- columns.forEach((col) => {
463
- const columnId =
464
- typeof col.accessorKey === "string" ? col.accessorKey : col.id || String(col.accessorKey);
465
-
466
- cols.push({
467
- ...col,
468
- id: columnId,
469
- cell: ({ row }) => {
470
- if (isCellHidden?.(row.original, columnId, row.index)) return null;
471
- const value = row.original[columnId as keyof TData];
472
- const field = (col.field || "input") as DataTableField;
473
- const fieldProps = col.fieldProps as DataTableFieldProps | undefined;
474
-
475
- return (
476
- <DataTableCell
477
- value={value}
478
- onChange={(newValue) => handleCellChange(row.index, columnId, newValue)}
479
- field={field}
480
- fieldProps={fieldProps}
481
- readOnly={readOnly}
482
- compact={compact}
483
- columnId={columnId}
484
- columnLabel={typeof col.header === "string" ? col.header : undefined}
485
- rowIndex={row.index}
486
- rowData={row.original}
487
- />
488
- );
489
- },
490
- } as ColumnDef<TData, unknown>);
491
- });
492
-
493
- if (rowDetail) {
494
- cols.push({
495
- id: "_detail",
496
- header: "",
497
- size: chromePx + 16,
498
- cell: ({ row }) => (
499
- <Detail
500
- row={row.original}
501
- rowIndex={row.index}
502
- columns={detailColumns}
503
- readOnly={readOnly}
504
- compact={compact}
505
- isCellHidden={isCellHidden}
506
- onCommit={(next, changed) => handleRowDetailCommit(row.index, next, changed)}
507
- />
508
- ),
509
- });
510
- }
511
-
512
- if (showDeleteRow && !readOnly) {
513
- cols.push({
514
- id: "_actions",
515
- header: "",
516
- size: chromePx + 16,
517
- cell: ({ row }) => {
518
- const canDelete = !minRows || rows.length > minRows;
519
- return (
520
- <Button
521
- chromeless
522
- compact={compact}
523
- onPress={() => handleDeleteRow(row.index)}
524
- disabled={!canDelete}
525
- hitSlop={knobProps.nestedControl.hitSlop}
526
- aria-label={withInterp(t("Delete row {{row}}", { row: row.index + 1 }), {
527
- row: row.index + 1,
528
- })}
529
- >
530
- <TrashIcon size={16} color={theme.red10?.val ?? "currentColor"} />
531
- </Button>
532
- );
533
- },
534
- });
540
+ if (reorderEnabled) chrome("_reorder", "", chromePx + 16, ReorderCell<TData>);
541
+ if (showRowNumbers) chrome("_rowNumber", "#", Math.max(36, chromePx), RowNumberCell<TData>);
542
+ for (const col of columns) {
543
+ cols.push({ ...col, id: columnKey(col), cell: DataCell<TData> } as ColumnDef<TData, unknown>);
535
544
  }
545
+ if (rowDetail) chrome("_detail", "", chromePx + 16, DetailCell<TData>);
546
+ if (showDeleteRow && !readOnly) chrome("_actions", "", chromePx + 16, DeleteCell<TData>);
536
547
 
537
548
  return cols;
538
- }, [
539
- columns,
540
- showRowNumbers,
541
- showDeleteRow,
549
+ }, [columns, reorderEnabled, showRowNumbers, rowDetail, showDeleteRow, readOnly, chromePx]);
550
+
551
+ const meta: GridMeta<TData> = {
542
552
  readOnly,
543
- handleCellChange,
544
- handleDeleteRow,
545
- rows.length,
546
- minRows,
547
- t,
548
553
  compact,
549
- chromePx,
550
- knobProps.label.fontSize,
551
- knobProps.nestedControl.hitSlop,
552
- reorderEnabled,
553
- rowDetail,
554
+ canDelete: !minRows || rows.length > minRows,
555
+ deleteColor: theme.red10?.val ?? "currentColor",
556
+ knobProps,
557
+ sortable,
554
558
  detailColumns,
555
559
  isCellHidden,
556
- handleRowDetailCommit,
557
- ]);
560
+ t,
561
+ changeCell: handleCellChange,
562
+ deleteRow: handleDeleteRow,
563
+ commitRowDetail: handleRowDetailCommit,
564
+ };
558
565
 
559
566
  const table = useReactTable({
560
567
  data: rows,
@@ -563,6 +570,7 @@ function TableInputInner<TData extends DataTableData>({
563
570
  // Stable row identity: keeps in-cell editor drafts pinned to their data
564
571
  // row across deletes instead of following the positional index.
565
572
  getRowId: (row, index) => String((row as DataTableData).id ?? index),
573
+ meta,
566
574
  });
567
575
 
568
576
  // Empty state flips to flex so a single auto-width cell fills the row