@multiplatform.one/table 7.29.1 → 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.
- package/dist/cjs/components/DataTableCell/index.cjs +7 -0
- package/dist/cjs/components/DataTableCell/index.native.js +9 -0
- package/dist/cjs/components/DataTableCell/index.native.js.map +1 -1
- package/dist/cjs/components/KnobDialog.cjs +8 -9
- package/dist/cjs/components/KnobDialog.native.js +8 -9
- package/dist/cjs/components/KnobDialog.native.js.map +1 -1
- package/dist/cjs/components/TableInput/index.cjs +139 -111
- package/dist/esm/components/DataTableCell/index.mjs +7 -0
- package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
- package/dist/esm/components/DataTableCell/index.native.js +9 -0
- package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
- package/dist/esm/components/KnobDialog.mjs +1 -2
- package/dist/esm/components/KnobDialog.mjs.map +1 -1
- package/dist/esm/components/KnobDialog.native.js +1 -2
- package/dist/esm/components/KnobDialog.native.js.map +1 -1
- package/dist/esm/components/TableInput/index.mjs +140 -112
- package/dist/esm/components/TableInput/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableCell/index.mjs +7 -0
- package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableCell/index.native.js +9 -0
- package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
- package/dist/jsx/components/KnobDialog.mjs +1 -2
- package/dist/jsx/components/KnobDialog.mjs.map +1 -1
- package/dist/jsx/components/KnobDialog.native.js +8 -9
- package/dist/jsx/components/KnobDialog.native.js.map +1 -1
- package/dist/jsx/components/TableInput/index.mjs +140 -112
- package/dist/jsx/components/TableInput/index.mjs.map +1 -1
- package/package.json +12 -12
- package/src/components/DataTableCell/cellDraftPolicy.spec.tsx +60 -0
- package/src/components/DataTableCell/index.tsx +10 -1
- package/src/components/KnobDialog.tsx +1 -2
- package/src/components/TableInput/index.tsx +137 -138
- package/src/components/TableInput/tableInput.cellIdentity.spec.tsx +196 -0
- package/types/components/KnobDialog.d.ts.map +1 -1
- package/types/components/TableInput/index.d.ts.map +1 -1
|
@@ -35,7 +35,13 @@ import {
|
|
|
35
35
|
useResolvedValidators,
|
|
36
36
|
type AnyFormApi,
|
|
37
37
|
} from "@multiplatform.one/forms";
|
|
38
|
-
import {
|
|
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;
|
|
@@ -363,34 +469,6 @@ function TableInputInner<TData extends DataTableData>({
|
|
|
363
469
|
},
|
|
364
470
|
[rows, commit, onCellChange],
|
|
365
471
|
);
|
|
366
|
-
const rowDetailCommitRef = useRef(handleRowDetailCommit);
|
|
367
|
-
rowDetailCommitRef.current = handleRowDetailCommit;
|
|
368
|
-
|
|
369
|
-
// flexRender mounts each `cell` as a component, so a fresh renderer on every
|
|
370
|
-
// row commit remounted the pencil under its closing dialog, and the dialog
|
|
371
|
-
// then had no connected trigger to return focus to (MPO-471).
|
|
372
|
-
const detailColumn = useMemo<ColumnDef<TData, unknown> | null>(
|
|
373
|
-
() =>
|
|
374
|
-
rowDetail
|
|
375
|
-
? {
|
|
376
|
-
id: "_detail",
|
|
377
|
-
header: "",
|
|
378
|
-
size: chromePx + 16,
|
|
379
|
-
cell: ({ row }) => (
|
|
380
|
-
<Detail
|
|
381
|
-
row={row.original}
|
|
382
|
-
rowIndex={row.index}
|
|
383
|
-
columns={detailColumns}
|
|
384
|
-
readOnly={readOnly}
|
|
385
|
-
compact={compact}
|
|
386
|
-
isCellHidden={isCellHidden}
|
|
387
|
-
onCommit={(next, changed) => rowDetailCommitRef.current(row.index, next, changed)}
|
|
388
|
-
/>
|
|
389
|
-
),
|
|
390
|
-
}
|
|
391
|
-
: null,
|
|
392
|
-
[rowDetail, chromePx, detailColumns, readOnly, compact, isCellHidden],
|
|
393
|
-
);
|
|
394
472
|
|
|
395
473
|
useEffect(() => {
|
|
396
474
|
const id = pendingFocusRef.current;
|
|
@@ -456,114 +534,34 @@ function TableInputInner<TData extends DataTableData>({
|
|
|
456
534
|
|
|
457
535
|
const tableColumns = useMemo(() => {
|
|
458
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 });
|
|
459
539
|
|
|
460
|
-
if (reorderEnabled)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
size: chromePx + 16,
|
|
465
|
-
cell: ({ row }) => (
|
|
466
|
-
<SortableHandle
|
|
467
|
-
label={labelsRef.current[row.index] ?? t("Row {{row}}", { row: row.index + 1 })}
|
|
468
|
-
itemId={String((row.original as DataTableData).id ?? row.id)}
|
|
469
|
-
compact={compact}
|
|
470
|
-
onPointerDown={(e) => handlePointerRef.current(row.index, e)}
|
|
471
|
-
onKeyDown={(e) => handleKeyRef.current(row.index, e)}
|
|
472
|
-
/>
|
|
473
|
-
),
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (showRowNumbers) {
|
|
478
|
-
cols.push({
|
|
479
|
-
id: "_rowNumber",
|
|
480
|
-
header: "#",
|
|
481
|
-
size: Math.max(36, chromePx),
|
|
482
|
-
cell: ({ row }) => (
|
|
483
|
-
<Text color="$color10" fontSize={knobProps.label.fontSize}>
|
|
484
|
-
{row.index + 1}
|
|
485
|
-
</Text>
|
|
486
|
-
),
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
columns.forEach((col) => {
|
|
491
|
-
const columnId =
|
|
492
|
-
typeof col.accessorKey === "string" ? col.accessorKey : col.id || String(col.accessorKey);
|
|
493
|
-
|
|
494
|
-
cols.push({
|
|
495
|
-
...col,
|
|
496
|
-
id: columnId,
|
|
497
|
-
cell: ({ row }) => {
|
|
498
|
-
if (isCellHidden?.(row.original, columnId, row.index)) return null;
|
|
499
|
-
const value = row.original[columnId as keyof TData];
|
|
500
|
-
const field = (col.field || "input") as DataTableField;
|
|
501
|
-
const fieldProps = col.fieldProps as DataTableFieldProps | undefined;
|
|
502
|
-
|
|
503
|
-
return (
|
|
504
|
-
<DataTableCell
|
|
505
|
-
value={value}
|
|
506
|
-
onChange={(newValue) => handleCellChange(row.index, columnId, newValue)}
|
|
507
|
-
field={field}
|
|
508
|
-
fieldProps={fieldProps}
|
|
509
|
-
readOnly={readOnly}
|
|
510
|
-
compact={compact}
|
|
511
|
-
columnId={columnId}
|
|
512
|
-
columnLabel={typeof col.header === "string" ? col.header : undefined}
|
|
513
|
-
rowIndex={row.index}
|
|
514
|
-
rowData={row.original}
|
|
515
|
-
/>
|
|
516
|
-
);
|
|
517
|
-
},
|
|
518
|
-
} as ColumnDef<TData, unknown>);
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
if (detailColumn) cols.push(detailColumn);
|
|
522
|
-
|
|
523
|
-
if (showDeleteRow && !readOnly) {
|
|
524
|
-
cols.push({
|
|
525
|
-
id: "_actions",
|
|
526
|
-
header: "",
|
|
527
|
-
size: chromePx + 16,
|
|
528
|
-
cell: ({ row }) => {
|
|
529
|
-
const canDelete = !minRows || rows.length > minRows;
|
|
530
|
-
return (
|
|
531
|
-
<Button
|
|
532
|
-
chromeless
|
|
533
|
-
compact={compact}
|
|
534
|
-
onPress={() => handleDeleteRow(row.index)}
|
|
535
|
-
disabled={!canDelete}
|
|
536
|
-
hitSlop={knobProps.nestedControl.hitSlop}
|
|
537
|
-
aria-label={withInterp(t("Delete row {{row}}", { row: row.index + 1 }), {
|
|
538
|
-
row: row.index + 1,
|
|
539
|
-
})}
|
|
540
|
-
>
|
|
541
|
-
<TrashIcon size={16} color={theme.red10?.val ?? "currentColor"} />
|
|
542
|
-
</Button>
|
|
543
|
-
);
|
|
544
|
-
},
|
|
545
|
-
});
|
|
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>);
|
|
546
544
|
}
|
|
545
|
+
if (rowDetail) chrome("_detail", "", chromePx + 16, DetailCell<TData>);
|
|
546
|
+
if (showDeleteRow && !readOnly) chrome("_actions", "", chromePx + 16, DeleteCell<TData>);
|
|
547
547
|
|
|
548
548
|
return cols;
|
|
549
|
-
}, [
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
showDeleteRow,
|
|
549
|
+
}, [columns, reorderEnabled, showRowNumbers, rowDetail, showDeleteRow, readOnly, chromePx]);
|
|
550
|
+
|
|
551
|
+
const meta: GridMeta<TData> = {
|
|
553
552
|
readOnly,
|
|
554
|
-
handleCellChange,
|
|
555
|
-
handleDeleteRow,
|
|
556
|
-
rows.length,
|
|
557
|
-
minRows,
|
|
558
|
-
t,
|
|
559
553
|
compact,
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
knobProps
|
|
563
|
-
|
|
564
|
-
|
|
554
|
+
canDelete: !minRows || rows.length > minRows,
|
|
555
|
+
deleteColor: theme.red10?.val ?? "currentColor",
|
|
556
|
+
knobProps,
|
|
557
|
+
sortable,
|
|
558
|
+
detailColumns,
|
|
565
559
|
isCellHidden,
|
|
566
|
-
|
|
560
|
+
t,
|
|
561
|
+
changeCell: handleCellChange,
|
|
562
|
+
deleteRow: handleDeleteRow,
|
|
563
|
+
commitRowDetail: handleRowDetailCommit,
|
|
564
|
+
};
|
|
567
565
|
|
|
568
566
|
const table = useReactTable({
|
|
569
567
|
data: rows,
|
|
@@ -572,6 +570,7 @@ function TableInputInner<TData extends DataTableData>({
|
|
|
572
570
|
// Stable row identity: keeps in-cell editor drafts pinned to their data
|
|
573
571
|
// row across deletes instead of following the positional index.
|
|
574
572
|
getRowId: (row, index) => String((row as DataTableData).id ?? index),
|
|
573
|
+
meta,
|
|
575
574
|
});
|
|
576
575
|
|
|
577
576
|
// Empty state flips to flex so a single auto-width cell fills the row
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A row commit re-renders the grid's cells in place (MPO-475). TanStack's
|
|
3
|
+
* flexRender mounts each column's `cell` as a component, so a renderer that
|
|
4
|
+
* changes identity on a commit remounts every cell, and focus that a Tab or
|
|
5
|
+
* Enter just placed in the grid lands on a detached node.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
10
|
+
import { useForm } from "@multiplatform.one/forms";
|
|
11
|
+
import { act, fireEvent, screen, waitFor, within } from "@testing-library/react";
|
|
12
|
+
import userEvent from "@testing-library/user-event";
|
|
13
|
+
import { describe, expect, it, vi } from "vitest";
|
|
14
|
+
import type { DataTableColumn, DataTableData } from "../../types";
|
|
15
|
+
import { ChildTable } from "../ChildTable";
|
|
16
|
+
import { TableInput } from "./index";
|
|
17
|
+
|
|
18
|
+
interface Line extends DataTableData {
|
|
19
|
+
id: string;
|
|
20
|
+
item: string;
|
|
21
|
+
qty: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const initial = (): Line[] => [
|
|
25
|
+
{ id: "a", item: "Bracket", qty: 12 },
|
|
26
|
+
{ id: "b", item: "Bolts", qty: 3 },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const columns: DataTableColumn<Line>[] = [
|
|
30
|
+
{ accessorKey: "item", header: "Item", field: "input" },
|
|
31
|
+
{ accessorKey: "qty", header: "Qty", field: "stepper", fieldProps: { min: 0 } },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function Harness({
|
|
35
|
+
onChange,
|
|
36
|
+
freshSchema = false,
|
|
37
|
+
}: {
|
|
38
|
+
onChange?: (rows: Line[]) => void;
|
|
39
|
+
freshSchema?: boolean;
|
|
40
|
+
}) {
|
|
41
|
+
const [rows, setRows] = React.useState(initial);
|
|
42
|
+
return (
|
|
43
|
+
<ChildTable<Line>
|
|
44
|
+
value={rows}
|
|
45
|
+
columns={freshSchema ? columns.map((col) => ({ ...col })) : columns}
|
|
46
|
+
isCellHidden={freshSchema ? () => false : undefined}
|
|
47
|
+
rowDetail
|
|
48
|
+
onChange={(next) => {
|
|
49
|
+
setRows(next);
|
|
50
|
+
onChange?.(next);
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function FormHarness({ onChange }: { onChange?: (rows: Line[]) => void }) {
|
|
57
|
+
const form = useForm({ defaultValues: { lines: initial() } });
|
|
58
|
+
return (
|
|
59
|
+
<TableInput<Line>
|
|
60
|
+
form={form as any}
|
|
61
|
+
name="lines"
|
|
62
|
+
columns={columns}
|
|
63
|
+
rowDetail
|
|
64
|
+
onChange={onChange}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cell(rowIndex: number, label: string) {
|
|
70
|
+
const row = document.querySelector<HTMLElement>(`[data-child-row-index="${rowIndex}"]`);
|
|
71
|
+
if (!row) throw new Error(`no grid row ${rowIndex}`);
|
|
72
|
+
return within(row).getByLabelText(label) as HTMLInputElement;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function gridControls() {
|
|
76
|
+
return [cell(0, "Item"), cell(0, "Qty"), cell(1, "Item"), cell(1, "Qty")];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function expectConnected(nodes: HTMLElement[]) {
|
|
80
|
+
expect(nodes.map((node) => node.isConnected)).toEqual(nodes.map(() => true));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const flushBlurTimer = () => act(() => new Promise<void>((resolve) => setTimeout(resolve, 15)));
|
|
84
|
+
|
|
85
|
+
describe("TableInput cells survive a row commit (MPO-475)", () => {
|
|
86
|
+
it("Enter commits an edit and focus stays on the same connected input", async () => {
|
|
87
|
+
const onChange = vi.fn();
|
|
88
|
+
renderWithProviders(<Harness onChange={onChange} />);
|
|
89
|
+
const before = gridControls();
|
|
90
|
+
const item = cell(0, "Item");
|
|
91
|
+
|
|
92
|
+
await userEvent.click(item);
|
|
93
|
+
await userEvent.type(item, "X");
|
|
94
|
+
fireEvent.keyDown(item, { key: "Enter" });
|
|
95
|
+
|
|
96
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
97
|
+
expect(onChange.mock.calls[0][0][0].item).toBe("BracketX");
|
|
98
|
+
expectConnected(before);
|
|
99
|
+
expect(document.activeElement).toBe(item);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("Tab after an edit commits and lands on the next cell's connected control", async () => {
|
|
103
|
+
const onChange = vi.fn();
|
|
104
|
+
renderWithProviders(<Harness onChange={onChange} />);
|
|
105
|
+
const before = gridControls();
|
|
106
|
+
const item = cell(0, "Item");
|
|
107
|
+
const qty = cell(0, "Qty");
|
|
108
|
+
|
|
109
|
+
await userEvent.click(item);
|
|
110
|
+
await userEvent.type(item, "X");
|
|
111
|
+
await userEvent.tab();
|
|
112
|
+
|
|
113
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(onChange.mock.calls[0][0][0].item).toBe("BracketX");
|
|
115
|
+
expectConnected(before);
|
|
116
|
+
expect(document.activeElement).toBe(qty);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("Shift+Tab after a Stepper edit commits and lands on the previous cell's connected control", async () => {
|
|
120
|
+
const onChange = vi.fn();
|
|
121
|
+
renderWithProviders(<Harness onChange={onChange} />);
|
|
122
|
+
const before = gridControls();
|
|
123
|
+
const item = cell(0, "Item");
|
|
124
|
+
const qty = cell(0, "Qty");
|
|
125
|
+
|
|
126
|
+
await userEvent.click(qty);
|
|
127
|
+
await userEvent.clear(qty);
|
|
128
|
+
await userEvent.type(qty, "7");
|
|
129
|
+
await userEvent.tab({ shift: true });
|
|
130
|
+
await flushBlurTimer();
|
|
131
|
+
|
|
132
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(onChange.mock.calls[0][0][0].qty).toBe(7);
|
|
134
|
+
expectConnected(before);
|
|
135
|
+
expect(document.activeElement).toBe(item);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("a commit in one row leaves the other row's cells connected in the form branch", async () => {
|
|
139
|
+
const onChange = vi.fn();
|
|
140
|
+
renderWithProviders(<FormHarness onChange={onChange} />);
|
|
141
|
+
const before = gridControls();
|
|
142
|
+
const item = cell(1, "Item");
|
|
143
|
+
|
|
144
|
+
await userEvent.click(item);
|
|
145
|
+
await userEvent.type(item, "X");
|
|
146
|
+
fireEvent.keyDown(item, { key: "Enter" });
|
|
147
|
+
|
|
148
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(onChange.mock.calls[0][0][1].item).toBe("BoltsX");
|
|
150
|
+
expectConnected(before);
|
|
151
|
+
expect(document.activeElement).toBe(item);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("a caller that rebuilds columns and isCellHidden every render keeps the cells mounted", async () => {
|
|
155
|
+
const onChange = vi.fn();
|
|
156
|
+
renderWithProviders(<Harness onChange={onChange} freshSchema />);
|
|
157
|
+
const before = gridControls();
|
|
158
|
+
const item = cell(0, "Item");
|
|
159
|
+
|
|
160
|
+
await userEvent.click(item);
|
|
161
|
+
await userEvent.type(item, "X");
|
|
162
|
+
await userEvent.tab();
|
|
163
|
+
|
|
164
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
165
|
+
expectConnected(before);
|
|
166
|
+
expect(document.activeElement).toBe(cell(0, "Qty"));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("a row detail Done leaves the grid mounted and shows the new values in its cells", async () => {
|
|
170
|
+
const onChange = vi.fn();
|
|
171
|
+
renderWithProviders(<Harness onChange={onChange} />);
|
|
172
|
+
const before = gridControls();
|
|
173
|
+
|
|
174
|
+
fireEvent.click(screen.getByRole("button", { name: "Edit row 2" }));
|
|
175
|
+
const dialog = await waitFor(() => screen.getByRole("dialog", { name: "Row 2" }));
|
|
176
|
+
const detailItem = within(dialog).getByLabelText("Item") as HTMLInputElement;
|
|
177
|
+
fireEvent.focus(detailItem);
|
|
178
|
+
fireEvent.change(detailItem, { target: { value: "Hinge" } });
|
|
179
|
+
fireEvent.blur(detailItem);
|
|
180
|
+
const detailQty = within(dialog).getByLabelText("Qty") as HTMLInputElement;
|
|
181
|
+
fireEvent.focus(detailQty);
|
|
182
|
+
fireEvent.change(detailQty, { target: { value: "9" } });
|
|
183
|
+
fireEvent.blur(detailQty);
|
|
184
|
+
await flushBlurTimer();
|
|
185
|
+
fireEvent.click(within(dialog).getByRole("button", { name: "Done" }));
|
|
186
|
+
|
|
187
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
188
|
+
expect(onChange.mock.calls[0][0][1]).toMatchObject({ item: "Hinge", qty: 9 });
|
|
189
|
+
await waitFor(() => expect(screen.queryByRole("dialog", { name: "Row 2" })).toBeNull(), {
|
|
190
|
+
timeout: 3000,
|
|
191
|
+
});
|
|
192
|
+
expectConnected(before.slice(0, 2));
|
|
193
|
+
expect(cell(1, "Item").value).toBe("Hinge");
|
|
194
|
+
expect(cell(1, "Qty").value).toBe("9");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KnobDialog.d.ts","sourceRoot":"","sources":["../../src/components/KnobDialog.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"KnobDialog.d.ts","sourceRoot":"","sources":["../../src/components/KnobDialog.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAY,EAAE,QAAQ,EAAE,EAAE,eAAe,2CAMzF;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CAoC/E;AAGD,eAAO,MAAM,iBAAiB;;;;wFAAiB,CAAC;AAChD,eAAO,MAAM,eAAe;;;;;;;;;;6CAAe,CAAC;AAC5C,eAAO,MAAM,qBAAqB;;;;;;;;;;6CAAqB,CAAC;AACxD,eAAO,MAAM,eAAe;;;;;;;;wFAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/TableInput/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAQH,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/TableInput/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAQH,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,0BAA0B,CAAC;AAalC,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,cAAc,EACf,MAAM,aAAa,CAAC;AAIrB,OAAO,EAOL,KAAK,eAAe,EACrB,MAAM,UAAU,CAAC;AA+GlB,MAAM,WAAW,eAAe,CAAC,KAAK,SAAS,aAAa,GAAG,aAAa;IAC1E,mFAAmF;IACnF,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC;IACf,4DAA4D;IAC5D,OAAO,EAAE,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;IAClC,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;IACnC,sEAAsE;IACtE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;IACvC,iFAAiF;IACjF,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5E,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kCAAkC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qCAAqC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yCAAyC;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,UAAU,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC5B,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;IACzC,kEAAkE;IAClE,YAAY,CAAC,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;CACvC;AAED,wBAAgB,UAAU,CAAC,KAAK,SAAS,aAAa,GAAG,aAAa,EAAE,EACtE,IAAS,EACT,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,IAAI,EACJ,IAAI,EAAE,QAAQ,EACd,UAAU,EACV,QAAQ,EACR,QAAgB,EAChB,QAAgB,EAChB,cAAqB,EACrB,UAAiB,EACjB,aAAoB,EACpB,OAAO,EACP,OAAO,EACP,UAAU,EACV,YAAY,EACZ,YAAY,EAAE,SAAS,EACvB,OAAc,EACd,aAAqB,EACrB,SAAiB,EACjB,aAAa,EACb,YAAY,GACb,EAAE,eAAe,CAAC,KAAK,CAAC,2CA0ExB;AAiZD,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,CAAC"}
|