@gridkitjs/react 0.5.0 → 0.6.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/index.js CHANGED
@@ -1,5 +1,10 @@
1
1
  // src/DataGrid/DataGrid.tsx
2
- import { useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
2
+ import {
3
+ useImperativeHandle,
4
+ useMemo as useMemo2,
5
+ useRef as useRef4,
6
+ useState as useState5
7
+ } from "react";
3
8
  import {
4
9
  applyColumnOrder,
5
10
  defineColumnsFromRows,
@@ -13,7 +18,17 @@ import {
13
18
 
14
19
  // src/DataGrid/components/GridHeader.tsx
15
20
  import { createPortal } from "react-dom";
16
- import { KEYBOARD_STEP } from "@gridkitjs/core";
21
+ import { buildKeyShortcuts, intentOf, KEYBOARD_STEP } from "@gridkitjs/core";
22
+
23
+ // src/DataGrid/ariaAttr.ts
24
+ function ariaAttr(condition, key, value) {
25
+ return condition ? { [key]: value } : {};
26
+ }
27
+
28
+ // src/DataGrid/classNames.ts
29
+ function classNames(...values) {
30
+ return values.filter(Boolean).join(" ");
31
+ }
17
32
 
18
33
  // src/DataGrid/useGridNavigation.ts
19
34
  import {
@@ -21,16 +36,14 @@ import {
21
36
  useRef,
22
37
  useState
23
38
  } from "react";
24
- var HEADER_ROW = -1;
39
+ import {
40
+ clampFocus,
41
+ nextFocusForKey,
42
+ HEADER_ROW
43
+ } from "@gridkitjs/core";
25
44
  var FALLBACK_PAGE = 10;
26
- function clampFocus(focus, rowCount, columnCount) {
27
- return {
28
- rowIndex: Math.min(Math.max(focus.rowIndex, HEADER_ROW), rowCount - 1),
29
- columnIndex: Math.min(
30
- Math.max(focus.columnIndex, 0),
31
- Math.max(columnCount - 1, 0)
32
- )
33
- };
45
+ function tabIndexFor(columnIndex, focusedColumnIndex) {
46
+ return columnIndex === focusedColumnIndex ? 0 : -1;
34
47
  }
35
48
  function cellAt(table, focus) {
36
49
  const row = focus.rowIndex === HEADER_ROW ? table.tHead?.rows[0] ?? null : table.tBodies[0]?.rows[focus.rowIndex] ?? null;
@@ -78,83 +91,71 @@ function useGridNavigation({
78
91
  pending.current = true;
79
92
  setStored(next);
80
93
  }
81
- function tabIndexFor(rowIndex, columnIndex) {
82
- return rowIndex === focus.rowIndex && columnIndex === focus.columnIndex ? 0 : -1;
83
- }
84
94
  function onKeyDown(event) {
85
- if (event.altKey) {
95
+ const next = nextFocusForKey(
96
+ event.key,
97
+ {
98
+ ctrlKey: event.ctrlKey,
99
+ metaKey: event.metaKey,
100
+ altKey: event.altKey,
101
+ shiftKey: event.shiftKey
102
+ },
103
+ focus,
104
+ rowCount,
105
+ columnCount,
106
+ pageSize()
107
+ );
108
+ if (next === null) {
86
109
  return;
87
110
  }
88
- const { rowIndex, columnIndex } = focus;
89
- let next;
90
- switch (event.key) {
91
- case "ArrowLeft":
92
- if (event.ctrlKey) return;
93
- next = { rowIndex, columnIndex: columnIndex - 1 };
94
- break;
95
- case "ArrowRight":
96
- if (event.ctrlKey) return;
97
- next = { rowIndex, columnIndex: columnIndex + 1 };
98
- break;
99
- case "ArrowUp":
100
- next = { rowIndex: rowIndex - 1, columnIndex };
101
- break;
102
- case "ArrowDown":
103
- next = { rowIndex: rowIndex + 1, columnIndex };
104
- break;
105
- // Ctrl takes Home and End to the grid's ends rather than the row's, which
106
- // is the one place Ctrl is navigation rather than reorder.
107
- case "Home":
108
- next = event.ctrlKey ? { rowIndex: HEADER_ROW, columnIndex: 0 } : { rowIndex, columnIndex: 0 };
109
- break;
110
- case "End":
111
- next = event.ctrlKey ? { rowIndex: rowCount - 1, columnIndex: columnCount - 1 } : { rowIndex, columnIndex: columnCount - 1 };
112
- break;
113
- case "PageUp":
114
- next = { rowIndex: rowIndex - pageSize(), columnIndex };
115
- break;
116
- case "PageDown":
117
- next = { rowIndex: rowIndex + pageSize(), columnIndex };
118
- break;
119
- default:
120
- return;
121
- }
122
111
  event.preventDefault();
123
112
  focusCell(next.rowIndex, next.columnIndex);
124
113
  }
125
- return { focus, tabIndexFor, focusCell, onKeyDown };
114
+ return {
115
+ focus,
116
+ tabIndexFor: (rowIndex, columnIndex) => tabIndexFor(
117
+ columnIndex,
118
+ rowIndex === focus.rowIndex ? focus.columnIndex : null
119
+ ),
120
+ focusCell,
121
+ onKeyDown
122
+ };
126
123
  }
127
124
 
128
125
  // src/DataGrid/useGridSelection.ts
129
- import { useMemo, useRef as useRef2 } from "react";
130
126
  import {
131
- accessDotted,
127
+ useMemo,
128
+ useRef as useRef2
129
+ } from "react";
130
+ import {
131
+ applySelectionIntent,
132
132
  clearSelection,
133
133
  diffSelection,
134
+ resolveCell,
135
+ resolveColumns,
136
+ resolveRows,
134
137
  selectAll,
135
138
  selectCell,
136
- selectOnly,
137
- selectRange,
138
- toggleCellSelection,
139
- toggleSelection
139
+ toggleCellSelection
140
140
  } from "@gridkitjs/core";
141
- function intentOf(event) {
142
- if (event.shiftKey) {
143
- return "range";
141
+
142
+ // src/DataGrid/commitIfChanged.ts
143
+ function commitIfChanged(current, next, setState, onCommit) {
144
+ if (next === current) {
145
+ return false;
144
146
  }
145
- return event.ctrlKey || event.metaKey ? "toggle" : "replace";
147
+ setState(next);
148
+ onCommit?.(next);
149
+ return true;
146
150
  }
147
- function applyIntent(selection, orderedIds, anchorId, id, intent, mode) {
148
- switch (intent) {
149
- case "toggle":
150
- return toggleSelection(selection, id, mode);
151
- // A range with no anchor yet spans from the id to itself, which is the
152
- // plain click a first Shift-click may as well be.
153
- case "range":
154
- return selectRange(selection, orderedIds, anchorId ?? id, id, mode);
155
- default:
156
- return selectOnly(selection, id, mode);
157
- }
151
+
152
+ // src/DataGrid/useGridSelection.ts
153
+ function keyboardSelectIntent(event) {
154
+ return {
155
+ ctrlKey: event.key === " " || event.ctrlKey,
156
+ metaKey: event.metaKey,
157
+ shiftKey: event.shiftKey
158
+ };
158
159
  }
159
160
  function countMessage(count, noun) {
160
161
  if (count === 0) {
@@ -162,6 +163,18 @@ function countMessage(count, noun) {
162
163
  }
163
164
  return count === 1 ? `1 ${noun} selected` : `${String(count)} ${noun}s selected`;
164
165
  }
166
+ function useAsSet(values) {
167
+ return useMemo(() => new Set(values), [values]);
168
+ }
169
+ function useIndexedBy(values, key) {
170
+ return useMemo(
171
+ () => new Map(values.map((value) => [value[key], value])),
172
+ [values, key]
173
+ );
174
+ }
175
+ function useMappedArray(values, key) {
176
+ return useMemo(() => values.map((value) => value[key]), [values, key]);
177
+ }
165
178
  function useGridSelection({
166
179
  rows,
167
180
  columns,
@@ -173,141 +186,106 @@ function useGridSelection({
173
186
  cellSelection,
174
187
  setCellSelection,
175
188
  callbacks,
176
- announce
189
+ announce,
190
+ columnName
177
191
  }) {
178
192
  const rowMode = selectable?.rows ?? false;
179
193
  const columnMode = selectable?.columns ?? false;
180
194
  const cellMode = selectable?.cells ?? false;
181
- const rowsById = useMemo(
182
- () => new Map(rows.map((entry) => [entry.rowId, entry])),
183
- [rows]
184
- );
185
- const rowIds = useMemo(() => rows.map((entry) => entry.rowId), [rows]);
186
- const columnIds = useMemo(() => columns.map((entry) => entry.id), [columns]);
187
- const selectedRowIds = useMemo(() => new Set(rowSelection), [rowSelection]);
188
- const selectedColumnIds = useMemo(
189
- () => new Set(columnSelection),
190
- [columnSelection]
191
- );
195
+ const rowsById = useIndexedBy(rows, "rowId");
196
+ const rowIds = useMappedArray(rows, "rowId");
197
+ const columnIds = useMappedArray(columns, "id");
198
+ const selectedRowIds = useAsSet(rowSelection);
199
+ const selectedColumnIds = useAsSet(columnSelection);
192
200
  const rowAnchor = useRef2(null);
193
201
  const columnAnchor = useRef2(null);
194
- function resolveRows(ids) {
195
- return ids.map((id) => rowsById.get(id)).filter((row) => row !== void 0);
196
- }
197
- function resolveColumns(ids) {
198
- return ids.map((id) => {
199
- const columnIndex = columns.findIndex((entry) => entry.id === id);
200
- const column = columns[columnIndex];
201
- return column === void 0 ? void 0 : { columnId: id, column, columnIndex };
202
- }).filter((column) => column !== void 0);
203
- }
204
- function resolveCell(ref) {
205
- if (ref === null) {
206
- return null;
207
- }
208
- const entry = rowsById.get(ref.rowId);
209
- const columnIndex = columns.findIndex(
210
- (candidate) => candidate.id === ref.columnId
211
- );
212
- const column = columns[columnIndex];
213
- if (entry === void 0 || column === void 0) {
214
- return null;
215
- }
216
- return {
217
- rowId: ref.rowId,
218
- columnId: ref.columnId,
219
- row: entry.row,
220
- column,
221
- rowIndex: entry.rowIndex,
222
- columnIndex,
223
- value: accessDotted(entry.row, column.column.field)
224
- };
225
- }
226
202
  function announceCount(changed, total, noun) {
227
203
  if (changed > 1) {
228
204
  announce(countMessage(total, noun));
229
205
  }
230
206
  }
231
207
  function commitRows(next) {
232
- if (next === rowSelection) {
233
- return;
234
- }
235
- setRowSelection(next);
236
- const { added, removed } = diffSelection(rowSelection, next);
237
- const addedRows = resolveRows(added);
238
- const removedRows = resolveRows(removed);
239
- for (const row of addedRows) {
240
- callbacks.onRowSelect?.({ row, selection: next });
241
- }
242
- if (addedRows.length > 0) {
243
- callbacks.onRowsSelect?.({ rows: addedRows, selection: next });
244
- }
245
- for (const row of removedRows) {
246
- callbacks.onRowDeselect?.({ row, selection: next });
247
- }
248
- if (removedRows.length > 0) {
249
- callbacks.onRowsDeselect?.({ rows: removedRows, selection: next });
250
- }
251
- callbacks.onRowSelectionChange?.({
252
- added: addedRows,
253
- removed: removedRows,
254
- selected: resolveRows(next),
255
- selection: next
208
+ commitIfChanged(rowSelection, next, setRowSelection, (committed) => {
209
+ const { added, removed } = diffSelection(rowSelection, committed);
210
+ const addedRows = resolveRows(rowsById, added);
211
+ const removedRows = resolveRows(rowsById, removed);
212
+ for (const row of addedRows) {
213
+ callbacks.onRowSelect?.({ row, selection: committed });
214
+ }
215
+ if (addedRows.length > 0) {
216
+ callbacks.onRowsSelect?.({ rows: addedRows, selection: committed });
217
+ }
218
+ for (const row of removedRows) {
219
+ callbacks.onRowDeselect?.({ row, selection: committed });
220
+ }
221
+ if (removedRows.length > 0) {
222
+ callbacks.onRowsDeselect?.({ rows: removedRows, selection: committed });
223
+ }
224
+ callbacks.onRowSelectionChange?.({
225
+ added: addedRows,
226
+ removed: removedRows,
227
+ selected: resolveRows(rowsById, committed),
228
+ selection: committed
229
+ });
230
+ announceCount(added.length + removed.length, committed.length, "row");
256
231
  });
257
- announceCount(added.length + removed.length, next.length, "row");
258
232
  }
259
233
  function commitColumns(next) {
260
- if (next === columnSelection) {
261
- return;
262
- }
263
- setColumnSelection(next);
264
- const { added, removed } = diffSelection(columnSelection, next);
265
- const addedColumns = resolveColumns(added);
266
- const removedColumns = resolveColumns(removed);
267
- for (const column of addedColumns) {
268
- callbacks.onColumnSelect?.({ column, selection: next });
269
- }
270
- if (addedColumns.length > 0) {
271
- callbacks.onColumnsSelect?.({ columns: addedColumns, selection: next });
272
- }
273
- for (const column of removedColumns) {
274
- callbacks.onColumnDeselect?.({ column, selection: next });
275
- }
276
- if (removedColumns.length > 0) {
277
- callbacks.onColumnsDeselect?.({
278
- columns: removedColumns,
279
- selection: next
234
+ commitIfChanged(columnSelection, next, setColumnSelection, (committed) => {
235
+ const { added, removed } = diffSelection(columnSelection, committed);
236
+ const addedColumns = resolveColumns(columns, added);
237
+ const removedColumns = resolveColumns(columns, removed);
238
+ for (const column of addedColumns) {
239
+ callbacks.onColumnSelect?.({ column, selection: committed });
240
+ }
241
+ if (addedColumns.length > 0) {
242
+ callbacks.onColumnsSelect?.({
243
+ columns: addedColumns,
244
+ selection: committed
245
+ });
246
+ }
247
+ for (const column of removedColumns) {
248
+ callbacks.onColumnDeselect?.({ column, selection: committed });
249
+ }
250
+ if (removedColumns.length > 0) {
251
+ callbacks.onColumnsDeselect?.({
252
+ columns: removedColumns,
253
+ selection: committed
254
+ });
255
+ }
256
+ callbacks.onColumnSelectionChange?.({
257
+ added: addedColumns,
258
+ removed: removedColumns,
259
+ selected: resolveColumns(columns, committed),
260
+ selection: committed
280
261
  });
281
- }
282
- callbacks.onColumnSelectionChange?.({
283
- added: addedColumns,
284
- removed: removedColumns,
285
- selected: resolveColumns(next),
286
- selection: next
262
+ announceCount(added.length + removed.length, committed.length, "column");
287
263
  });
288
- announceCount(added.length + removed.length, next.length, "column");
289
264
  }
290
265
  function commitCell(next) {
291
- if (next === cellSelection) {
292
- return;
293
- }
294
- setCellSelection(next);
295
- const selected = resolveCell(next);
296
- const deselected = resolveCell(cellSelection);
297
- if (deselected !== null) {
298
- callbacks.onCellDeselect?.({ cell: deselected, selection: next });
299
- }
300
- if (selected !== null) {
301
- callbacks.onCellSelect?.({ cell: selected, selection: next });
302
- }
303
- callbacks.onCellSelectionChange?.({
304
- selected,
305
- deselected,
306
- selection: next
266
+ commitIfChanged(cellSelection, next, setCellSelection, (committed) => {
267
+ const selected = resolveCell(rowsById, columns, committed);
268
+ const deselected = resolveCell(rowsById, columns, cellSelection);
269
+ if (deselected !== null) {
270
+ callbacks.onCellDeselect?.({ cell: deselected, selection: committed });
271
+ }
272
+ if (selected !== null) {
273
+ callbacks.onCellSelect?.({ cell: selected, selection: committed });
274
+ }
275
+ callbacks.onCellSelectionChange?.({
276
+ selected,
277
+ deselected,
278
+ selection: committed
279
+ });
280
+ if (selected !== null) {
281
+ announce(
282
+ `${columnName(selected.columnId)}, row ${String(selected.rowIndex + 1)}, selected`
283
+ );
284
+ }
307
285
  });
308
286
  }
309
287
  function selectRow(rowId, intent) {
310
- const next = applyIntent(
288
+ const next = applySelectionIntent(
311
289
  rowSelection,
312
290
  rowIds,
313
291
  rowAnchor.current,
@@ -321,7 +299,7 @@ function useGridSelection({
321
299
  commitRows(next);
322
300
  }
323
301
  function selectColumn(columnId, intent) {
324
- const next = applyIntent(
302
+ const next = applySelectionIntent(
325
303
  columnSelection,
326
304
  columnIds,
327
305
  columnAnchor.current,
@@ -364,8 +342,8 @@ function useGridSelection({
364
342
 
365
343
  // src/DataGrid/components/GridHeader.tsx
366
344
  import { jsx, jsxs } from "react/jsx-runtime";
367
- function ChevronsUpDownIcon() {
368
- return /* @__PURE__ */ jsxs(
345
+ function Icon({ children }) {
346
+ return /* @__PURE__ */ jsx(
369
347
  "svg",
370
348
  {
371
349
  viewBox: "0 0 24 24",
@@ -375,27 +353,18 @@ function ChevronsUpDownIcon() {
375
353
  strokeLinecap: "round",
376
354
  strokeLinejoin: "round",
377
355
  "aria-hidden": "true",
378
- children: [
379
- /* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5" }),
380
- /* @__PURE__ */ jsx("path", { d: "m7 9 5-5 5 5" })
381
- ]
356
+ children
382
357
  }
383
358
  );
384
359
  }
360
+ function ChevronsUpDownIcon() {
361
+ return /* @__PURE__ */ jsxs(Icon, { children: [
362
+ /* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5" }),
363
+ /* @__PURE__ */ jsx("path", { d: "m7 9 5-5 5 5" })
364
+ ] });
365
+ }
385
366
  function ChevronUpIcon() {
386
- return /* @__PURE__ */ jsx(
387
- "svg",
388
- {
389
- viewBox: "0 0 24 24",
390
- fill: "none",
391
- stroke: "currentColor",
392
- strokeWidth: 2,
393
- strokeLinecap: "round",
394
- strokeLinejoin: "round",
395
- "aria-hidden": "true",
396
- children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" })
397
- }
398
- );
367
+ return /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" }) });
399
368
  }
400
369
  function GridHeader({
401
370
  columns,
@@ -419,21 +388,24 @@ function GridHeader({
419
388
  const sortable = column.sortable ?? sortableColumns;
420
389
  const sortDirection = sort.directionFor(entry.id);
421
390
  const sortPriority = sort.priorityFor(entry.id);
422
- const shortcuts = [
423
- entry.reorderable ? "Control+ArrowLeft Control+ArrowRight" : "",
424
- entry.resizable ? "Alt+ArrowLeft Alt+ArrowRight" : "",
425
- sortable ? "Alt+ArrowUp" : ""
426
- ].filter(Boolean).join(" ");
391
+ const shortcuts = buildKeyShortcuts({
392
+ reorderable: entry.reorderable,
393
+ resizable: entry.resizable,
394
+ sortable
395
+ });
427
396
  return /* @__PURE__ */ jsxs(
428
397
  "th",
429
398
  {
430
399
  scope: "col",
400
+ role: "columnheader",
431
401
  "data-gridkit-column": entry.id,
432
402
  "aria-colindex": index + 1,
433
403
  tabIndex: nav.tabIndexFor(HEADER_ROW, index),
434
- ...selection.columnMode !== false && {
435
- "aria-selected": selected
436
- },
404
+ ...ariaAttr(
405
+ selection.columnMode !== false,
406
+ "aria-selected",
407
+ selected
408
+ ),
437
409
  onFocus: () => {
438
410
  nav.focusCell(HEADER_ROW, index);
439
411
  },
@@ -459,6 +431,11 @@ function GridHeader({
459
431
  resize.nudge(entry, direction * KEYBOARD_STEP);
460
432
  return;
461
433
  }
434
+ if (event.key === "Enter" && event.altKey && entry.resizable) {
435
+ event.preventDefault();
436
+ resize.sizeToContent(entry);
437
+ return;
438
+ }
462
439
  if (event.key === "ArrowUp" && event.altKey && sortable) {
463
440
  event.preventDefault();
464
441
  sort.toggle(entry, { shiftKey: event.shiftKey });
@@ -468,17 +445,13 @@ function GridHeader({
468
445
  event.preventDefault();
469
446
  selection.selectColumn(
470
447
  entry.id,
471
- intentOf({
472
- ctrlKey: event.key === " " || event.ctrlKey,
473
- metaKey: event.metaKey,
474
- shiftKey: event.shiftKey
475
- })
448
+ intentOf(keyboardSelectIntent(event))
476
449
  );
477
450
  return;
478
451
  }
479
452
  nav.onKeyDown(event);
480
453
  },
481
- className: [
454
+ className: classNames(
482
455
  "header-cell",
483
456
  entry.reorderable ? "is-reorderable" : "",
484
457
  selected ? "is-selected" : "",
@@ -488,11 +461,13 @@ function GridHeader({
488
461
  dropAfter ? "is-drop-after" : "",
489
462
  column.wrap?.header ? "is-wrapped" : "",
490
463
  column.headerClassName ?? ""
491
- ].filter(Boolean).join(" "),
492
- ...shortcuts !== "" && { "aria-keyshortcuts": shortcuts },
493
- ...sortable && sortDirection !== null && {
494
- "aria-sort": sortDirection === "asc" ? "ascending" : "descending"
495
- },
464
+ ),
465
+ ...ariaAttr(shortcuts !== "", "aria-keyshortcuts", shortcuts),
466
+ ...ariaAttr(
467
+ sortable && sortDirection !== null,
468
+ "aria-sort",
469
+ sortDirection === "asc" ? "ascending" : "descending"
470
+ ),
496
471
  ...entry.reorderable && {
497
472
  onPointerDown: (event) => {
498
473
  drag.startDrag(entry, event);
@@ -509,10 +484,10 @@ function GridHeader({
509
484
  /* @__PURE__ */ jsxs(
510
485
  "span",
511
486
  {
512
- className: [
487
+ className: classNames(
513
488
  "header-sort-toggle",
514
489
  sortDirection !== null ? `is-sorted-${sortDirection}` : ""
515
- ].filter(Boolean).join(" "),
490
+ ),
516
491
  "aria-hidden": "true",
517
492
  tabIndex: -1,
518
493
  onPointerDown: (event) => {
@@ -570,8 +545,11 @@ function GridHeader({
570
545
  ] });
571
546
  }
572
547
 
548
+ // src/DataGrid/components/GridBody.tsx
549
+ import { intentOf as intentOf2 } from "@gridkitjs/core";
550
+
573
551
  // src/DataGrid/components/GridRow.tsx
574
- import { accessDotted as accessDotted2 } from "@gridkitjs/core";
552
+ import { accessDotted } from "@gridkitjs/core";
575
553
  import { memo } from "react";
576
554
  import { jsx as jsx2 } from "react/jsx-runtime";
577
555
  function GridRowComponent({
@@ -592,10 +570,10 @@ function GridRowComponent({
592
570
  {
593
571
  role: "row",
594
572
  "aria-rowindex": rowIndex + 2,
595
- ...rowsSelectable && { "aria-selected": selected },
596
- className: ["grid-row", selected ? "is-selected" : ""].filter(Boolean).join(" "),
573
+ ...ariaAttr(rowsSelectable, "aria-selected", selected),
574
+ className: classNames("grid-row", selected ? "is-selected" : ""),
597
575
  children: columns.map(({ id, column, alignment }, columnIndex) => {
598
- const value = accessDotted2(row, column.field);
576
+ const value = accessDotted(row, column.field);
599
577
  const cellSelected = id === selectedColumnId;
600
578
  return /* @__PURE__ */ jsx2(
601
579
  "td",
@@ -603,15 +581,16 @@ function GridRowComponent({
603
581
  role: "gridcell",
604
582
  "data-gridkit-column": id,
605
583
  "aria-colindex": columnIndex + 1,
606
- tabIndex: columnIndex === focusedColumnIndex ? 0 : -1,
607
- ...cellsSelectable && { "aria-selected": cellSelected },
608
- className: [
584
+ tabIndex: tabIndexFor(columnIndex, focusedColumnIndex),
585
+ ...ariaAttr(cellsSelectable, "aria-selected", cellSelected),
586
+ ...ariaAttr(cellsSelectable, "aria-keyshortcuts", "Space Enter"),
587
+ className: classNames(
609
588
  "grid-cell",
610
589
  id === activeColumnId ? "is-resizing" : "",
611
590
  cellSelected || selectedColumnIds.has(id) ? "is-selected" : "",
612
591
  column.wrap?.cells ? "is-wrapped" : "",
613
592
  column.cellClassName ?? ""
614
- ].filter(Boolean).join(" "),
593
+ ),
615
594
  style: { textAlign: alignment },
616
595
  children: column.cellTemplate ? column.cellTemplate({ value, row, rowIndex, rowId, selected }) : value
617
596
  },
@@ -656,7 +635,7 @@ function GridBody({
656
635
  return position === null || entry === void 0 ? null : { position, rowId: entry.rowId };
657
636
  }
658
637
  function select(rowId, columnId, modifiers) {
659
- const intent = intentOf(modifiers);
638
+ const intent = intentOf2(modifiers);
660
639
  if (rowMode !== false) {
661
640
  selection.selectRow(rowId, intent);
662
641
  }
@@ -683,11 +662,11 @@ function GridBody({
683
662
  const found = targetOf(event.target);
684
663
  if (found === null) return;
685
664
  event.preventDefault();
686
- select(found.rowId, found.position.columnId, {
687
- ctrlKey: event.key === " " || event.ctrlKey,
688
- metaKey: event.metaKey,
689
- shiftKey: event.shiftKey
690
- });
665
+ select(
666
+ found.rowId,
667
+ found.position.columnId,
668
+ keyboardSelectIntent(event)
669
+ );
691
670
  return;
692
671
  }
693
672
  nav.onKeyDown(event);
@@ -720,8 +699,45 @@ import {
720
699
  } from "react";
721
700
  import {
722
701
  movesColumn,
723
- resolveDropBefore
702
+ resolveDropBefore,
703
+ resolveKeyboardDropTarget
724
704
  } from "@gridkitjs/core";
705
+
706
+ // src/DataGrid/pointerGesture.ts
707
+ function startPointerGesture(element, pointerId, handlers) {
708
+ element.setPointerCapture(pointerId);
709
+ function stop() {
710
+ element.removeEventListener("pointermove", move);
711
+ element.removeEventListener("pointerup", end);
712
+ element.removeEventListener("pointercancel", cancel);
713
+ window.removeEventListener("keydown", onKeyDown);
714
+ if (element.hasPointerCapture(pointerId)) {
715
+ element.releasePointerCapture(pointerId);
716
+ }
717
+ }
718
+ function move(event) {
719
+ handlers.onMove(event);
720
+ }
721
+ function end() {
722
+ stop();
723
+ handlers.onEnd();
724
+ }
725
+ function cancel() {
726
+ stop();
727
+ handlers.onCancel();
728
+ }
729
+ function onKeyDown(event) {
730
+ if (event.key === "Escape") {
731
+ cancel();
732
+ }
733
+ }
734
+ element.addEventListener("pointermove", move);
735
+ element.addEventListener("pointerup", end);
736
+ element.addEventListener("pointercancel", cancel);
737
+ window.addEventListener("keydown", onKeyDown);
738
+ }
739
+
740
+ // src/DataGrid/useColumnDrag.ts
725
741
  var DRAG_THRESHOLD = 4;
726
742
  function headerAt(x, y) {
727
743
  const element = document.elementFromPoint(x, y);
@@ -753,70 +769,49 @@ function useColumnDrag({
753
769
  function placeGhost(x, y) {
754
770
  setGhostTransform(`translate(${String(x - grabX)}px, ${String(y)}px)`);
755
771
  }
756
- header.setPointerCapture(pointerId);
757
- function move(moveEvent) {
758
- const { clientX, clientY } = moveEvent;
759
- if (!dragging) {
760
- if (Math.abs(clientX - startX) < DRAG_THRESHOLD) {
761
- return;
762
- }
763
- dragging = true;
764
- dragged.current = true;
765
- setDraggedColumnId(entry.id);
766
- }
767
- placeGhost(clientX, clientY);
768
- const over = headerAt(clientX, clientY);
769
- const overId = over?.getAttribute("data-gridkit-column");
770
- const beforeId = over && overId ? resolveDropBefore(order, overId, sideOf(over, clientX)) : void 0;
771
- target = beforeId !== void 0 && movesColumn(order, entry.id, beforeId) ? { kind: "column-order", beforeId } : null;
772
- setDropTarget(target);
773
- }
774
- function stop() {
775
- header.removeEventListener("pointermove", move);
776
- header.removeEventListener("pointerup", end);
777
- header.removeEventListener("pointercancel", cancel);
778
- window.removeEventListener("keydown", onKeyDown);
779
- if (header.hasPointerCapture(pointerId)) {
780
- header.releasePointerCapture(pointerId);
781
- }
772
+ function reset() {
782
773
  setGhostTransform(null);
783
774
  setDraggedColumnId(null);
784
775
  setDropTarget(null);
785
776
  }
786
- function end() {
787
- const dropped = target;
788
- stop();
789
- if (dragging && dropped !== null) {
790
- onDrop(dropped, entry.id);
791
- }
792
- }
793
- function cancel() {
794
- target = null;
795
- stop();
796
- }
797
- function onKeyDown(keyEvent) {
798
- if (keyEvent.key === "Escape") {
799
- cancel();
777
+ startPointerGesture(header, pointerId, {
778
+ onMove(moveEvent) {
779
+ const { clientX, clientY } = moveEvent;
780
+ if (!dragging) {
781
+ if (Math.abs(clientX - startX) < DRAG_THRESHOLD) {
782
+ return;
783
+ }
784
+ dragging = true;
785
+ dragged.current = true;
786
+ setDraggedColumnId(entry.id);
787
+ }
788
+ placeGhost(clientX, clientY);
789
+ const over = headerAt(clientX, clientY);
790
+ const overId = over?.getAttribute("data-gridkit-column");
791
+ const beforeId = over && overId ? resolveDropBefore(order, overId, sideOf(over, clientX)) : void 0;
792
+ target = beforeId !== void 0 && movesColumn(order, entry.id, beforeId) ? { kind: "column-order", beforeId } : null;
793
+ setDropTarget(target);
794
+ },
795
+ onEnd() {
796
+ const dropped = target;
797
+ reset();
798
+ if (dragging && dropped !== null) {
799
+ onDrop(dropped, entry.id);
800
+ }
801
+ },
802
+ // Escape and a cancelled pointer both leave the order as it was.
803
+ onCancel() {
804
+ target = null;
805
+ reset();
800
806
  }
801
- }
802
- header.addEventListener("pointermove", move);
803
- header.addEventListener("pointerup", end);
804
- header.addEventListener("pointercancel", cancel);
805
- window.addEventListener("keydown", onKeyDown);
807
+ });
806
808
  }
807
809
  function moveByKeyboard(entry, direction) {
808
- const index = order.indexOf(entry.id);
809
- if (index === -1) {
810
- return;
811
- }
812
- const targetIndex = direction === -1 ? index - 1 : index + 2;
813
- if (targetIndex < 0) {
810
+ const beforeId = resolveKeyboardDropTarget(order, entry.id, direction);
811
+ if (beforeId === void 0) {
814
812
  return;
815
813
  }
816
- onDrop(
817
- { kind: "column-order", beforeId: order[targetIndex] ?? null },
818
- entry.id
819
- );
814
+ onDrop({ kind: "column-order", beforeId }, entry.id);
820
815
  }
821
816
  return {
822
817
  draggedColumnId,
@@ -837,6 +832,7 @@ import {
837
832
  beginColumnResize,
838
833
  clampColumnWidth,
839
834
  resolveColumnConstraints,
835
+ revertColumnSize,
840
836
  sizeColumnToContent
841
837
  } from "@gridkitjs/core";
842
838
 
@@ -885,57 +881,39 @@ function useColumnResize({
885
881
  let width = session.startWidth;
886
882
  event.preventDefault();
887
883
  event.stopPropagation();
888
- handle.setPointerCapture(pointerId);
889
884
  setActiveColumnId(session.columnId);
890
- function move(moveEvent) {
891
- width = applyColumnResize(session, moveEvent.clientX);
892
- commit(base, session.columnId, width, "move");
893
- }
894
- function stop() {
895
- handle.removeEventListener("pointermove", move);
896
- handle.removeEventListener("pointerup", end);
897
- handle.removeEventListener("pointercancel", cancel);
898
- window.removeEventListener("keydown", onKeyDown);
899
- if (handle.hasPointerCapture(pointerId)) {
900
- handle.releasePointerCapture(pointerId);
901
- }
902
- setActiveColumnId(null);
903
- }
904
- function end() {
905
- stop();
906
- if (width !== session.startWidth) {
907
- commit(base, session.columnId, width, "end");
908
- }
909
- }
910
- function cancel() {
911
- stop();
912
- setSizing((current) => {
913
- const next = Object.fromEntries(
914
- Object.entries(current).filter(
915
- ([columnId]) => columnId !== session.columnId
916
- )
917
- );
918
- if (session.columnId in base) {
919
- next[session.columnId] = base[session.columnId];
885
+ startPointerGesture(handle, pointerId, {
886
+ onMove(moveEvent) {
887
+ width = applyColumnResize(session, moveEvent.clientX);
888
+ commit(base, session.columnId, width, "move");
889
+ },
890
+ onEnd() {
891
+ setActiveColumnId(null);
892
+ if (width !== session.startWidth) {
893
+ commit(base, session.columnId, width, "end");
920
894
  }
921
- onColumnResize?.({
922
- columnId: session.columnId,
923
- width: session.startWidth,
924
- sizing: next,
925
- phase: "end"
895
+ },
896
+ /**
897
+ * Escape puts back only the dragged column's pre-drag width, merged
898
+ * into whatever the sizing state is *now* — not the `base` snapshot
899
+ * taken at drag-start — so a different column resized via keyboard
900
+ * while this drag was still open keeps its change instead of being
901
+ * clobbered by a stale restore.
902
+ */
903
+ onCancel() {
904
+ setActiveColumnId(null);
905
+ setSizing((current) => {
906
+ const next = revertColumnSize(current, base, session.columnId);
907
+ onColumnResize?.({
908
+ columnId: session.columnId,
909
+ width: session.startWidth,
910
+ sizing: next,
911
+ phase: "end"
912
+ });
913
+ return next;
926
914
  });
927
- return next;
928
- });
929
- }
930
- function onKeyDown(keyEvent) {
931
- if (keyEvent.key === "Escape") {
932
- cancel();
933
915
  }
934
- }
935
- handle.addEventListener("pointermove", move);
936
- handle.addEventListener("pointerup", end);
937
- handle.addEventListener("pointercancel", cancel);
938
- window.addEventListener("keydown", onKeyDown);
916
+ });
939
917
  }
940
918
  function sizeToContent(entry) {
941
919
  const table = tableRef.current;
@@ -983,11 +961,9 @@ function useColumnSort({
983
961
  }) {
984
962
  function toggle(entry, event) {
985
963
  const next = toggleColumnSort(sort, entry.id, { stack: event.shiftKey });
986
- if (next === sort) {
987
- return;
988
- }
989
- setSort(next);
990
- onColumnSortChange?.({ columnId: entry.id, sort: next });
964
+ commitIfChanged(sort, next, setSort, (committed) => {
965
+ onColumnSortChange?.({ columnId: entry.id, sort: committed });
966
+ });
991
967
  }
992
968
  return {
993
969
  sort,
@@ -1044,6 +1020,7 @@ function DataGridComponent({
1044
1020
  defaultFilter,
1045
1021
  label,
1046
1022
  labelledBy,
1023
+ ref,
1047
1024
  ...callbacks
1048
1025
  }) {
1049
1026
  const hoverRows = hoverable?.rows ?? true;
@@ -1186,9 +1163,42 @@ function DataGridComponent({
1186
1163
  cellSelection,
1187
1164
  setCellSelection,
1188
1165
  callbacks,
1189
- announce
1166
+ announce,
1167
+ columnName
1190
1168
  });
1191
1169
  const multiselectable = selection.rowMode === "multiple" || selection.columnMode === "multiple";
1170
+ useImperativeHandle(ref, () => ({
1171
+ get element() {
1172
+ return viewportRef.current;
1173
+ },
1174
+ get table() {
1175
+ return tableRef.current;
1176
+ },
1177
+ getRows: () => shownRows,
1178
+ getColumns: () => resolved,
1179
+ getColumnSizing: () => sizing,
1180
+ getColumnOrder: () => order,
1181
+ getColumnSort: () => sort,
1182
+ getRowSelection: () => rowSelection,
1183
+ getColumnSelection: () => columnSelection,
1184
+ getCellSelection: () => cellSelection,
1185
+ getFocusedCell: () => nav.focus,
1186
+ focusCell: nav.focusCell,
1187
+ clearSelection: selection.clear,
1188
+ selectAllRows: selection.selectAllRows,
1189
+ scrollToRow: (rowId, options) => {
1190
+ const index = shownRows.findIndex((entry) => entry.rowId === rowId);
1191
+ if (index === -1) return;
1192
+ tableRef.current?.tBodies[0]?.rows[index]?.scrollIntoView(options);
1193
+ },
1194
+ scrollToColumn: (columnId, options) => {
1195
+ const cells = tableRef.current?.querySelectorAll("[data-gridkit-column]");
1196
+ const cell = cells ? Array.from(cells).find(
1197
+ (entry) => entry.getAttribute("data-gridkit-column") === columnId
1198
+ ) : void 0;
1199
+ cell?.scrollIntoView(options);
1200
+ }
1201
+ }));
1192
1202
  return /* @__PURE__ */ jsxs2("div", { className: "gridkit-data-grid-viewport", ref: viewportRef, children: [
1193
1203
  /* @__PURE__ */ jsxs2(
1194
1204
  "table",
@@ -1197,9 +1207,13 @@ function DataGridComponent({
1197
1207
  role: "grid",
1198
1208
  "aria-rowcount": shownRows.length + 1,
1199
1209
  "aria-colcount": resolved.length,
1200
- ...multiselectable && { "aria-multiselectable": true },
1201
- ...labelledBy !== void 0 && { "aria-labelledby": labelledBy },
1202
- ...labelledBy === void 0 && label !== void 0 && { "aria-label": label },
1210
+ ...ariaAttr(multiselectable, "aria-multiselectable", true),
1211
+ ...ariaAttr(labelledBy !== void 0, "aria-labelledby", labelledBy),
1212
+ ...ariaAttr(
1213
+ labelledBy === void 0 && label !== void 0,
1214
+ "aria-label",
1215
+ label
1216
+ ),
1203
1217
  onKeyDown: (event) => {
1204
1218
  if (event.key === "Escape") {
1205
1219
  if (drag.draggedColumnId !== null || resize.activeColumnId !== null)
@@ -1214,7 +1228,7 @@ function DataGridComponent({
1214
1228
  }
1215
1229
  },
1216
1230
  style: { width: totalColumnWidth(resolved) },
1217
- className: [
1231
+ className: classNames(
1218
1232
  "gridkit-data-grid",
1219
1233
  borders ? `borders-${borders}` : "",
1220
1234
  // Hover is on by default and selection off, so one set of classes
@@ -1226,7 +1240,7 @@ function DataGridComponent({
1226
1240
  selection.rowMode === false ? "" : "selectable-rows",
1227
1241
  selection.columnMode === false ? "" : "selectable-columns",
1228
1242
  selection.cellMode === false ? "" : "selectable-cells"
1229
- ].filter(Boolean).join(" "),
1243
+ ),
1230
1244
  children: [
1231
1245
  /* @__PURE__ */ jsx4("colgroup", { children: resolved.map((entry) => /* @__PURE__ */ jsx4("col", { style: { width: entry.width } }, entry.id)) }),
1232
1246
  /* @__PURE__ */ jsx4(