@jielga/tmdatagrid 2.0.0-beta.7 → 2.0.0-beta.8
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/index.js +1093 -1007
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +1 -1
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +1 -1
- package/skills/rows/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +4 -3
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +59 -14
- package/src/tmdatagrid/components/TMDataGridTable.module.css +3 -3
- package/src/tmdatagrid/components/TMDataGridTable.tsx +88 -16
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +2 -1
- package/src/tmdatagrid/core/resizePreview.ts +141 -0
- package/src/tmdatagrid/core/useSettledTableState.ts +36 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +43 -6
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a running column resize drag paints, frame by frame.
|
|
3
|
+
*
|
|
4
|
+
* The grid's whole layout is one `grid-template-columns` on the grid element -
|
|
5
|
+
* every row follows it through `subgrid` - so a drag only has to rewrite that
|
|
6
|
+
* one string. Rendering the drag through React state instead costs a pass over
|
|
7
|
+
* every mounted cell per pointer move, which is what makes a resize feel heavy
|
|
8
|
+
* on a wide grid.
|
|
9
|
+
*
|
|
10
|
+
* This module computes the frame; `TMDataGridTable` writes it to the DOM and
|
|
11
|
+
* TanStack commits the width into `columnSizing` when the pointer is released.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** One entry of the grid's track list, paired with the column it sizes. */
|
|
15
|
+
export type TMDataGridColumnTrack = {
|
|
16
|
+
id: string;
|
|
17
|
+
/** The `grid-template-columns` entry - `120px` or `minmax(120px, 1fr)`. */
|
|
18
|
+
track: string;
|
|
19
|
+
/** The exact width where the track is exact, the floor where it is fluid. */
|
|
20
|
+
minWidth: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** TanStack's transient `columnResizing` state, narrowed to what a frame reads. */
|
|
24
|
+
export type TMDataGridResizeProgress = {
|
|
25
|
+
isResizingColumn: string | false;
|
|
26
|
+
columnSizingStart: Array<[string, number]>;
|
|
27
|
+
deltaPercentage: number | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** A column's declared bounds, as `column_getSize` reads them. */
|
|
31
|
+
export type TMDataGridSizeBounds = { minSize?: number; maxSize?: number };
|
|
32
|
+
|
|
33
|
+
/** A sticky offset the drag has moved. */
|
|
34
|
+
export type TMDataGridOffsetPatch = {
|
|
35
|
+
columnId: string;
|
|
36
|
+
side: "left" | "right";
|
|
37
|
+
offset: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TMDataGridResizePreview = {
|
|
41
|
+
gridTemplateColumns: string;
|
|
42
|
+
minWidth: number;
|
|
43
|
+
offsets: Array<TMDataGridOffsetPatch>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* TanStack's own defaults, from `getDefaultColumnSizingColumnDef()`. Not
|
|
48
|
+
* exported by the package, and the preview has to clamp exactly the way
|
|
49
|
+
* `column_getSize` will once the width is committed - a preview that rounds
|
|
50
|
+
* differently makes the column settle by a pixel when the pointer is released.
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_MIN_SIZE = 20;
|
|
53
|
+
const DEFAULT_MAX_SIZE = Number.MAX_SAFE_INTEGER;
|
|
54
|
+
|
|
55
|
+
/** The width `columnSizing` would hold if the drag ended on this delta. */
|
|
56
|
+
function draggedWidth(
|
|
57
|
+
startWidth: number,
|
|
58
|
+
deltaPercentage: number,
|
|
59
|
+
bounds: TMDataGridSizeBounds,
|
|
60
|
+
): number {
|
|
61
|
+
// Two decimals and the `max(…, 0)` are what `header_getResizeHandler`
|
|
62
|
+
// commits; the clamp around it is what `column_getSize` applies on read.
|
|
63
|
+
const dragged =
|
|
64
|
+
Math.round(Math.max(startWidth + startWidth * deltaPercentage, 0) * 100) /
|
|
65
|
+
100;
|
|
66
|
+
return Math.min(
|
|
67
|
+
Math.max(bounds.minSize ?? DEFAULT_MIN_SIZE, dragged),
|
|
68
|
+
bounds.maxSize ?? DEFAULT_MAX_SIZE,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function* laneOffsets(
|
|
73
|
+
lane: Array<string>,
|
|
74
|
+
side: "left" | "right",
|
|
75
|
+
widthOf: (columnId: string) => number,
|
|
76
|
+
): Generator<TMDataGridOffsetPatch> {
|
|
77
|
+
let offset = 0;
|
|
78
|
+
for (const columnId of lane) {
|
|
79
|
+
yield { columnId, side, offset };
|
|
80
|
+
offset += widthOf(columnId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The frame to paint for the drag in `progress`, or `null` when no drag is
|
|
86
|
+
* running - which is also the answer for the first publish of a gesture, made
|
|
87
|
+
* before the pointer has moved at all.
|
|
88
|
+
*
|
|
89
|
+
* `leftLane` and `rightLane` are the pinned lanes in the order the grid stacks
|
|
90
|
+
* them: left to right, and outermost last on the right. Their offsets come
|
|
91
|
+
* back only when the drag is resizing a pinned column, since nothing else
|
|
92
|
+
* moves them.
|
|
93
|
+
*/
|
|
94
|
+
export function resizePreview({
|
|
95
|
+
progress,
|
|
96
|
+
tracks,
|
|
97
|
+
boundsOf,
|
|
98
|
+
leftLane,
|
|
99
|
+
rightLane,
|
|
100
|
+
}: {
|
|
101
|
+
progress: TMDataGridResizeProgress;
|
|
102
|
+
tracks: Array<TMDataGridColumnTrack>;
|
|
103
|
+
boundsOf: (columnId: string) => TMDataGridSizeBounds;
|
|
104
|
+
leftLane: Array<string>;
|
|
105
|
+
rightLane: Array<string>;
|
|
106
|
+
}): TMDataGridResizePreview | null {
|
|
107
|
+
if (progress.isResizingColumn === false) return null;
|
|
108
|
+
if (progress.columnSizingStart.length === 0) return null;
|
|
109
|
+
|
|
110
|
+
const deltaPercentage = progress.deltaPercentage ?? 0;
|
|
111
|
+
const widths = new Map<string, number>();
|
|
112
|
+
for (const [columnId, startWidth] of progress.columnSizingStart) {
|
|
113
|
+
widths.set(
|
|
114
|
+
columnId,
|
|
115
|
+
draggedWidth(startWidth, deltaPercentage, boundsOf(columnId)),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const template: Array<string> = [];
|
|
120
|
+
let minWidth = 0;
|
|
121
|
+
for (const track of tracks) {
|
|
122
|
+
const width = widths.get(track.id);
|
|
123
|
+
template.push(width === undefined ? track.track : `${width}px`);
|
|
124
|
+
minWidth += width ?? track.minWidth;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const inLane = (lane: Array<string>) => lane.some((id) => widths.has(id));
|
|
128
|
+
const widthOf = (columnId: string) =>
|
|
129
|
+
widths.get(columnId) ??
|
|
130
|
+
tracks.find((track) => track.id === columnId)?.minWidth ??
|
|
131
|
+
0;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
gridTemplateColumns: template.join(" "),
|
|
135
|
+
minWidth,
|
|
136
|
+
offsets: [
|
|
137
|
+
...(inLane(leftLane) ? laneOffsets(leftLane, "left", widthOf) : []),
|
|
138
|
+
...(inLane(rightLane) ? laneOffsets(rightLane, "right", widthOf) : []),
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useSelector } from "@tanstack/react-store";
|
|
2
|
+
import { shallow } from "@tanstack/store";
|
|
3
|
+
import type { ReadonlyStore, Store } from "@tanstack/store";
|
|
4
|
+
|
|
5
|
+
/** The one slice a settled subscription leaves out. */
|
|
6
|
+
const TRANSIENT_SLICE = "columnResizing";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Subscribes to every table state slice except the transient resize state.
|
|
10
|
+
*
|
|
11
|
+
* `columnResizing` publishes a new delta on every pointer move of a resize
|
|
12
|
+
* drag. Nothing renders from those deltas - the divider highlight reads
|
|
13
|
+
* `isResizingColumn` through its own selector, and the drag itself is painted
|
|
14
|
+
* straight onto the grid's track list (see `resizePreview`) - so a component
|
|
15
|
+
* subscribed to the whole store would re-render the grid dozens of times a
|
|
16
|
+
* second for state it does not show.
|
|
17
|
+
*
|
|
18
|
+
* The comparison is shallow over the remaining slices, which is the
|
|
19
|
+
* granularity TanStack publishes at: a slice keeps its identity until it
|
|
20
|
+
* changes.
|
|
21
|
+
*/
|
|
22
|
+
export function useSettledTableState<TState extends object>(
|
|
23
|
+
store: Store<TState> | ReadonlyStore<TState>,
|
|
24
|
+
): void {
|
|
25
|
+
useSelector(
|
|
26
|
+
store,
|
|
27
|
+
(state) => {
|
|
28
|
+
const settled: Record<string, unknown> = {};
|
|
29
|
+
for (const [slice, value] of Object.entries(state)) {
|
|
30
|
+
if (slice !== TRANSIENT_SLICE) settled[slice] = value;
|
|
31
|
+
}
|
|
32
|
+
return settled;
|
|
33
|
+
},
|
|
34
|
+
{ compare: shallow },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -1137,8 +1137,44 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1137
1137
|
republishControlledStateRef.current = false;
|
|
1138
1138
|
controlledStateRef.current = controlledState;
|
|
1139
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* What the hook's own subscription watches.
|
|
1142
|
+
*
|
|
1143
|
+
* `useTable` re-renders whoever called it whenever a state slice changes
|
|
1144
|
+
* identity, and `columnResizing` publishes a new delta on every pointer move
|
|
1145
|
+
* of a resize drag - so without this the consumer's component, and the whole
|
|
1146
|
+
* grid under it, would re-render for each of those moves. The slice is
|
|
1147
|
+
* pinned to the identity it had when the drag started: a drag starting and a
|
|
1148
|
+
* drag ending still publish, the deltas in between do not.
|
|
1149
|
+
*
|
|
1150
|
+
* The live deltas stay on `table.store.state`, which is where the grid reads
|
|
1151
|
+
* them - see the resize preview in TMDataGridTable.
|
|
1152
|
+
*/
|
|
1153
|
+
const pinnedResizingRef = useRef<
|
|
1154
|
+
TableState<TMDataGridFeatures>["columnResizing"] | null
|
|
1155
|
+
>(null);
|
|
1156
|
+
const selectSettledState = useCallback(
|
|
1157
|
+
(state: TableState<TMDataGridFeatures>) => {
|
|
1158
|
+
const resizing = state.columnResizing;
|
|
1159
|
+
const pinned = pinnedResizingRef.current;
|
|
1160
|
+
if (
|
|
1161
|
+
pinned === null ||
|
|
1162
|
+
pinned.isResizingColumn !== resizing.isResizingColumn
|
|
1163
|
+
) {
|
|
1164
|
+
pinnedResizingRef.current = resizing;
|
|
1165
|
+
return state;
|
|
1166
|
+
}
|
|
1167
|
+
return { ...state, columnResizing: pinned };
|
|
1168
|
+
},
|
|
1169
|
+
[],
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1140
1172
|
const table = useTable({
|
|
1141
|
-
|
|
1173
|
+
// The grid paints a running drag itself, one style write per frame - see
|
|
1174
|
+
// the resize preview in TMDataGridTable - and takes the width into state
|
|
1175
|
+
// once, when the pointer is released. `"onChange"` publishes a width on
|
|
1176
|
+
// every pointer move instead, which re-renders the grid for each of them.
|
|
1177
|
+
columnResizeMode: "onEnd",
|
|
1142
1178
|
enableSorting: true,
|
|
1143
1179
|
enableColumnResizing: true,
|
|
1144
1180
|
// The quick search's matcher. Fuzzy by default (Q4); `"contains"` keeps
|
|
@@ -1229,7 +1265,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1229
1265
|
...persistedState.pagination,
|
|
1230
1266
|
},
|
|
1231
1267
|
},
|
|
1232
|
-
});
|
|
1268
|
+
}, selectSettledState);
|
|
1233
1269
|
|
|
1234
1270
|
// The edit engine. Built once per mount; everything it needs later - the
|
|
1235
1271
|
// table, the mode, the consumer's callbacks - is read through a ref updated
|
|
@@ -1393,10 +1429,11 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1393
1429
|
// from an effect on a state snapshot) means nothing is missed, including
|
|
1394
1430
|
// changes made straight through the table API by the consumer.
|
|
1395
1431
|
//
|
|
1396
|
-
// Writes are debounced because
|
|
1397
|
-
//
|
|
1398
|
-
// synchronously on the main thread. The
|
|
1399
|
-
// has to agree with the table once the
|
|
1432
|
+
// Writes are debounced because a state change can arrive on every pointer
|
|
1433
|
+
// move - a resize drag under `columnResizeMode: "onChange"`, a range
|
|
1434
|
+
// selection - and `setItem` serialises synchronously on the main thread. The
|
|
1435
|
+
// trailing edge is enough: storage only has to agree with the table once the
|
|
1436
|
+
// user stops.
|
|
1400
1437
|
useEffect(() => {
|
|
1401
1438
|
if (!hasPersistenceKeys(persist)) return;
|
|
1402
1439
|
writePersistedState(table.store.state, persist);
|