@jielga/tmdatagrid 1.0.2 → 1.1.1
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/README.md +17 -14
- package/dist/index.d.ts +193 -21
- package/dist/index.js +1914 -1802
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +4 -4
- package/skills/columns/SKILL.md +1 -1
- package/skills/data/SKILL.md +33 -20
- package/skills/editing/SKILL.md +100 -103
- package/skills/editing/references/editing-api.md +5 -2
- package/skills/editing/references/editors-and-validation.md +7 -1
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +23 -17
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +1 -1
- package/skills/rows/SKILL.md +50 -15
- package/skills/rows/references/rows-api.md +3 -2
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +132 -32
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +223 -95
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +41 -4
- package/src/tmdatagrid/components/TMDataGridTable.tsx +218 -67
- package/src/tmdatagrid/core/editEngine.ts +28 -20
- package/src/tmdatagrid/index.ts +17 -2
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type UIEvent,
|
|
18
18
|
useCallback,
|
|
19
19
|
useEffect,
|
|
20
|
+
useLayoutEffect,
|
|
20
21
|
useRef,
|
|
21
22
|
useState,
|
|
22
23
|
} from "react";
|
|
@@ -24,14 +25,20 @@ import classes from "./TMDataGridTable.module.css";
|
|
|
24
25
|
import sticky from "./sticky.module.css";
|
|
25
26
|
import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
|
|
26
27
|
import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
|
|
27
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
TMDataGridHeaderCell,
|
|
30
|
+
type TMDataGridColumnMenuItemsRenderer,
|
|
31
|
+
} from "./TMDataGridHeaderCell";
|
|
28
32
|
import {
|
|
29
33
|
TMDataGridCellEditor,
|
|
30
34
|
type TMDataGridCellEditorClose,
|
|
31
35
|
} from "./TMDataGridCellEditor";
|
|
32
36
|
import { TMDataGridEntryRows } from "./TMDataGridEntryRows";
|
|
33
37
|
import { getColumnAlign, isControlColumn } from "../core/columnUtils";
|
|
34
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
getEditFieldName,
|
|
40
|
+
type TMDataGridEditState,
|
|
41
|
+
} from "../core/editEngine";
|
|
35
42
|
import {
|
|
36
43
|
getNextEditableCell,
|
|
37
44
|
resolveCellMove,
|
|
@@ -386,7 +393,7 @@ function TMDataGridBodyCell({
|
|
|
386
393
|
|
|
387
394
|
/**
|
|
388
395
|
* The virtualized body rows, wrapped in the single Menu that serves all of them
|
|
389
|
-
* when `
|
|
396
|
+
* when `renderRowContextMenu` is set.
|
|
390
397
|
*
|
|
391
398
|
* One instance for the whole body rather than one per row. A Mantine `Menu` is
|
|
392
399
|
* a `Popover`, and a closed Popover still runs its hooks on every render - and
|
|
@@ -447,7 +454,7 @@ function TMDataGridBodyRowGroup({
|
|
|
447
454
|
);
|
|
448
455
|
}
|
|
449
456
|
|
|
450
|
-
/** What
|
|
457
|
+
/** What `renderRowContextMenu` is handed for the right-clicked row. */
|
|
451
458
|
export type TMDataGridRowContextMenuArgs<TData extends RowData> = {
|
|
452
459
|
table: Table<TMDataGridFeatures, TData>;
|
|
453
460
|
row: Row<TMDataGridFeatures, TData>;
|
|
@@ -462,6 +469,14 @@ export type TMDataGridRowContextMenuArgs<TData extends RowData> = {
|
|
|
462
469
|
* `closeOnItemClick`), so this is for content that isn't a menu item.
|
|
463
470
|
*/
|
|
464
471
|
close: () => void;
|
|
472
|
+
/**
|
|
473
|
+
* The grid's own items for this menu: the cell copy and export items, under
|
|
474
|
+
* `cellSelection: "range"`, and `null` otherwise.
|
|
475
|
+
*
|
|
476
|
+
* Placing it decides the order. Ignore it and the grid keeps today's
|
|
477
|
+
* behavior, rendering its items above a divider and yours below.
|
|
478
|
+
*/
|
|
479
|
+
internalItems: ReactNode;
|
|
465
480
|
};
|
|
466
481
|
|
|
467
482
|
/**
|
|
@@ -469,7 +484,7 @@ export type TMDataGridRowContextMenuArgs<TData extends RowData> = {
|
|
|
469
484
|
* `Menu.Divider`, `Menu.Sub`, or any other node. The grid owns the `Menu` and
|
|
470
485
|
* its `Menu.Dropdown`; this fills the dropdown.
|
|
471
486
|
*/
|
|
472
|
-
export type
|
|
487
|
+
export type TMDataGridRowContextMenuRenderer<TData extends RowData> = (
|
|
473
488
|
args: TMDataGridRowContextMenuArgs<TData>,
|
|
474
489
|
) => ReactNode;
|
|
475
490
|
|
|
@@ -509,7 +524,7 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
509
524
|
/** As {@link onCellClick}; a double-click that opens an editor still does. */
|
|
510
525
|
onCellDoubleClick?: (args: TMDataGridCellEventArgs<TData>) => void;
|
|
511
526
|
/**
|
|
512
|
-
* As {@link onCellClick}; `
|
|
527
|
+
* As {@link onCellClick}; `renderRowContextMenu` and the cell-selection menu still
|
|
513
528
|
* open. To suppress the browser's own menu on a grid with neither, call
|
|
514
529
|
* `event.preventDefault()` here.
|
|
515
530
|
*/
|
|
@@ -575,7 +590,7 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
575
590
|
*
|
|
576
591
|
* ```tsx
|
|
577
592
|
* <TMDataGrid.Table<Employee>
|
|
578
|
-
*
|
|
593
|
+
* renderRowContextMenu={({ row, close }) => (
|
|
579
594
|
* <>
|
|
580
595
|
* <Menu.Label>{row.original.firstName}</Menu.Label>
|
|
581
596
|
* <Menu.Item onClick={() => open(row.original.id)}>Open</Menu.Item>
|
|
@@ -588,10 +603,33 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
588
603
|
* />
|
|
589
604
|
* ```
|
|
590
605
|
*
|
|
606
|
+
* `internalItems` is the grid's own half - place it to control the order,
|
|
607
|
+
* ignore it and the grid keeps its items above a divider and yours below.
|
|
608
|
+
*
|
|
591
609
|
* Called during render, and only for the row whose menu is open - so it stays
|
|
592
610
|
* a pure function of the row, and cost per row does not matter.
|
|
593
611
|
*/
|
|
594
|
-
|
|
612
|
+
renderRowContextMenu?: TMDataGridRowContextMenuRenderer<TData>;
|
|
613
|
+
/**
|
|
614
|
+
* Replaces a column menu's contents, taking the grid's own items and
|
|
615
|
+
* returning the full list.
|
|
616
|
+
*
|
|
617
|
+
* ```tsx
|
|
618
|
+
* <TMDataGrid.Table
|
|
619
|
+
* renderColumnMenuItems={({ column, internalItems }) => [
|
|
620
|
+
* ...internalItems,
|
|
621
|
+
* <Menu.Divider key="custom" />,
|
|
622
|
+
* <Menu.Item key="stats" onClick={() => showStats(column.id)}>
|
|
623
|
+
* Statistics
|
|
624
|
+
* </Menu.Item>,
|
|
625
|
+
* ]}
|
|
626
|
+
* />
|
|
627
|
+
* ```
|
|
628
|
+
*
|
|
629
|
+
* Returning an empty list leaves the column with no menu button, the same as
|
|
630
|
+
* a column whose every feature is switched off.
|
|
631
|
+
*/
|
|
632
|
+
renderColumnMenuItems?: TMDataGridColumnMenuItemsRenderer;
|
|
595
633
|
/**
|
|
596
634
|
* Passed to the Mantine `Menu` the grid wraps the row in - `width`, `shadow`,
|
|
597
635
|
* `position`, `transitionProps` and the rest. Its open state is the grid's.
|
|
@@ -651,7 +689,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
651
689
|
onScrollToLeft,
|
|
652
690
|
onScrollToRight,
|
|
653
691
|
renderEmptyState,
|
|
654
|
-
|
|
692
|
+
renderRowContextMenu,
|
|
693
|
+
renderColumnMenuItems,
|
|
655
694
|
rowContextMenuProps,
|
|
656
695
|
cellExport,
|
|
657
696
|
onReachEnd,
|
|
@@ -685,6 +724,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
685
724
|
// Which cell has an editor open. The projection (dirty/error markers) is
|
|
686
725
|
// subscribed per cell instead - see TMDataGridBodyCell.
|
|
687
726
|
const editActive = useSelector(edit.store, (state) => state.active);
|
|
727
|
+
// Row mode opens whole rows and any number of them at once, so which rows
|
|
728
|
+
// are showing editors is the set of open forms, not the single active slot.
|
|
729
|
+
const editOpenRowIds = useSelector(edit.store, (state) => state.openRowIds);
|
|
688
730
|
const newRowCount = useSelector(edit.store, (state) => state.newRows.length);
|
|
689
731
|
const deletedRowIds = useSelector(edit.store, (state) => state.deletedRowIds);
|
|
690
732
|
|
|
@@ -1364,6 +1406,74 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1364
1406
|
return seed;
|
|
1365
1407
|
};
|
|
1366
1408
|
|
|
1409
|
+
/**
|
|
1410
|
+
* The row's first cell that edits, in visual order - where the caret goes
|
|
1411
|
+
* for a gesture that named no column, which is the edit lane's pencil.
|
|
1412
|
+
*/
|
|
1413
|
+
const firstEditableColumnId = (rowId: string): string | null => {
|
|
1414
|
+
const row = rows.find((candidate) => candidate.id === rowId);
|
|
1415
|
+
if (row === undefined) return null;
|
|
1416
|
+
const cells = [
|
|
1417
|
+
...row.getLeftVisibleCells(),
|
|
1418
|
+
...row.getCenterVisibleCells(),
|
|
1419
|
+
...row.getRightVisibleCells(),
|
|
1420
|
+
];
|
|
1421
|
+
return (
|
|
1422
|
+
cells.find((cell) => edit.canEditCell(row, cell.column))?.column.id ??
|
|
1423
|
+
null
|
|
1424
|
+
);
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* The `active` the caret has already been placed for. Identity, not value:
|
|
1429
|
+
* `beginOn` publishes a fresh object per gesture, so re-opening the same
|
|
1430
|
+
* cell is a new intent, while every later render for the same gesture - a
|
|
1431
|
+
* remount as the virtualizer scrolls among open rows - is not.
|
|
1432
|
+
*/
|
|
1433
|
+
const placedEditCaretRef = useRef<TMDataGridEditState["active"]>(null);
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Puts the caret in the cell the open gesture named, once per gesture.
|
|
1437
|
+
*
|
|
1438
|
+
* The grid does this rather than leaving it to the editor's `autoFocus`,
|
|
1439
|
+
* because only the built-ins honour that prop: a `meta.editor` is free to
|
|
1440
|
+
* ignore it, and then the row opened with the caret in none of its editors -
|
|
1441
|
+
* the ring on the cell and a Tab still needed to reach the input. Focus goes
|
|
1442
|
+
* to `editor-input` when the editor publishes it, otherwise to whatever is
|
|
1443
|
+
* focusable inside, which is what a custom editor renders.
|
|
1444
|
+
*
|
|
1445
|
+
* A layout effect, ahead of the focused-cell effect below: the ring follows
|
|
1446
|
+
* the caret there rather than fighting it.
|
|
1447
|
+
*/
|
|
1448
|
+
useLayoutEffect(() => {
|
|
1449
|
+
if (editActive === null) {
|
|
1450
|
+
placedEditCaretRef.current = null;
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (placedEditCaretRef.current === editActive) return;
|
|
1454
|
+
const container = scrollContainerRef.current;
|
|
1455
|
+
if (container === null) return;
|
|
1456
|
+
// The lane's pencil names no column - the row's first editable cell.
|
|
1457
|
+
const columnId =
|
|
1458
|
+
editActive.columnId ?? firstEditableColumnId(editActive.rowId);
|
|
1459
|
+
if (columnId === null) {
|
|
1460
|
+
placedEditCaretRef.current = editActive;
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const editor = container.querySelector<HTMLElement>(
|
|
1464
|
+
`[data-dg-part="editor"][data-row-id="${CSS.escape(editActive.rowId)}"]` +
|
|
1465
|
+
`[data-column-id="${CSS.escape(columnId)}"]`,
|
|
1466
|
+
);
|
|
1467
|
+
// Not mounted yet - the virtualizer is still bringing the row in. The
|
|
1468
|
+
// intent stays up, and the next render tries again.
|
|
1469
|
+
if (editor === null) return;
|
|
1470
|
+
placedEditCaretRef.current = editActive;
|
|
1471
|
+
const target =
|
|
1472
|
+
editor.querySelector<HTMLElement>('[data-dg-part="editor-input"]') ??
|
|
1473
|
+
editor.querySelector<HTMLElement>(FOCUSABLE_IN_CELL);
|
|
1474
|
+
target?.focus();
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1367
1477
|
/** DOM focus back onto a cell - after Escape, or a commit with nowhere to go. */
|
|
1368
1478
|
const refocusCell = (position: TMDataGridCellPosition) => {
|
|
1369
1479
|
const container = scrollContainerRef.current;
|
|
@@ -1374,12 +1484,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1374
1484
|
|
|
1375
1485
|
const openEditor = (position: TMDataGridCellPosition) => {
|
|
1376
1486
|
if (cellSelection) ui.actions.setFocusedCell(position);
|
|
1377
|
-
// Row mode edits whole rows: any editable cell's open gesture opens
|
|
1378
|
-
//
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1487
|
+
// Row mode edits whole rows: any editable cell's open gesture opens all of
|
|
1488
|
+
// them, exactly as the lane's pencil does. The column still travels, as
|
|
1489
|
+
// the cell the caret belongs in - the one the gesture named. Dropping it
|
|
1490
|
+
// here sent the caret to the row's first editable cell instead, which the
|
|
1491
|
+
// focused-cell effect above then pulled back onto the cell just clicked,
|
|
1492
|
+
// leaving the row open with the caret in none of its editors.
|
|
1493
|
+
edit.begin({ rowId: position.rowId, columnId: position.columnId });
|
|
1383
1494
|
};
|
|
1384
1495
|
|
|
1385
1496
|
const handleEditorClose = (
|
|
@@ -1658,6 +1769,18 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1658
1769
|
// Focus is already inside - on the control Enter stepped into. The cell is
|
|
1659
1770
|
// where the ring belongs, so there is nothing to move.
|
|
1660
1771
|
if (cellElement.contains(document.activeElement)) return;
|
|
1772
|
+
// An open editor outranks the ring, wherever it is. Row mode opens a whole
|
|
1773
|
+
// row from one gesture, so the caret legitimately sits in a cell other
|
|
1774
|
+
// than the one that gesture landed the ring on - the lane's pencil is the
|
|
1775
|
+
// plain case, focusing its own cell on the way to opening the row. Pulling
|
|
1776
|
+
// the caret back onto that cell would leave the row open with the caret in
|
|
1777
|
+
// none of its editors, and the user with nothing to type into.
|
|
1778
|
+
if (
|
|
1779
|
+
document.activeElement instanceof Element &&
|
|
1780
|
+
document.activeElement.closest('[data-dg-part="editor"]') !== null
|
|
1781
|
+
) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1661
1784
|
|
|
1662
1785
|
// The vertical half is the virtualizer's, and it has already been asked.
|
|
1663
1786
|
cellElement.focus({ preventScroll: true });
|
|
@@ -1700,7 +1823,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1700
1823
|
const columnIndex = orderedColumns.findIndex(
|
|
1701
1824
|
(column) => column.id === columnId,
|
|
1702
1825
|
);
|
|
1703
|
-
|
|
1826
|
+
// `rowIndex < 0` is a pinned row: those sit out the cell range, so
|
|
1827
|
+
// moving the selection onto one would resolve to no rectangle at all
|
|
1828
|
+
// and drop the selection the user could still see at the edge.
|
|
1829
|
+
if (
|
|
1830
|
+
rowIndex >= 0 &&
|
|
1831
|
+
!isWithinBounds(selectionBounds, rowIndex, columnIndex)
|
|
1832
|
+
) {
|
|
1704
1833
|
ui.actions.setFocusedCell({ rowId, columnId });
|
|
1705
1834
|
setRangeTo({ rowId, columnId }, { extend: false });
|
|
1706
1835
|
}
|
|
@@ -1709,34 +1838,24 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1709
1838
|
setContextMenuTarget(rowId === null ? null : { rowId, columnId });
|
|
1710
1839
|
};
|
|
1711
1840
|
|
|
1841
|
+
// The body list, then the edge blocks: pinning takes a row out of the
|
|
1842
|
+
// scrolling order, and unpinning is the first thing its menu exists to
|
|
1843
|
+
// offer, so a menu that could not resolve a pinned row would strand it
|
|
1844
|
+
// there. Searched in that order because the body holds nearly every row.
|
|
1712
1845
|
const contextMenuRow =
|
|
1713
1846
|
contextMenuTarget === null
|
|
1714
1847
|
? undefined
|
|
1715
|
-
: rows.find((row) => row.id === contextMenuTarget.rowId)
|
|
1848
|
+
: (rows.find((row) => row.id === contextMenuTarget.rowId) ??
|
|
1849
|
+
pinnedTopRows.find((row) => row.id === contextMenuTarget.rowId) ??
|
|
1850
|
+
pinnedBottomRows.find((row) => row.id === contextMenuTarget.rowId));
|
|
1851
|
+
|
|
1852
|
+
const canRenderRowMenu =
|
|
1853
|
+
renderRowContextMenu !== undefined &&
|
|
1854
|
+
contextMenuTarget !== null &&
|
|
1855
|
+
contextMenuRow !== undefined &&
|
|
1856
|
+
!contextMenuRow.getIsGrouped();
|
|
1857
|
+
|
|
1716
1858
|
|
|
1717
|
-
/**
|
|
1718
|
-
* Built for the open row only, so a consumer's render prop is called once per
|
|
1719
|
-
* opened menu rather than once per mounted row. `null` back from it means
|
|
1720
|
-
* this row has no menu, which is also how the menu stays shut.
|
|
1721
|
-
*
|
|
1722
|
-
* The casts put back the concrete row type the context boundary erased - the
|
|
1723
|
-
* same crossing `onRowClick` makes.
|
|
1724
|
-
*/
|
|
1725
|
-
const rowMenuContent =
|
|
1726
|
-
rowContextMenu &&
|
|
1727
|
-
contextMenuTarget &&
|
|
1728
|
-
contextMenuRow &&
|
|
1729
|
-
!contextMenuRow.getIsGrouped()
|
|
1730
|
-
? rowContextMenu({
|
|
1731
|
-
table: table as unknown as Table<TMDataGridFeatures, TData>,
|
|
1732
|
-
row: contextMenuRow as unknown as Row<TMDataGridFeatures, TData>,
|
|
1733
|
-
cell: (contextMenuRow
|
|
1734
|
-
.getAllCells()
|
|
1735
|
-
.find((cell) => cell.column.id === contextMenuTarget.columnId) ??
|
|
1736
|
-
null) as unknown as Cell<TMDataGridFeatures, TData, unknown> | null,
|
|
1737
|
-
close: closeContextMenu,
|
|
1738
|
-
}) ?? null
|
|
1739
|
-
: null;
|
|
1740
1859
|
|
|
1741
1860
|
/**
|
|
1742
1861
|
* The built-in half of the menu: what to do with the cells that are selected.
|
|
@@ -1780,18 +1899,56 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1780
1899
|
</>
|
|
1781
1900
|
) : null;
|
|
1782
1901
|
|
|
1902
|
+
/**
|
|
1903
|
+
* The consumer's half, built for the open row only - so the render prop runs
|
|
1904
|
+
* once per opened menu rather than once per mounted row. `null` back from it
|
|
1905
|
+
* means this row has no menu, which is also how the menu stays shut.
|
|
1906
|
+
*
|
|
1907
|
+
* `internalItems` is a getter, and reading it is the signal: a consumer who
|
|
1908
|
+
* touches it has taken the composition over, so the grid stops prepending
|
|
1909
|
+
* its own half. One who never mentions it gets today's behavior, the cell
|
|
1910
|
+
* items above a divider. The alternative - a second prop declaring intent -
|
|
1911
|
+
* asks people to say twice what the placement already says.
|
|
1912
|
+
*
|
|
1913
|
+
* The casts put back the concrete row type the context boundary erased - the
|
|
1914
|
+
* same crossing `onRowClick` makes.
|
|
1915
|
+
*/
|
|
1916
|
+
let tookOverMenu = false;
|
|
1917
|
+
const rowMenuContent = canRenderRowMenu
|
|
1918
|
+
? (renderRowContextMenu({
|
|
1919
|
+
table: table as unknown as Table<TMDataGridFeatures, TData>,
|
|
1920
|
+
row: contextMenuRow as unknown as Row<TMDataGridFeatures, TData>,
|
|
1921
|
+
cell: (contextMenuRow
|
|
1922
|
+
?.getAllCells()
|
|
1923
|
+
.find((cell) => cell.column.id === contextMenuTarget?.columnId) ??
|
|
1924
|
+
null) as unknown as Cell<TMDataGridFeatures, TData, unknown> | null,
|
|
1925
|
+
close: closeContextMenu,
|
|
1926
|
+
get internalItems() {
|
|
1927
|
+
tookOverMenu = true;
|
|
1928
|
+
return cellMenuContent;
|
|
1929
|
+
},
|
|
1930
|
+
}) ?? null)
|
|
1931
|
+
: null;
|
|
1932
|
+
|
|
1783
1933
|
/**
|
|
1784
1934
|
* The two halves, in the order they are about: the cells first, since the
|
|
1785
1935
|
* right-click landed on one, then whatever the consumer offers for the row.
|
|
1936
|
+
* Unless the consumer took the composition over, in which case what they
|
|
1937
|
+
* returned is the whole menu.
|
|
1786
1938
|
*/
|
|
1787
|
-
const contextMenuContent =
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1939
|
+
const contextMenuContent = tookOverMenu
|
|
1940
|
+
? rowMenuContent
|
|
1941
|
+
: cellMenuContent === null && rowMenuContent === null
|
|
1942
|
+
? null
|
|
1943
|
+
: (
|
|
1944
|
+
<>
|
|
1945
|
+
{cellMenuContent}
|
|
1946
|
+
{cellMenuContent !== null && rowMenuContent !== null && (
|
|
1947
|
+
<Menu.Divider />
|
|
1948
|
+
)}
|
|
1949
|
+
{rowMenuContent}
|
|
1950
|
+
</>
|
|
1951
|
+
);
|
|
1795
1952
|
|
|
1796
1953
|
/**
|
|
1797
1954
|
* Which edges the scroll position is currently at. Seeded "at top-left",
|
|
@@ -1908,6 +2065,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1908
2065
|
>
|
|
1909
2066
|
{headerGroup.headers.map((header) => (
|
|
1910
2067
|
<TMDataGridHeaderCell
|
|
2068
|
+
renderColumnMenuItems={renderColumnMenuItems}
|
|
1911
2069
|
key={header.id}
|
|
1912
2070
|
header={header}
|
|
1913
2071
|
layout={layoutFor(header.column.id)}
|
|
@@ -1929,8 +2087,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1929
2087
|
|
|
1930
2088
|
<TMDataGridBodyRowGroup
|
|
1931
2089
|
// Under `"range"` the grid has a menu of its own to offer, so the
|
|
1932
|
-
// wrapper goes on even without a consumer's `
|
|
1933
|
-
enabled={
|
|
2090
|
+
// wrapper goes on even without a consumer's `renderRowContextMenu`.
|
|
2091
|
+
enabled={renderRowContextMenu !== undefined || cellRangeSelection}
|
|
1934
2092
|
menuContent={contextMenuContent}
|
|
1935
2093
|
menuProps={rowContextMenuProps}
|
|
1936
2094
|
onContextMenu={handleContextMenu}
|
|
@@ -1959,17 +2117,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1959
2117
|
...row.getCenterVisibleCells(),
|
|
1960
2118
|
...row.getRightVisibleCells(),
|
|
1961
2119
|
];
|
|
1962
|
-
// Row mode opens every editable cell of the row at once
|
|
1963
|
-
//
|
|
2120
|
+
// Row mode opens every editable cell of the row at once, and
|
|
2121
|
+
// rows accumulate: a second row opening leaves the first one
|
|
2122
|
+
// open, so every row holding a form shows its editors. Which of
|
|
2123
|
+
// them holds the caret is not decided here - see
|
|
2124
|
+
// placeEditCaret, which places it once per gesture.
|
|
1964
2125
|
const rowEditing =
|
|
1965
|
-
|
|
1966
|
-
editActive.rowId === row.id &&
|
|
1967
|
-
editActive.columnId === null;
|
|
1968
|
-
const firstEditableColumnId = rowEditing
|
|
1969
|
-
? (rowCells.find((cell) =>
|
|
1970
|
-
edit.canEditCell(row, cell.column),
|
|
1971
|
-
)?.column.id ?? null)
|
|
1972
|
-
: null;
|
|
2126
|
+
features.editMode === "row" && editOpenRowIds.includes(row.id);
|
|
1973
2127
|
// Cell selection takes the body's tab stop off the row and puts
|
|
1974
2128
|
// it on a cell - two stops per row would make Tab a way of
|
|
1975
2129
|
// walking the grid, which is what the arrow keys are for. Space
|
|
@@ -2110,14 +2264,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2110
2264
|
// is recorded before the dropdown is built.
|
|
2111
2265
|
//
|
|
2112
2266
|
// A group row records itself only for the cell menu, which is
|
|
2113
|
-
// about the cells it covers. `
|
|
2267
|
+
// about the cells it covers. `renderRowContextMenu` is still never
|
|
2114
2268
|
// called for one: TanStack builds a group row on its first
|
|
2115
2269
|
// child's record, so a render prop reaching for `row.original`
|
|
2116
2270
|
// would be handed a real employee that has nothing to do with
|
|
2117
2271
|
// the group - see where the content is built. The browser menu
|
|
2118
2272
|
// stays suppressed over them either way.
|
|
2119
2273
|
onContextMenu={
|
|
2120
|
-
(
|
|
2274
|
+
(renderRowContextMenu && !isGroupRow) || cellRangeSelection
|
|
2121
2275
|
? () => {
|
|
2122
2276
|
contextMenuRowRef.current = row.id;
|
|
2123
2277
|
}
|
|
@@ -2149,10 +2303,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2149
2303
|
cell={cell}
|
|
2150
2304
|
row={row}
|
|
2151
2305
|
takeSeedText={takeEditSeed}
|
|
2152
|
-
autoFocus={
|
|
2153
|
-
!rowEditing ||
|
|
2154
|
-
cell.column.id === firstEditableColumnId
|
|
2155
|
-
}
|
|
2306
|
+
autoFocus={!rowEditing}
|
|
2156
2307
|
onClose={(close) =>
|
|
2157
2308
|
handleEditorClose(
|
|
2158
2309
|
{ rowId: row.id, columnId: cell.column.id },
|
|
@@ -2252,7 +2403,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2252
2403
|
: undefined
|
|
2253
2404
|
}
|
|
2254
2405
|
onContextMenu={
|
|
2255
|
-
|
|
2406
|
+
renderRowContextMenu || cellRangeSelection || onCellContextMenu
|
|
2256
2407
|
? (event) => {
|
|
2257
2408
|
contextMenuColumnRef.current = cell.column.id;
|
|
2258
2409
|
if (isGroupRow) return;
|
|
@@ -118,9 +118,16 @@ export type TMDataGridEditRowProjection = {
|
|
|
118
118
|
* reads a form's store directly - it is mounted for one cell at a time.
|
|
119
119
|
*/
|
|
120
120
|
export type TMDataGridEditState = {
|
|
121
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* The cell the last open gesture named - where the caret goes. In row mode
|
|
123
|
+
* the whole row is editing whatever this says, so there it is only the
|
|
124
|
+
* caret's cell, and `null` for the lane's pencil, which names no column.
|
|
125
|
+
*/
|
|
122
126
|
active: { rowId: string; columnId: string | null } | null;
|
|
123
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Rows with a live form. In cell mode at most one; in row, cellConfirm and
|
|
129
|
+
* batch, as many as the user opened - which rows are editing.
|
|
130
|
+
*/
|
|
124
131
|
openRowIds: ReadonlyArray<string>;
|
|
125
132
|
rows: Record<string, TMDataGridEditRowProjection>;
|
|
126
133
|
/** Phase 4 - rows being created, not yet in `data`. */
|
|
@@ -166,8 +173,13 @@ export type TMDataGridEditorArgs = {
|
|
|
166
173
|
cancel: () => void;
|
|
167
174
|
size: TMDataGridSize;
|
|
168
175
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
176
|
+
* @deprecated The grid places the caret itself, once per open gesture, so
|
|
177
|
+
* honouring this is optional and it will go in the next major. It focuses
|
|
178
|
+
* `data-dg-part="editor-input"` when the editor publishes it and the first
|
|
179
|
+
* focusable element inside the editor otherwise, which is why a custom
|
|
180
|
+
* editor that ignores this prop still gets the caret. False under
|
|
181
|
+
* `editMode: "row"`, where a whole row of editors is mounted at once and
|
|
182
|
+
* only the named cell may take it.
|
|
171
183
|
*/
|
|
172
184
|
autoFocus: boolean;
|
|
173
185
|
/**
|
|
@@ -285,7 +297,11 @@ export type TMDataGridEditApi = {
|
|
|
285
297
|
canEditCell: (row: ErasedRow, column: ErasedColumn) => boolean;
|
|
286
298
|
/** Whether the row takes edits at all - the edit lane's pencil gate. */
|
|
287
299
|
canEditRow: (row: ErasedRow) => boolean;
|
|
288
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* Opens an editor. In row mode the whole row opens either way and `columnId`
|
|
302
|
+
* only says which cell takes the caret; `null` (the lane's pencil) leaves it
|
|
303
|
+
* to the row's first editable cell.
|
|
304
|
+
*/
|
|
289
305
|
begin: (target: { rowId: string; columnId: string | null }) => void;
|
|
290
306
|
/**
|
|
291
307
|
* Commits one row - `form.handleSubmit` under the hood. Resolves `true`
|
|
@@ -636,30 +652,22 @@ export function createEditEngine(
|
|
|
636
652
|
|
|
637
653
|
const begin: TMDataGridEditApi["begin"] = ({ rowId, columnId }) => {
|
|
638
654
|
const { editMode } = getContext();
|
|
639
|
-
|
|
640
|
-
// Each mode's policy about the row being left:
|
|
655
|
+
// Each mode's policy about a row already open elsewhere:
|
|
641
656
|
//
|
|
642
657
|
// | Mode | Another row open |
|
|
643
658
|
// | ---- | ---------------- |
|
|
644
659
|
// | cell | committed - Sheets; a failed commit keeps holding the edit |
|
|
645
|
-
// | row
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
if (
|
|
649
|
-
|
|
660
|
+
// | row, cellConfirm, batch | accumulates. Every draft waits for its own
|
|
661
|
+
// save, so a second row opening can neither discard the first nor be
|
|
662
|
+
// refused by it - nothing about one row's form bears on another's |
|
|
663
|
+
if (editMode === "cell") {
|
|
664
|
+
const openElsewhere = [...forms.keys()].find((id) => id !== rowId);
|
|
665
|
+
if (openElsewhere !== undefined) {
|
|
650
666
|
void commit(openElsewhere).then((ok) => {
|
|
651
667
|
if (ok) beginOn(rowId, columnId);
|
|
652
668
|
});
|
|
653
669
|
return;
|
|
654
670
|
}
|
|
655
|
-
if (editMode === "row") {
|
|
656
|
-
const entry = forms.get(openElsewhere);
|
|
657
|
-
const pristine =
|
|
658
|
-
entry !== undefined &&
|
|
659
|
-
diff(entry, entry.form.state.values as TMDataGridRowData).length === 0;
|
|
660
|
-
if (!pristine) return;
|
|
661
|
-
drop(openElsewhere);
|
|
662
|
-
}
|
|
663
671
|
}
|
|
664
672
|
beginOn(rowId, columnId);
|
|
665
673
|
};
|
package/src/tmdatagrid/index.ts
CHANGED
|
@@ -66,7 +66,14 @@ export {
|
|
|
66
66
|
type TMDataGridOptionsArgs,
|
|
67
67
|
type TMDataGridOptionsSource,
|
|
68
68
|
} from "./core/columnOptions";
|
|
69
|
-
export {
|
|
69
|
+
export {
|
|
70
|
+
TMDataGridEditActions,
|
|
71
|
+
type TMDataGridEditActionsActions,
|
|
72
|
+
type TMDataGridEditActionsControls,
|
|
73
|
+
type TMDataGridEditActionsProps,
|
|
74
|
+
type TMDataGridEditActionsSlotArgs,
|
|
75
|
+
type TMDataGridEditActionsState,
|
|
76
|
+
} from "./components/TMDataGridEditActions";
|
|
70
77
|
export {
|
|
71
78
|
clearedValueForType,
|
|
72
79
|
getEditFieldName,
|
|
@@ -207,13 +214,21 @@ export type {
|
|
|
207
214
|
TMDataGridCellEventArgs,
|
|
208
215
|
TMDataGridCellNav,
|
|
209
216
|
TMDataGridColumnLayout,
|
|
210
|
-
TMDataGridRowContextMenu,
|
|
211
217
|
TMDataGridRowContextMenuArgs,
|
|
218
|
+
TMDataGridRowContextMenuRenderer,
|
|
212
219
|
TMDataGridRowStyle,
|
|
213
220
|
TMDataGridTableProps,
|
|
214
221
|
} from "./components/TMDataGridTable";
|
|
222
|
+
export type {
|
|
223
|
+
TMDataGridColumnMenuItemsArgs,
|
|
224
|
+
TMDataGridColumnMenuItemsRenderer,
|
|
225
|
+
} from "./components/TMDataGridHeaderCell";
|
|
215
226
|
export {
|
|
216
227
|
getTMDataGridPaginationApi,
|
|
217
228
|
type TMDataGridFooterProps,
|
|
229
|
+
type TMDataGridPaginationActions,
|
|
218
230
|
type TMDataGridPaginationApi,
|
|
231
|
+
type TMDataGridPaginationControls,
|
|
232
|
+
type TMDataGridPaginationSlotArgs,
|
|
233
|
+
type TMDataGridPaginationState,
|
|
219
234
|
} from "./components/TMDataGridFooter";
|