@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.
@@ -6,17 +6,18 @@ description: >
6
6
  highlight as state of its own with defaultHighlightedRowId and
7
7
  onHighlightedRowChange, showSelectedBackground and --dg-row-selected-bg,
8
8
  reading a selection through the table store, onRowClick / onCellClick /
9
- onCellDoubleClick / onCellContextMenu, the rowContextMenu render prop and
10
- rowContextMenuProps, per-row styling with rowStyle, rowClassName, striped and
11
- the --row-bg rule, the row details panel through renderDetails and
12
- DETAILS_COLUMN_ID, row pinning with enableRowPinning and row.pin, and the
13
- row-number gutter through enableRowNumbers. Load when selecting rows, reacting
14
- to a click, opening a detail panel, colouring rows by their data, pinning rows
15
- to an edge, or numbering them.
9
+ onCellDoubleClick / onCellContextMenu, the renderRowContextMenu slot with its
10
+ internalItems handback, renderColumnMenuItems and rowContextMenuProps, per-row
11
+ styling with rowStyle, rowClassName, striped and the --row-bg rule, the row
12
+ details panel through renderDetails and DETAILS_COLUMN_ID, row pinning with
13
+ enableRowPinning and row.pin, and the row-number gutter through
14
+ enableRowNumbers. Load when selecting rows, reacting to a click, opening a
15
+ detail panel, colouring rows by their data, pinning rows to an edge, or
16
+ numbering them.
16
17
  metadata:
17
18
  type: core
18
19
  library: '@jielga/tmdatagrid'
19
- library_version: '1.0.2'
20
+ library_version: '1.1.1'
20
21
  sources:
21
22
  - 'Jielga/TMDataGrid:src/docs/row-selection.md'
22
23
  - 'Jielga/TMDataGrid:src/docs/row-interaction.md'
@@ -117,7 +118,7 @@ not replace selection or the highlight; yours runs in addition.
117
118
  | `onRowClick` | `row` | Rows show a pointer cursor when set |
118
119
  | `onCellClick` | `{ cell, row, column, event }` | The cell cursor still moves |
119
120
  | `onCellDoubleClick` | same | A double-click that opens an editor still does |
120
- | `onCellContextMenu` | same | `rowContextMenu` still opens |
121
+ | `onCellContextMenu` | same | `renderRowContextMenu` still opens |
121
122
 
122
123
  ```tsx
123
124
  <TMDataGrid.Table<Employee> onRowClick={(row) => open(row.original.id)} />
@@ -127,13 +128,13 @@ Pass the row type, as above, and `row.original` is typed. Group rows sit out all
127
128
  four: TanStack builds a group row on top of its first child's record, so a
128
129
  handler would receive a real-looking row that is the wrong one.
129
130
 
130
- `rowContextMenu` is a render prop saying what goes inside the menu. The grid
131
+ `renderRowContextMenu` is a slot saying what goes inside the menu. The grid
131
132
  owns the Mantine `Menu`, opening it at the pointer and closing it on Escape, an
132
133
  outside click, a body scroll and after an item is picked.
133
134
 
134
135
  ```tsx
135
136
  <TMDataGrid.Table<Employee>
136
- rowContextMenu={({ table, row, cell }) => (
137
+ renderRowContextMenu={({ table, row, cell }) => (
137
138
  <>
138
139
  <Menu.Label>{row.original.firstName}</Menu.Label>
139
140
  <Menu.Item onClick={() => open(row.original.id)}>Open</Menu.Item>
@@ -156,6 +157,35 @@ a pure function of its arguments and do the work in the item handlers. Return
156
157
  the row with `data-context-menu` while its menu is open, so an action meant for
157
158
  a multi-selection reads `table` and falls back to the clicked row.
158
159
 
160
+ Under `cellSelection: "range"` the grid has its own items for this menu - copy,
161
+ export, include headers. `internalItems` is those items, and **reading it hands
162
+ the composition over**: place it and the menu is exactly what you returned;
163
+ never mention it and the grid keeps its half above a divider and yours below.
164
+
165
+ ```tsx
166
+ renderRowContextMenu={({ row, internalItems }) => (
167
+ <>
168
+ <Menu.Item onClick={() => open(row.id)}>Open</Menu.Item>
169
+ <Menu.Divider />
170
+ {internalItems}
171
+ </>
172
+ )}
173
+ ```
174
+
175
+ `renderColumnMenuItems` is the same idea for a column's menu, and returns an
176
+ array rather than a node:
177
+
178
+ ```tsx
179
+ <TMDataGrid.Table
180
+ renderColumnMenuItems={({ column, internalItems }) => [
181
+ ...internalItems,
182
+ <Menu.Item key="stats" onClick={() => showStats(column.id)}>Stats</Menu.Item>,
183
+ ]}
184
+ />
185
+ ```
186
+
187
+ Returning an empty list leaves the column with no menu button at all.
188
+
159
189
  ## Row styling
160
190
 
161
191
  ```tsx
@@ -219,10 +249,15 @@ const grid = useTMDataGrid({
219
249
  });
220
250
  ```
221
251
 
222
- There is no built-in pin gesture - pin from wherever suits, most naturally the
223
- row context menu, through TanStack's own `row.pin("top" | "bottom" | false)`,
224
- `row.getIsPinned()` and `row.getCanPin()`. Pinned rows leave the scrolling order
225
- and render in sticky blocks: top under the header, bottom above the summary row.
252
+ There is no built-in pin gesture and no pin icon - build one from TanStack's own
253
+ `row.pin("top" | "bottom" | false)`, `row.getIsPinned()` and `row.getCanPin()`,
254
+ either as a display column whose cell is a pin button or in the row context
255
+ menu. Read the pinned state through a subscription
256
+ (`useSelector(row.table.store, () => row.getIsPinned())`) rather than calling it
257
+ in a component body: the `row` identity survives a pin, so the React Compiler
258
+ would cache the call and the icon would never change. Whichever gesture you
259
+ build, put unpin in it too. Pinned rows leave the scrolling order and render in
260
+ sticky blocks: top under the header, bottom above the summary row.
226
261
 
227
262
  Pinned rows are still body rows - selection, editing, details, the context menu
228
263
  and per-row styling all behave normally. What they sit out are the statements
@@ -31,11 +31,12 @@ All are props of `TMDataGrid.Table`, not hook options.
31
31
  | `onCellClick` | `(args) => void` | Cell click. `args` is `TMDataGridCellEventArgs`. |
32
32
  | `onCellDoubleClick` | `(args) => void` | Cell double-click. |
33
33
  | `onCellContextMenu` | `(args) => void` | Cell right-click. |
34
- | `rowContextMenu` | `({ table, row, cell, close }) => ReactNode` | Contents of the row's context menu. `null` for no menu. |
34
+ | `renderRowContextMenu` | `({ table, row, cell, close, internalItems }) => ReactNode` | Contents of the row's context menu. `null` for no menu. Reading `internalItems` hands the composition over. |
35
+ | `renderColumnMenuItems` | `({ column, table, internalItems }) => ReactNode[]` | Contents of a column's menu. An empty list removes the button. |
35
36
  | `rowContextMenuProps` | `MenuProps` | Passed to the Mantine `Menu` untouched apart from its open state. |
36
37
 
37
38
  `TMDataGridCellEventArgs` is `{ cell, row, column, event }`. The context-menu
38
- render prop's `cell` is `null` only when a custom cell renderer stopped the
39
+ slot's `cell` is `null` only when a custom cell renderer stopped the
39
40
  event. One `Menu` serves the whole body: a closed Mantine `Popover` still runs
40
41
  its hooks on every render, and the virtualized body re-renders every scroll
41
42
  frame.
@@ -10,7 +10,7 @@ description: >
10
10
  metadata:
11
11
  type: core
12
12
  library: '@jielga/tmdatagrid'
13
- library_version: '1.0.2'
13
+ library_version: '1.1.1'
14
14
  sources:
15
15
  - 'Jielga/TMDataGrid:src/docs/server-side.md'
16
16
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '1.0.2'
14
+ library_version: '1.1.1'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/testing.md'
17
17
  - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGrid.tsx'
@@ -64,7 +64,7 @@ export function TMDataGridCellEditor({
64
64
  row: Row<TMDataGridFeatures, TMDataGridRowData>;
65
65
  /** Consumes the pending type-to-edit seed, when typing opened this editor. */
66
66
  takeSeedText: () => string | undefined;
67
- /** In row mode only the first editable cell takes the focus. */
67
+ /** @deprecated The grid places the caret; see `TMDataGridEditorArgs`. */
68
68
  autoFocus?: boolean;
69
69
  /** After the editor closed itself - the table moves the focus. */
70
70
  onClose: (args: TMDataGridCellEditorClose) => void;
@@ -1,7 +1,124 @@
1
1
  import { Button, Group } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
+ import type { ReactNode } from "react";
3
4
  import { useTMDataGridContext } from "../TMDataGridContext";
4
5
 
6
+ /**
7
+ * How many rows are waiting to be saved: dirty edits, entry rows (an add is
8
+ * pending whether or not it was touched), and deletion marks.
9
+ */
10
+ function usePendingCount(): number {
11
+ const { edit } = useTMDataGridContext();
12
+ return useSelector(
13
+ edit.store,
14
+ (state) =>
15
+ state.openRowIds.filter(
16
+ (rowId) =>
17
+ state.newRows.some((newRow) => newRow.tempId === rowId) ||
18
+ (state.rows[rowId]?.dirtyFields.length ?? 0) > 0,
19
+ ).length + state.deletedRowIds.length,
20
+ );
21
+ }
22
+
23
+ /*
24
+ * Module scope, like the pagination controls and for the same reason: a
25
+ * component defined inside the render body is a new type every render, so
26
+ * React would remount the buttons underneath an interaction.
27
+ */
28
+
29
+ function EditSaveButton() {
30
+ const { edit, labels } = useTMDataGridContext();
31
+ const pendingCount = usePendingCount();
32
+ const isSubmitting = useSelector(edit.store, (state) =>
33
+ state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
34
+ );
35
+
36
+ return (
37
+ <Button
38
+ size="compact-sm"
39
+ disabled={pendingCount === 0}
40
+ loading={isSubmitting}
41
+ data-dg-part="save-all"
42
+ onClick={() => void edit.submitAll()}
43
+ >
44
+ {labels.saveAllEdits(pendingCount)}
45
+ </Button>
46
+ );
47
+ }
48
+
49
+ function EditDiscardButton() {
50
+ const { edit, labels } = useTMDataGridContext();
51
+ const pendingCount = usePendingCount();
52
+
53
+ return (
54
+ <Button
55
+ variant="subtle"
56
+ color="gray"
57
+ size="compact-sm"
58
+ disabled={pendingCount === 0}
59
+ data-dg-part="discard-all"
60
+ onClick={() => edit.cancelAll()}
61
+ >
62
+ {labels.discardAllEdits}
63
+ </Button>
64
+ );
65
+ }
66
+
67
+ const EDIT_ACTIONS_CONTROLS: TMDataGridEditActionsControls = {
68
+ Save: EditSaveButton,
69
+ Discard: EditDiscardButton,
70
+ };
71
+
72
+ /** What the edit chrome is showing. */
73
+ export type TMDataGridEditActionsState = {
74
+ /** Rows with unsaved work: dirty edits, entry rows, and deletion marks. */
75
+ pendingCount: number;
76
+ /** Whether a submit is in flight. */
77
+ isSubmitting: boolean;
78
+ };
79
+
80
+ /** What the edit chrome can do. */
81
+ export type TMDataGridEditActionsActions = {
82
+ /** Commits every open row. Resolves `false` when a row stayed open. */
83
+ save: () => Promise<boolean>;
84
+ /** Drops every draft. */
85
+ discard: () => void;
86
+ };
87
+
88
+ /** The pre-bound pieces of the built-in edit chrome. */
89
+ export type TMDataGridEditActionsControls = {
90
+ /** Save, with the pending count, disabled while nothing is pending. */
91
+ Save: () => ReactNode;
92
+ /** Discard, disabled while nothing is pending. */
93
+ Discard: () => ReactNode;
94
+ };
95
+
96
+ /** What {@link TMDataGridEditActionsProps.renderActions} is handed. */
97
+ export type TMDataGridEditActionsSlotArgs = {
98
+ state: TMDataGridEditActionsState;
99
+ actions: TMDataGridEditActionsActions;
100
+ Controls: TMDataGridEditActionsControls;
101
+ };
102
+
103
+ export type TMDataGridEditActionsProps = {
104
+ /**
105
+ * Replaces the built-in Save/Discard pair, and is handed the pieces of it.
106
+ *
107
+ * ```tsx
108
+ * <TMDataGrid.EditActions
109
+ * renderActions={({ state, Controls }) => (
110
+ * <Group>
111
+ * {state.pendingCount > 0 && <Badge>{state.pendingCount}</Badge>}
112
+ * <Controls.Save />
113
+ * <Controls.Discard />
114
+ * </Group>
115
+ * )}
116
+ * />
117
+ * ```
118
+ */
119
+ renderActions?: (args: TMDataGridEditActionsSlotArgs) => ReactNode;
120
+ };
121
+
5
122
  /**
6
123
  * Batch mode's toolbar chrome: Save with the dirty-row count, and Discard.
7
124
  * Both read the edit store, so they grey out while nothing is dirty and the
@@ -17,46 +134,29 @@ import { useTMDataGridContext } from "../TMDataGridContext";
17
134
  * </TMDataGrid.Toolbar>
18
135
  * ```
19
136
  */
20
- export function TMDataGridEditActions() {
21
- const { edit, features, labels } = useTMDataGridContext();
22
- // Everything pending counts: dirty edits, entry rows (an add is pending
23
- // whether or not it was touched), and deletion marks.
24
- const dirtyCount = useSelector(
25
- edit.store,
26
- (state) =>
27
- state.openRowIds.filter(
28
- (rowId) =>
29
- state.newRows.some((newRow) => newRow.tempId === rowId) ||
30
- (state.rows[rowId]?.dirtyFields.length ?? 0) > 0,
31
- ).length + state.deletedRowIds.length,
32
- );
137
+ export function TMDataGridEditActions({
138
+ renderActions,
139
+ }: TMDataGridEditActionsProps = {}) {
140
+ const { edit, features } = useTMDataGridContext();
141
+ const pendingCount = usePendingCount();
33
142
  const isSubmitting = useSelector(edit.store, (state) =>
34
143
  state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
35
144
  );
36
145
 
37
146
  if (!features.editing) return null;
38
147
 
148
+ if (renderActions) {
149
+ return renderActions({
150
+ state: { pendingCount, isSubmitting },
151
+ actions: { save: () => edit.submitAll(), discard: () => edit.cancelAll() },
152
+ Controls: EDIT_ACTIONS_CONTROLS,
153
+ });
154
+ }
155
+
39
156
  return (
40
157
  <Group gap="xs" wrap="nowrap">
41
- <Button
42
- size="compact-sm"
43
- disabled={dirtyCount === 0}
44
- loading={isSubmitting}
45
- data-dg-part="save-all"
46
- onClick={() => void edit.submitAll()}
47
- >
48
- {labels.saveAllEdits(dirtyCount)}
49
- </Button>
50
- <Button
51
- variant="subtle"
52
- color="gray"
53
- size="compact-sm"
54
- disabled={dirtyCount === 0}
55
- data-dg-part="discard-all"
56
- onClick={() => edit.cancelAll()}
57
- >
58
- {labels.discardAllEdits}
59
- </Button>
158
+ <EditSaveButton />
159
+ <EditDiscardButton />
60
160
  </Group>
61
161
  );
62
162
  }