@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.
@@ -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
- columnResizeMode: "onChange",
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 `columnResizeMode: "onChange"` publishes a new
1397
- // state on every pointer move of a resize drag, and `setItem` serialises
1398
- // synchronously on the main thread. The trailing edge is enough: storage only
1399
- // has to agree with the table once the user stops.
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);