@gridkitjs/react 0.5.0 → 0.7.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,9 +1,15 @@
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 useMemo3,
5
+ useRef as useRef4,
6
+ useState as useState5
7
+ } from "react";
3
8
  import {
4
9
  applyColumnOrder,
5
10
  defineColumnsFromRows,
6
11
  fitColumnsToWidth,
12
+ groupRows,
7
13
  moveColumnBefore,
8
14
  resolveColumnWidths,
9
15
  resolveRowId,
@@ -13,7 +19,17 @@ import {
13
19
 
14
20
  // src/DataGrid/components/GridHeader.tsx
15
21
  import { createPortal } from "react-dom";
16
- import { KEYBOARD_STEP } from "@gridkitjs/core";
22
+ import { buildKeyShortcuts, intentOf, KEYBOARD_STEP } from "@gridkitjs/core";
23
+
24
+ // src/DataGrid/ariaAttr.ts
25
+ function ariaAttr(condition, key, value) {
26
+ return condition ? { [key]: value } : {};
27
+ }
28
+
29
+ // src/DataGrid/classNames.ts
30
+ function classNames(...values) {
31
+ return values.filter(Boolean).join(" ");
32
+ }
17
33
 
18
34
  // src/DataGrid/useGridNavigation.ts
19
35
  import {
@@ -21,20 +37,22 @@ import {
21
37
  useRef,
22
38
  useState
23
39
  } from "react";
24
- var HEADER_ROW = -1;
40
+ import {
41
+ clampFocus,
42
+ nextFocusForKey,
43
+ HEADER_ROW
44
+ } from "@gridkitjs/core";
25
45
  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
- };
46
+ function tabIndexFor(columnIndex, focusedColumnIndex) {
47
+ return columnIndex === focusedColumnIndex ? 0 : -1;
34
48
  }
35
49
  function cellAt(table, focus) {
36
50
  const row = focus.rowIndex === HEADER_ROW ? table.tHead?.rows[0] ?? null : table.tBodies[0]?.rows[focus.rowIndex] ?? null;
37
- return row?.cells[focus.columnIndex] ?? null;
51
+ if (row === null) {
52
+ return null;
53
+ }
54
+ const columnIndex = Math.min(focus.columnIndex, row.cells.length - 1);
55
+ return row.cells[columnIndex] ?? null;
38
56
  }
39
57
  function useGridNavigation({
40
58
  tableRef,
@@ -78,83 +96,71 @@ function useGridNavigation({
78
96
  pending.current = true;
79
97
  setStored(next);
80
98
  }
81
- function tabIndexFor(rowIndex, columnIndex) {
82
- return rowIndex === focus.rowIndex && columnIndex === focus.columnIndex ? 0 : -1;
83
- }
84
99
  function onKeyDown(event) {
85
- if (event.altKey) {
100
+ const next = nextFocusForKey(
101
+ event.key,
102
+ {
103
+ ctrlKey: event.ctrlKey,
104
+ metaKey: event.metaKey,
105
+ altKey: event.altKey,
106
+ shiftKey: event.shiftKey
107
+ },
108
+ focus,
109
+ rowCount,
110
+ columnCount,
111
+ pageSize()
112
+ );
113
+ if (next === null) {
86
114
  return;
87
115
  }
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
116
  event.preventDefault();
123
117
  focusCell(next.rowIndex, next.columnIndex);
124
118
  }
125
- return { focus, tabIndexFor, focusCell, onKeyDown };
119
+ return {
120
+ focus,
121
+ tabIndexFor: (rowIndex, columnIndex) => tabIndexFor(
122
+ columnIndex,
123
+ rowIndex === focus.rowIndex ? focus.columnIndex : null
124
+ ),
125
+ focusCell,
126
+ onKeyDown
127
+ };
126
128
  }
127
129
 
128
130
  // src/DataGrid/useGridSelection.ts
129
- import { useMemo, useRef as useRef2 } from "react";
130
131
  import {
131
- accessDotted,
132
+ useMemo,
133
+ useRef as useRef2
134
+ } from "react";
135
+ import {
136
+ applySelectionIntent,
132
137
  clearSelection,
133
138
  diffSelection,
139
+ resolveCell,
140
+ resolveColumns,
141
+ resolveRows,
134
142
  selectAll,
135
143
  selectCell,
136
- selectOnly,
137
- selectRange,
138
- toggleCellSelection,
139
- toggleSelection
144
+ toggleCellSelection
140
145
  } from "@gridkitjs/core";
141
- function intentOf(event) {
142
- if (event.shiftKey) {
143
- return "range";
146
+
147
+ // src/DataGrid/commitIfChanged.ts
148
+ function commitIfChanged(current, next, setState, onCommit) {
149
+ if (next === current) {
150
+ return false;
144
151
  }
145
- return event.ctrlKey || event.metaKey ? "toggle" : "replace";
152
+ setState(next);
153
+ onCommit?.(next);
154
+ return true;
146
155
  }
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
- }
156
+
157
+ // src/DataGrid/useGridSelection.ts
158
+ function keyboardSelectIntent(event) {
159
+ return {
160
+ ctrlKey: event.key === " " || event.ctrlKey,
161
+ metaKey: event.metaKey,
162
+ shiftKey: event.shiftKey
163
+ };
158
164
  }
159
165
  function countMessage(count, noun) {
160
166
  if (count === 0) {
@@ -162,6 +168,18 @@ function countMessage(count, noun) {
162
168
  }
163
169
  return count === 1 ? `1 ${noun} selected` : `${String(count)} ${noun}s selected`;
164
170
  }
171
+ function useAsSet(values) {
172
+ return useMemo(() => new Set(values), [values]);
173
+ }
174
+ function useIndexedBy(values, key) {
175
+ return useMemo(
176
+ () => new Map(values.map((value) => [value[key], value])),
177
+ [values, key]
178
+ );
179
+ }
180
+ function useMappedArray(values, key) {
181
+ return useMemo(() => values.map((value) => value[key]), [values, key]);
182
+ }
165
183
  function useGridSelection({
166
184
  rows,
167
185
  columns,
@@ -173,141 +191,106 @@ function useGridSelection({
173
191
  cellSelection,
174
192
  setCellSelection,
175
193
  callbacks,
176
- announce
194
+ announce,
195
+ columnName
177
196
  }) {
178
197
  const rowMode = selectable?.rows ?? false;
179
198
  const columnMode = selectable?.columns ?? false;
180
199
  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
- );
200
+ const rowsById = useIndexedBy(rows, "rowId");
201
+ const rowIds = useMappedArray(rows, "rowId");
202
+ const columnIds = useMappedArray(columns, "id");
203
+ const selectedRowIds = useAsSet(rowSelection);
204
+ const selectedColumnIds = useAsSet(columnSelection);
192
205
  const rowAnchor = useRef2(null);
193
206
  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
207
  function announceCount(changed, total, noun) {
227
208
  if (changed > 1) {
228
209
  announce(countMessage(total, noun));
229
210
  }
230
211
  }
231
212
  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
213
+ commitIfChanged(rowSelection, next, setRowSelection, (committed) => {
214
+ const { added, removed } = diffSelection(rowSelection, committed);
215
+ const addedRows = resolveRows(rowsById, added);
216
+ const removedRows = resolveRows(rowsById, removed);
217
+ for (const row of addedRows) {
218
+ callbacks.onRowSelect?.({ row, selection: committed });
219
+ }
220
+ if (addedRows.length > 0) {
221
+ callbacks.onRowsSelect?.({ rows: addedRows, selection: committed });
222
+ }
223
+ for (const row of removedRows) {
224
+ callbacks.onRowDeselect?.({ row, selection: committed });
225
+ }
226
+ if (removedRows.length > 0) {
227
+ callbacks.onRowsDeselect?.({ rows: removedRows, selection: committed });
228
+ }
229
+ callbacks.onRowSelectionChange?.({
230
+ added: addedRows,
231
+ removed: removedRows,
232
+ selected: resolveRows(rowsById, committed),
233
+ selection: committed
234
+ });
235
+ announceCount(added.length + removed.length, committed.length, "row");
256
236
  });
257
- announceCount(added.length + removed.length, next.length, "row");
258
237
  }
259
238
  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
239
+ commitIfChanged(columnSelection, next, setColumnSelection, (committed) => {
240
+ const { added, removed } = diffSelection(columnSelection, committed);
241
+ const addedColumns = resolveColumns(columns, added);
242
+ const removedColumns = resolveColumns(columns, removed);
243
+ for (const column of addedColumns) {
244
+ callbacks.onColumnSelect?.({ column, selection: committed });
245
+ }
246
+ if (addedColumns.length > 0) {
247
+ callbacks.onColumnsSelect?.({
248
+ columns: addedColumns,
249
+ selection: committed
250
+ });
251
+ }
252
+ for (const column of removedColumns) {
253
+ callbacks.onColumnDeselect?.({ column, selection: committed });
254
+ }
255
+ if (removedColumns.length > 0) {
256
+ callbacks.onColumnsDeselect?.({
257
+ columns: removedColumns,
258
+ selection: committed
259
+ });
260
+ }
261
+ callbacks.onColumnSelectionChange?.({
262
+ added: addedColumns,
263
+ removed: removedColumns,
264
+ selected: resolveColumns(columns, committed),
265
+ selection: committed
280
266
  });
281
- }
282
- callbacks.onColumnSelectionChange?.({
283
- added: addedColumns,
284
- removed: removedColumns,
285
- selected: resolveColumns(next),
286
- selection: next
267
+ announceCount(added.length + removed.length, committed.length, "column");
287
268
  });
288
- announceCount(added.length + removed.length, next.length, "column");
289
269
  }
290
270
  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
271
+ commitIfChanged(cellSelection, next, setCellSelection, (committed) => {
272
+ const selected = resolveCell(rowsById, columns, committed);
273
+ const deselected = resolveCell(rowsById, columns, cellSelection);
274
+ if (deselected !== null) {
275
+ callbacks.onCellDeselect?.({ cell: deselected, selection: committed });
276
+ }
277
+ if (selected !== null) {
278
+ callbacks.onCellSelect?.({ cell: selected, selection: committed });
279
+ }
280
+ callbacks.onCellSelectionChange?.({
281
+ selected,
282
+ deselected,
283
+ selection: committed
284
+ });
285
+ if (selected !== null) {
286
+ announce(
287
+ `${columnName(selected.columnId)}, row ${String(selected.rowIndex + 1)}, selected`
288
+ );
289
+ }
307
290
  });
308
291
  }
309
292
  function selectRow(rowId, intent) {
310
- const next = applyIntent(
293
+ const next = applySelectionIntent(
311
294
  rowSelection,
312
295
  rowIds,
313
296
  rowAnchor.current,
@@ -321,7 +304,7 @@ function useGridSelection({
321
304
  commitRows(next);
322
305
  }
323
306
  function selectColumn(columnId, intent) {
324
- const next = applyIntent(
307
+ const next = applySelectionIntent(
325
308
  columnSelection,
326
309
  columnIds,
327
310
  columnAnchor.current,
@@ -364,8 +347,8 @@ function useGridSelection({
364
347
 
365
348
  // src/DataGrid/components/GridHeader.tsx
366
349
  import { jsx, jsxs } from "react/jsx-runtime";
367
- function ChevronsUpDownIcon() {
368
- return /* @__PURE__ */ jsxs(
350
+ function Icon({ children }) {
351
+ return /* @__PURE__ */ jsx(
369
352
  "svg",
370
353
  {
371
354
  viewBox: "0 0 24 24",
@@ -375,27 +358,25 @@ function ChevronsUpDownIcon() {
375
358
  strokeLinecap: "round",
376
359
  strokeLinejoin: "round",
377
360
  "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
- ]
361
+ children
382
362
  }
383
363
  );
384
364
  }
365
+ function ChevronsUpDownIcon() {
366
+ return /* @__PURE__ */ jsxs(Icon, { children: [
367
+ /* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5" }),
368
+ /* @__PURE__ */ jsx("path", { d: "m7 9 5-5 5 5" })
369
+ ] });
370
+ }
385
371
  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
- );
372
+ return /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" }) });
373
+ }
374
+ function GroupIcon() {
375
+ return /* @__PURE__ */ jsxs(Icon, { children: [
376
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "7", height: "7", rx: "1" }),
377
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "3", width: "7", height: "7", rx: "1" }),
378
+ /* @__PURE__ */ jsx("rect", { x: "8.5", y: "14", width: "7", height: "7", rx: "1" })
379
+ ] });
399
380
  }
400
381
  function GridHeader({
401
382
  columns,
@@ -403,10 +384,13 @@ function GridHeader({
403
384
  drag,
404
385
  sort,
405
386
  sortableColumns,
387
+ grouping,
388
+ groupableColumns,
389
+ groupToggleIconColumns,
406
390
  nav,
407
391
  selection
408
392
  }) {
409
- const { beforeId } = drag.dropTarget ?? {};
393
+ const beforeId = drag.dropTarget?.kind === "column-order" ? drag.dropTarget.beforeId : void 0;
410
394
  const draggedEntry = columns.find((entry) => entry.id === drag.draggedColumnId) ?? null;
411
395
  return /* @__PURE__ */ jsxs("thead", { children: [
412
396
  /* @__PURE__ */ jsx("tr", { className: "grid-header", role: "row", "aria-rowindex": 1, children: columns.map((entry, index) => {
@@ -419,21 +403,30 @@ function GridHeader({
419
403
  const sortable = column.sortable ?? sortableColumns;
420
404
  const sortDirection = sort.directionFor(entry.id);
421
405
  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(" ");
406
+ const groupable = column.groupable ?? groupableColumns;
407
+ const showGroupToggleIcon = column.groupToggleIcon ?? groupToggleIconColumns;
408
+ const isGrouped = grouping.groupBy.some(
409
+ (level) => level.columnId === entry.id
410
+ );
411
+ const shortcuts = buildKeyShortcuts({
412
+ reorderable: entry.reorderable,
413
+ resizable: entry.resizable,
414
+ sortable,
415
+ groupable
416
+ });
427
417
  return /* @__PURE__ */ jsxs(
428
418
  "th",
429
419
  {
430
420
  scope: "col",
421
+ role: "columnheader",
431
422
  "data-gridkit-column": entry.id,
432
423
  "aria-colindex": index + 1,
433
424
  tabIndex: nav.tabIndexFor(HEADER_ROW, index),
434
- ...selection.columnMode !== false && {
435
- "aria-selected": selected
436
- },
425
+ ...ariaAttr(
426
+ selection.columnMode !== false,
427
+ "aria-selected",
428
+ selected
429
+ ),
437
430
  onFocus: () => {
438
431
  nav.focusCell(HEADER_ROW, index);
439
432
  },
@@ -441,7 +434,7 @@ function GridHeader({
441
434
  if (drag.justDragged()) {
442
435
  return;
443
436
  }
444
- if (event.target instanceof Element && (event.target.closest(".header-resize-handle") !== null || event.target.closest(".header-sort-toggle") !== null)) {
437
+ if (event.target instanceof Element && (event.target.closest(".header-resize-handle") !== null || event.target.closest(".header-sort-toggle") !== null || event.target.closest(".header-group-toggle") !== null)) {
445
438
  return;
446
439
  }
447
440
  selection.selectColumn(entry.id, intentOf(event));
@@ -459,28 +452,38 @@ function GridHeader({
459
452
  resize.nudge(entry, direction * KEYBOARD_STEP);
460
453
  return;
461
454
  }
455
+ if (event.key === "Enter" && event.altKey && entry.resizable) {
456
+ event.preventDefault();
457
+ resize.sizeToContent(entry);
458
+ return;
459
+ }
462
460
  if (event.key === "ArrowUp" && event.altKey && sortable) {
463
461
  event.preventDefault();
464
462
  sort.toggle(entry, { shiftKey: event.shiftKey });
465
463
  return;
466
464
  }
465
+ if (event.key === "ArrowDown" && event.altKey && groupable) {
466
+ event.preventDefault();
467
+ grouping.toggleGroupBy(entry.id);
468
+ return;
469
+ }
467
470
  if (event.key === " " || event.key === "Enter") {
468
471
  event.preventDefault();
469
472
  selection.selectColumn(
470
473
  entry.id,
471
- intentOf({
472
- ctrlKey: event.key === " " || event.ctrlKey,
473
- metaKey: event.metaKey,
474
- shiftKey: event.shiftKey
475
- })
474
+ intentOf(keyboardSelectIntent(event))
476
475
  );
477
476
  return;
478
477
  }
479
478
  nav.onKeyDown(event);
480
479
  },
481
- className: [
480
+ className: classNames(
482
481
  "header-cell",
483
- entry.reorderable ? "is-reorderable" : "",
482
+ // The grab affordance a header showing either drag
483
+ // destination shares — a column dragged only for grouping
484
+ // (`groupByDraggable` without `reorderable`) grabs the same
485
+ // way one dragged only for reordering does.
486
+ entry.reorderable || entry.groupByDraggable ? "is-reorderable" : "",
484
487
  selected ? "is-selected" : "",
485
488
  resizing ? "is-resizing" : "",
486
489
  dragging ? "is-dragging" : "",
@@ -488,17 +491,47 @@ function GridHeader({
488
491
  dropAfter ? "is-drop-after" : "",
489
492
  column.wrap?.header ? "is-wrapped" : "",
490
493
  column.headerClassName ?? ""
491
- ].filter(Boolean).join(" "),
492
- ...shortcuts !== "" && { "aria-keyshortcuts": shortcuts },
493
- ...sortable && sortDirection !== null && {
494
- "aria-sort": sortDirection === "asc" ? "ascending" : "descending"
495
- },
496
- ...entry.reorderable && {
494
+ ),
495
+ ...ariaAttr(shortcuts !== "", "aria-keyshortcuts", shortcuts),
496
+ ...ariaAttr(
497
+ sortable && sortDirection !== null,
498
+ "aria-sort",
499
+ sortDirection === "asc" ? "ascending" : "descending"
500
+ ),
501
+ ...(entry.reorderable || entry.groupByDraggable) && {
497
502
  onPointerDown: (event) => {
498
503
  drag.startDrag(entry, event);
499
504
  }
500
505
  },
501
506
  children: [
507
+ groupable && showGroupToggleIcon && /*
508
+ * Inline, before the label, rather than absolutely
509
+ * positioned like the sort toggle and resize handle: those
510
+ * two already share the header's right edge, and a third
511
+ * overlay there would need its own carve-out. Sitting in
512
+ * normal flow costs nothing but a few pixels of label width.
513
+ * A pointer affordance only, same as the other two — the
514
+ * keyboard equivalent is Alt+ArrowDown on the header itself.
515
+ */
516
+ /* @__PURE__ */ jsx(
517
+ "span",
518
+ {
519
+ className: classNames(
520
+ "header-group-toggle",
521
+ isGrouped ? "is-grouped" : ""
522
+ ),
523
+ "aria-hidden": "true",
524
+ tabIndex: -1,
525
+ onPointerDown: (event) => {
526
+ event.stopPropagation();
527
+ },
528
+ onClick: (event) => {
529
+ event.stopPropagation();
530
+ grouping.toggleGroupBy(entry.id);
531
+ },
532
+ children: /* @__PURE__ */ jsx(GroupIcon, {})
533
+ }
534
+ ),
502
535
  entry.label,
503
536
  sortable && /*
504
537
  * A pointer affordance only, and hidden from assistive
@@ -509,10 +542,10 @@ function GridHeader({
509
542
  /* @__PURE__ */ jsxs(
510
543
  "span",
511
544
  {
512
- className: [
545
+ className: classNames(
513
546
  "header-sort-toggle",
514
547
  sortDirection !== null ? `is-sorted-${sortDirection}` : ""
515
- ].filter(Boolean).join(" "),
548
+ ),
516
549
  "aria-hidden": "true",
517
550
  tabIndex: -1,
518
551
  onPointerDown: (event) => {
@@ -570,10 +603,109 @@ function GridHeader({
570
603
  ] });
571
604
  }
572
605
 
573
- // src/DataGrid/components/GridRow.tsx
574
- import { accessDotted as accessDotted2 } from "@gridkitjs/core";
606
+ // src/DataGrid/components/GridBody.tsx
607
+ import { useMemo as useMemo2 } from "react";
608
+ import { groupRowId, intentOf as intentOf2 } from "@gridkitjs/core";
609
+
610
+ // src/DataGrid/components/GridGroupRow.tsx
575
611
  import { memo } from "react";
576
- import { jsx as jsx2 } from "react/jsx-runtime";
612
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
613
+ function formatGroupValue(value) {
614
+ if (value === null || value === void 0) {
615
+ return "(blank)";
616
+ }
617
+ if (value instanceof Date) {
618
+ return value.toLocaleString();
619
+ }
620
+ switch (typeof value) {
621
+ case "string":
622
+ case "number":
623
+ case "boolean":
624
+ case "bigint":
625
+ case "symbol":
626
+ return String(value);
627
+ case "function":
628
+ return "(function)";
629
+ default:
630
+ return JSON.stringify(value);
631
+ }
632
+ }
633
+ function GridGroupRowComponent({
634
+ columnCount,
635
+ groupId,
636
+ level,
637
+ columnLabel,
638
+ value,
639
+ expanded,
640
+ count,
641
+ rowIndex,
642
+ posinset,
643
+ setsize,
644
+ focused
645
+ }) {
646
+ return /* @__PURE__ */ jsx2(
647
+ "tr",
648
+ {
649
+ role: "row",
650
+ "aria-rowindex": rowIndex + 2,
651
+ "aria-level": level + 1,
652
+ "aria-expanded": expanded,
653
+ "aria-setsize": setsize,
654
+ "aria-posinset": posinset,
655
+ "data-gridkit-group": groupId,
656
+ className: classNames(
657
+ "grid-group-row",
658
+ `is-group-level-${String(level)}`
659
+ ),
660
+ children: /* @__PURE__ */ jsx2(
661
+ "td",
662
+ {
663
+ role: "gridcell",
664
+ colSpan: columnCount,
665
+ tabIndex: focused ? 0 : -1,
666
+ "aria-keyshortcuts": "Space Enter",
667
+ className: "grid-group-cell",
668
+ children: /* @__PURE__ */ jsxs2(
669
+ "div",
670
+ {
671
+ className: "grid-group-cell-content",
672
+ style: { paddingInlineStart: `${String(level * 1.25 + 0.5)}rem` },
673
+ children: [
674
+ /* @__PURE__ */ jsx2(
675
+ "span",
676
+ {
677
+ className: classNames(
678
+ "group-toggle",
679
+ expanded ? "is-expanded" : "is-collapsed"
680
+ ),
681
+ "aria-hidden": "true"
682
+ }
683
+ ),
684
+ /* @__PURE__ */ jsxs2("span", { className: "group-label", children: [
685
+ columnLabel,
686
+ ": ",
687
+ formatGroupValue(value)
688
+ ] }),
689
+ /* @__PURE__ */ jsxs2("span", { className: "group-count", children: [
690
+ "(",
691
+ count,
692
+ ")"
693
+ ] })
694
+ ]
695
+ }
696
+ )
697
+ }
698
+ )
699
+ }
700
+ );
701
+ }
702
+ var GridGroupRow = memo(GridGroupRowComponent);
703
+ var GridGroupRow_default = GridGroupRow;
704
+
705
+ // src/DataGrid/components/GridRow.tsx
706
+ import { accessDotted } from "@gridkitjs/core";
707
+ import { memo as memo2 } from "react";
708
+ import { jsx as jsx3 } from "react/jsx-runtime";
577
709
  function GridRowComponent({
578
710
  columns,
579
711
  rowId,
@@ -587,31 +719,32 @@ function GridRowComponent({
587
719
  rowsSelectable,
588
720
  cellsSelectable
589
721
  }) {
590
- return /* @__PURE__ */ jsx2(
722
+ return /* @__PURE__ */ jsx3(
591
723
  "tr",
592
724
  {
593
725
  role: "row",
594
726
  "aria-rowindex": rowIndex + 2,
595
- ...rowsSelectable && { "aria-selected": selected },
596
- className: ["grid-row", selected ? "is-selected" : ""].filter(Boolean).join(" "),
727
+ ...ariaAttr(rowsSelectable, "aria-selected", selected),
728
+ className: classNames("grid-row", selected ? "is-selected" : ""),
597
729
  children: columns.map(({ id, column, alignment }, columnIndex) => {
598
- const value = accessDotted2(row, column.field);
730
+ const value = accessDotted(row, column.field);
599
731
  const cellSelected = id === selectedColumnId;
600
- return /* @__PURE__ */ jsx2(
732
+ return /* @__PURE__ */ jsx3(
601
733
  "td",
602
734
  {
603
735
  role: "gridcell",
604
736
  "data-gridkit-column": id,
605
737
  "aria-colindex": columnIndex + 1,
606
- tabIndex: columnIndex === focusedColumnIndex ? 0 : -1,
607
- ...cellsSelectable && { "aria-selected": cellSelected },
608
- className: [
738
+ tabIndex: tabIndexFor(columnIndex, focusedColumnIndex),
739
+ ...ariaAttr(cellsSelectable, "aria-selected", cellSelected),
740
+ ...ariaAttr(cellsSelectable, "aria-keyshortcuts", "Space Enter"),
741
+ className: classNames(
609
742
  "grid-cell",
610
743
  id === activeColumnId ? "is-resizing" : "",
611
744
  cellSelected || selectedColumnIds.has(id) ? "is-selected" : "",
612
745
  column.wrap?.cells ? "is-wrapped" : "",
613
746
  column.cellClassName ?? ""
614
- ].filter(Boolean).join(" "),
747
+ ),
615
748
  style: { textAlign: alignment },
616
749
  children: column.cellTemplate ? column.cellTemplate({ value, row, rowIndex, rowId, selected }) : value
617
750
  },
@@ -621,11 +754,11 @@ function GridRowComponent({
621
754
  }
622
755
  );
623
756
  }
624
- var GridRow = memo(GridRowComponent);
757
+ var GridRow = memo2(GridRowComponent);
625
758
  var GridRow_default = GridRow;
626
759
 
627
760
  // src/DataGrid/components/GridBody.tsx
628
- import { jsx as jsx3 } from "react/jsx-runtime";
761
+ import { jsx as jsx4 } from "react/jsx-runtime";
629
762
  function cellFrom(target) {
630
763
  if (!(target instanceof Element)) {
631
764
  return null;
@@ -642,21 +775,58 @@ function cellFrom(target) {
642
775
  columnId
643
776
  };
644
777
  }
778
+ function groupRowFrom(target) {
779
+ if (!(target instanceof Element)) {
780
+ return null;
781
+ }
782
+ const row = target.closest("tr[data-gridkit-group]");
783
+ const groupId = row?.getAttribute("data-gridkit-group");
784
+ if (!(row instanceof HTMLTableRowElement) || groupId === null || groupId === void 0) {
785
+ return null;
786
+ }
787
+ return { rowIndex: row.sectionRowIndex, groupId };
788
+ }
789
+ function groupAriaMeta(rows) {
790
+ const totals = /* @__PURE__ */ new Map();
791
+ for (const entry of rows) {
792
+ if (!("kind" in entry)) continue;
793
+ const parentKey = groupRowId(entry.path.slice(0, -1));
794
+ totals.set(parentKey, (totals.get(parentKey) ?? 0) + 1);
795
+ }
796
+ const seen = /* @__PURE__ */ new Map();
797
+ const meta = /* @__PURE__ */ new Map();
798
+ for (const entry of rows) {
799
+ if (!("kind" in entry)) continue;
800
+ const parentKey = groupRowId(entry.path.slice(0, -1));
801
+ const posinset = (seen.get(parentKey) ?? 0) + 1;
802
+ seen.set(parentKey, posinset);
803
+ meta.set(entry.groupId, {
804
+ posinset,
805
+ setsize: totals.get(parentKey) ?? posinset
806
+ });
807
+ }
808
+ return meta;
809
+ }
645
810
  function GridBody({
646
811
  columns,
647
812
  rows,
648
813
  activeColumnId,
649
814
  nav,
650
- selection
815
+ selection,
816
+ grouping
651
817
  }) {
652
818
  const { selectedCell, rowMode, cellMode } = selection;
819
+ const ariaMeta = useMemo2(() => groupAriaMeta(rows), [rows]);
653
820
  function targetOf(target) {
654
821
  const position = cellFrom(target);
655
822
  const entry = position === null ? void 0 : rows[position.rowIndex];
656
- return position === null || entry === void 0 ? null : { position, rowId: entry.rowId };
823
+ if (position === null || entry === void 0 || "kind" in entry) {
824
+ return null;
825
+ }
826
+ return { position, rowId: entry.rowId };
657
827
  }
658
828
  function select(rowId, columnId, modifiers) {
659
- const intent = intentOf(modifiers);
829
+ const intent = intentOf2(modifiers);
660
830
  if (rowMode !== false) {
661
831
  selection.selectRow(rowId, intent);
662
832
  }
@@ -664,78 +834,161 @@ function GridBody({
664
834
  selection.selectCell({ rowId, columnId }, intent);
665
835
  }
666
836
  }
667
- return /* @__PURE__ */ jsx3(
837
+ return /* @__PURE__ */ jsx4(
668
838
  "tbody",
669
839
  {
670
840
  className: "grid-body",
671
841
  onFocus: (event) => {
842
+ const group = groupRowFrom(event.target);
843
+ if (group !== null) {
844
+ nav.focusCell(group.rowIndex, 0);
845
+ return;
846
+ }
672
847
  const found = targetOf(event.target);
673
848
  if (found === null) return;
674
849
  nav.focusCell(found.position.rowIndex, found.position.columnIndex);
675
850
  },
676
851
  onClick: (event) => {
852
+ const group = groupRowFrom(event.target);
853
+ if (group !== null) {
854
+ grouping.toggleExpansion(group.groupId);
855
+ return;
856
+ }
677
857
  const found = targetOf(event.target);
678
858
  if (found === null) return;
679
859
  select(found.rowId, found.position.columnId, event);
680
860
  },
681
861
  onKeyDown: (event) => {
862
+ const group = groupRowFrom(event.target);
863
+ if (group !== null) {
864
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
865
+ event.preventDefault();
866
+ return;
867
+ }
868
+ if (event.key === " " || event.key === "Enter") {
869
+ event.preventDefault();
870
+ grouping.toggleExpansion(group.groupId);
871
+ return;
872
+ }
873
+ nav.onKeyDown(event);
874
+ return;
875
+ }
682
876
  if (event.key === " " || event.key === "Enter") {
683
877
  const found = targetOf(event.target);
684
878
  if (found === null) return;
685
879
  event.preventDefault();
686
- select(found.rowId, found.position.columnId, {
687
- ctrlKey: event.key === " " || event.ctrlKey,
688
- metaKey: event.metaKey,
689
- shiftKey: event.shiftKey
690
- });
880
+ select(
881
+ found.rowId,
882
+ found.position.columnId,
883
+ keyboardSelectIntent(event)
884
+ );
691
885
  return;
692
886
  }
693
887
  nav.onKeyDown(event);
694
888
  },
695
- children: rows.map(({ rowId, row, rowIndex }) => /* @__PURE__ */ jsx3(
696
- GridRow_default,
697
- {
698
- columns,
699
- rowId,
700
- row,
701
- rowIndex,
702
- activeColumnId,
703
- selectedColumnIds: selection.selectedColumnIds,
704
- selected: selection.selectedRowIds.has(rowId),
705
- selectedColumnId: selectedCell?.rowId === rowId ? selectedCell.columnId : null,
706
- focusedColumnIndex: nav.focus.rowIndex === rowIndex ? nav.focus.columnIndex : null,
707
- rowsSelectable: rowMode !== false,
708
- cellsSelectable: cellMode !== false
709
- },
710
- rowId
711
- ))
889
+ children: rows.map(
890
+ (entry) => "kind" in entry ? /* @__PURE__ */ jsx4(
891
+ GridGroupRow_default,
892
+ {
893
+ columnCount: columns.length,
894
+ groupId: entry.groupId,
895
+ level: entry.level,
896
+ columnLabel: columns.find((column) => column.id === entry.columnId)?.label ?? entry.columnId,
897
+ value: entry.value,
898
+ expanded: entry.expanded,
899
+ count: entry.count,
900
+ rowIndex: entry.rowIndex,
901
+ posinset: ariaMeta.get(entry.groupId)?.posinset ?? 1,
902
+ setsize: ariaMeta.get(entry.groupId)?.setsize ?? 1,
903
+ focused: nav.focus.rowIndex === entry.rowIndex
904
+ },
905
+ entry.groupId
906
+ ) : /* @__PURE__ */ jsx4(
907
+ GridRow_default,
908
+ {
909
+ columns,
910
+ rowId: entry.rowId,
911
+ row: entry.row,
912
+ rowIndex: entry.rowIndex,
913
+ activeColumnId,
914
+ selectedColumnIds: selection.selectedColumnIds,
915
+ selected: selection.selectedRowIds.has(entry.rowId),
916
+ selectedColumnId: selectedCell?.rowId === entry.rowId ? selectedCell.columnId : null,
917
+ focusedColumnIndex: nav.focus.rowIndex === entry.rowIndex ? nav.focus.columnIndex : null,
918
+ rowsSelectable: rowMode !== false,
919
+ cellsSelectable: cellMode !== false
920
+ },
921
+ entry.rowId
922
+ )
923
+ )
712
924
  }
713
925
  );
714
926
  }
715
927
 
716
- // src/DataGrid/useColumnDrag.ts
928
+ // src/DataGrid/components/GroupByBar.tsx
929
+ import { createPortal as createPortal2 } from "react-dom";
930
+ import { resolveKeyboardDropTarget } from "@gridkitjs/core";
931
+
932
+ // src/DataGrid/groupByDropTarget.ts
933
+ import {
934
+ movesGroupBy,
935
+ resolveDropBefore
936
+ } from "@gridkitjs/core";
937
+
938
+ // src/DataGrid/useDragReorder.ts
717
939
  import {
718
940
  useRef as useRef3,
719
941
  useState as useState2
720
942
  } from "react";
721
- import {
722
- movesColumn,
723
- resolveDropBefore
724
- } from "@gridkitjs/core";
943
+
944
+ // src/DataGrid/pointerGesture.ts
945
+ function startPointerGesture(element, pointerId, handlers) {
946
+ element.setPointerCapture(pointerId);
947
+ function stop() {
948
+ element.removeEventListener("pointermove", move);
949
+ element.removeEventListener("pointerup", end);
950
+ element.removeEventListener("pointercancel", cancel);
951
+ window.removeEventListener("keydown", onKeyDown);
952
+ if (element.hasPointerCapture(pointerId)) {
953
+ element.releasePointerCapture(pointerId);
954
+ }
955
+ }
956
+ function move(event) {
957
+ handlers.onMove(event);
958
+ }
959
+ function end() {
960
+ stop();
961
+ handlers.onEnd();
962
+ }
963
+ function cancel() {
964
+ stop();
965
+ handlers.onCancel();
966
+ }
967
+ function onKeyDown(event) {
968
+ if (event.key === "Escape") {
969
+ cancel();
970
+ }
971
+ }
972
+ element.addEventListener("pointermove", move);
973
+ element.addEventListener("pointerup", end);
974
+ element.addEventListener("pointercancel", cancel);
975
+ window.addEventListener("keydown", onKeyDown);
976
+ }
977
+
978
+ // src/DataGrid/useDragReorder.ts
725
979
  var DRAG_THRESHOLD = 4;
726
- function headerAt(x, y) {
727
- const element = document.elementFromPoint(x, y);
728
- return element?.closest("th[data-gridkit-column]") ?? null;
980
+ function elementAt(x, y, selector) {
981
+ return document.elementFromPoint(x, y)?.closest(selector) ?? null;
729
982
  }
730
983
  function sideOf(element, x) {
731
984
  const rect = element.getBoundingClientRect();
732
985
  return x < rect.left + rect.width / 2 ? "before" : "after";
733
986
  }
734
- function useColumnDrag({
735
- order,
987
+ function useDragReorder({
988
+ resolveTarget,
736
989
  onDrop
737
990
  }) {
738
- const [draggedColumnId, setDraggedColumnId] = useState2(null);
991
+ const [draggedId, setDraggedId] = useState2(null);
739
992
  const [dropTarget, setDropTarget] = useState2(null);
740
993
  const [ghostTransform, setGhostTransform] = useState2(null);
741
994
  const dragged = useRef3(false);
@@ -744,87 +997,256 @@ function useColumnDrag({
744
997
  dragged.current = false;
745
998
  return answer;
746
999
  }
747
- function startDrag(entry, event) {
748
- const header = event.currentTarget;
1000
+ function startDrag(id, grabElement, event) {
749
1001
  const { pointerId, clientX: startX } = event;
750
- const grabX = startX - header.getBoundingClientRect().left;
1002
+ const grabX = startX - grabElement.getBoundingClientRect().left;
751
1003
  let dragging = false;
752
1004
  let target = null;
753
1005
  function placeGhost(x, y) {
754
1006
  setGhostTransform(`translate(${String(x - grabX)}px, ${String(y)}px)`);
755
1007
  }
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
- }
1008
+ function reset() {
782
1009
  setGhostTransform(null);
783
- setDraggedColumnId(null);
1010
+ setDraggedId(null);
784
1011
  setDropTarget(null);
785
1012
  }
786
- function end() {
787
- const dropped = target;
788
- stop();
789
- if (dragging && dropped !== null) {
790
- onDrop(dropped, entry.id);
1013
+ startPointerGesture(grabElement, pointerId, {
1014
+ onMove(moveEvent) {
1015
+ const { clientX, clientY } = moveEvent;
1016
+ if (!dragging) {
1017
+ if (Math.abs(clientX - startX) < DRAG_THRESHOLD) {
1018
+ return;
1019
+ }
1020
+ dragging = true;
1021
+ dragged.current = true;
1022
+ setDraggedId(id);
1023
+ }
1024
+ placeGhost(clientX, clientY);
1025
+ target = resolveTarget(clientX, clientY, id);
1026
+ setDropTarget(target);
1027
+ },
1028
+ onEnd() {
1029
+ const dropped = target;
1030
+ reset();
1031
+ if (dragging && dropped !== null) {
1032
+ onDrop(dropped, id);
1033
+ }
1034
+ },
1035
+ // Escape and a cancelled pointer both leave the order as it was.
1036
+ onCancel() {
1037
+ target = null;
1038
+ reset();
791
1039
  }
1040
+ });
1041
+ }
1042
+ return {
1043
+ draggedId,
1044
+ dropTarget,
1045
+ ghostTransform,
1046
+ startDrag,
1047
+ justDragged
1048
+ };
1049
+ }
1050
+
1051
+ // src/DataGrid/groupByDropTarget.ts
1052
+ function resolveGroupByDropTarget(clientX, clientY, movedId, groupBy) {
1053
+ const overBar = elementAt(clientX, clientY, "[data-gridkit-group-by-bar]");
1054
+ if (overBar === null) {
1055
+ return null;
1056
+ }
1057
+ const overChip = elementAt(clientX, clientY, "[data-gridkit-group-chip]");
1058
+ const chipId = overChip?.getAttribute("data-gridkit-group-chip");
1059
+ const beforeColumnId = overChip && chipId ? resolveDropBefore(
1060
+ groupBy.map((entry) => entry.columnId),
1061
+ chipId,
1062
+ sideOf(overChip, clientX)
1063
+ ) : null;
1064
+ return movesGroupBy(groupBy, movedId, beforeColumnId) ? { kind: "group-by", beforeColumnId } : null;
1065
+ }
1066
+
1067
+ // src/DataGrid/useGroupByDrag.ts
1068
+ function useGroupByDrag({
1069
+ groupBy,
1070
+ onDrop
1071
+ }) {
1072
+ const drag = useDragReorder({
1073
+ resolveTarget: (clientX, clientY, movedId) => resolveGroupByDropTarget(clientX, clientY, movedId, groupBy),
1074
+ onDrop: (target, movedId) => {
1075
+ onDrop(movedId, target.beforeColumnId);
792
1076
  }
793
- function cancel() {
794
- target = null;
795
- stop();
796
- }
797
- function onKeyDown(keyEvent) {
798
- if (keyEvent.key === "Escape") {
799
- cancel();
800
- }
1077
+ });
1078
+ return {
1079
+ draggedColumnId: drag.draggedId,
1080
+ dropTarget: drag.dropTarget,
1081
+ ghostTransform: drag.ghostTransform,
1082
+ startDrag: (columnId, event) => {
1083
+ drag.startDrag(columnId, event.currentTarget, event);
1084
+ },
1085
+ justDragged: drag.justDragged
1086
+ };
1087
+ }
1088
+
1089
+ // src/DataGrid/components/GroupByBar.tsx
1090
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1091
+ function GroupByBar({
1092
+ columns,
1093
+ grouping,
1094
+ columnName,
1095
+ headerDragEligible,
1096
+ headerDragBlocked,
1097
+ headerDropTarget
1098
+ }) {
1099
+ const { groupBy } = grouping;
1100
+ const groupByIds = groupBy.map((entry) => entry.columnId);
1101
+ const chipDrag = useGroupByDrag({
1102
+ groupBy,
1103
+ onDrop: (movedId, beforeColumnId) => {
1104
+ grouping.moveGroupBy(movedId, beforeColumnId);
801
1105
  }
802
- header.addEventListener("pointermove", move);
803
- header.addEventListener("pointerup", end);
804
- header.addEventListener("pointercancel", cancel);
805
- window.addEventListener("keydown", onKeyDown);
1106
+ });
1107
+ const activeDropTarget = chipDrag.dropTarget ?? headerDropTarget;
1108
+ const isDragTarget = chipDrag.draggedColumnId !== null || headerDragEligible;
1109
+ const showPlaceholder = groupBy.length === 0 && columns.some((entry) => entry.groupByDraggable);
1110
+ function labelFor(columnId) {
1111
+ return columns.find((entry) => entry.id === columnId)?.label ?? columnId;
806
1112
  }
807
- function moveByKeyboard(entry, direction) {
808
- const index = order.indexOf(entry.id);
809
- if (index === -1) {
1113
+ function moveByKeyboard(columnId, direction) {
1114
+ const beforeColumnId = resolveKeyboardDropTarget(
1115
+ groupByIds,
1116
+ columnId,
1117
+ direction
1118
+ );
1119
+ if (beforeColumnId === void 0) {
810
1120
  return;
811
1121
  }
812
- const targetIndex = direction === -1 ? index - 1 : index + 2;
813
- if (targetIndex < 0) {
1122
+ grouping.moveGroupBy(columnId, beforeColumnId);
1123
+ }
1124
+ return /* @__PURE__ */ jsxs3(
1125
+ "div",
1126
+ {
1127
+ className: classNames(
1128
+ "gridkit-group-by-bar",
1129
+ isDragTarget ? "is-drag-target" : "",
1130
+ headerDragBlocked ? "is-drag-blocked" : ""
1131
+ ),
1132
+ "data-gridkit-group-by-bar": "",
1133
+ role: "group",
1134
+ "aria-label": "Grouped by",
1135
+ children: [
1136
+ showPlaceholder && /* @__PURE__ */ jsx5("span", { className: "group-by-bar-placeholder", children: "Drag a column here to group by it" }),
1137
+ groupBy.map((entry, index) => {
1138
+ const dropBefore = activeDropTarget?.beforeColumnId === entry.columnId;
1139
+ const dropAfter = activeDropTarget?.beforeColumnId === null && index === groupBy.length - 1;
1140
+ const dragging = chipDrag.draggedColumnId === entry.columnId;
1141
+ return /* @__PURE__ */ jsxs3(
1142
+ "span",
1143
+ {
1144
+ className: classNames(
1145
+ "group-by-chip",
1146
+ dragging ? "is-dragging" : "",
1147
+ dropBefore ? "is-drop-before" : "",
1148
+ dropAfter ? "is-drop-after" : ""
1149
+ ),
1150
+ "data-gridkit-group-chip": entry.columnId,
1151
+ tabIndex: 0,
1152
+ "aria-label": groupBy.length > 1 ? `${columnName(entry.columnId)}, level ${String(index + 1)} of ${String(groupBy.length)}` : columnName(entry.columnId),
1153
+ "aria-keyshortcuts": "Control+ArrowLeft Control+ArrowRight",
1154
+ onPointerDown: (event) => {
1155
+ chipDrag.startDrag(entry.columnId, event);
1156
+ },
1157
+ onKeyDown: (event) => {
1158
+ const horizontal = event.key === "ArrowLeft" || event.key === "ArrowRight";
1159
+ if (horizontal && event.ctrlKey) {
1160
+ event.preventDefault();
1161
+ moveByKeyboard(
1162
+ entry.columnId,
1163
+ event.key === "ArrowLeft" ? -1 : 1
1164
+ );
1165
+ }
1166
+ },
1167
+ children: [
1168
+ /* @__PURE__ */ jsx5("span", { className: "group-by-chip-label", children: labelFor(entry.columnId) }),
1169
+ /* @__PURE__ */ jsx5(
1170
+ "button",
1171
+ {
1172
+ type: "button",
1173
+ className: "group-by-chip-remove",
1174
+ "aria-label": `Stop grouping by ${columnName(entry.columnId)}`,
1175
+ onPointerDown: (event) => {
1176
+ event.stopPropagation();
1177
+ },
1178
+ onClick: () => {
1179
+ grouping.toggleGroupBy(entry.columnId);
1180
+ },
1181
+ children: "\xD7"
1182
+ }
1183
+ )
1184
+ ]
1185
+ },
1186
+ entry.columnId
1187
+ );
1188
+ }),
1189
+ chipDrag.draggedColumnId !== null && chipDrag.ghostTransform !== null && createPortal2(
1190
+ /* @__PURE__ */ jsx5(
1191
+ "span",
1192
+ {
1193
+ className: "group-by-chip drag-ghost",
1194
+ style: { transform: chipDrag.ghostTransform },
1195
+ "aria-hidden": "true",
1196
+ children: labelFor(chipDrag.draggedColumnId)
1197
+ }
1198
+ ),
1199
+ document.body
1200
+ )
1201
+ ]
1202
+ }
1203
+ );
1204
+ }
1205
+
1206
+ // src/DataGrid/useColumnDrag.ts
1207
+ import {
1208
+ movesColumn,
1209
+ resolveDropBefore as resolveDropBefore2,
1210
+ resolveKeyboardDropTarget as resolveKeyboardDropTarget2
1211
+ } from "@gridkitjs/core";
1212
+ function useColumnDrag({
1213
+ order,
1214
+ groupBy,
1215
+ groupByDraggableIds,
1216
+ onDrop
1217
+ }) {
1218
+ const drag = useDragReorder({
1219
+ resolveTarget(clientX, clientY, movedId) {
1220
+ const over = elementAt(clientX, clientY, "th[data-gridkit-column]");
1221
+ const overId = over?.getAttribute("data-gridkit-column");
1222
+ const beforeId = over && overId ? resolveDropBefore2(order, overId, sideOf(over, clientX)) : void 0;
1223
+ if (beforeId !== void 0) {
1224
+ return movesColumn(order, movedId, beforeId) ? { kind: "column-order", beforeId } : null;
1225
+ }
1226
+ const alreadyGrouped = groupBy.some(
1227
+ (entry) => entry.columnId === movedId
1228
+ );
1229
+ return groupByDraggableIds.has(movedId) && !alreadyGrouped ? resolveGroupByDropTarget(clientX, clientY, movedId, groupBy) : null;
1230
+ },
1231
+ onDrop
1232
+ });
1233
+ function startDrag(entry, event) {
1234
+ drag.startDrag(entry.id, event.currentTarget, event);
1235
+ }
1236
+ function moveByKeyboard(entry, direction) {
1237
+ const beforeId = resolveKeyboardDropTarget2(order, entry.id, direction);
1238
+ if (beforeId === void 0) {
814
1239
  return;
815
1240
  }
816
- onDrop(
817
- { kind: "column-order", beforeId: order[targetIndex] ?? null },
818
- entry.id
819
- );
1241
+ onDrop({ kind: "column-order", beforeId }, entry.id);
820
1242
  }
821
1243
  return {
822
- draggedColumnId,
823
- dropTarget,
824
- ghostTransform,
1244
+ draggedColumnId: drag.draggedId,
1245
+ dropTarget: drag.dropTarget,
1246
+ ghostTransform: drag.ghostTransform,
825
1247
  startDrag,
826
1248
  moveByKeyboard,
827
- justDragged
1249
+ justDragged: drag.justDragged
828
1250
  };
829
1251
  }
830
1252
 
@@ -837,6 +1259,7 @@ import {
837
1259
  beginColumnResize,
838
1260
  clampColumnWidth,
839
1261
  resolveColumnConstraints,
1262
+ revertColumnSize,
840
1263
  sizeColumnToContent
841
1264
  } from "@gridkitjs/core";
842
1265
 
@@ -885,57 +1308,39 @@ function useColumnResize({
885
1308
  let width = session.startWidth;
886
1309
  event.preventDefault();
887
1310
  event.stopPropagation();
888
- handle.setPointerCapture(pointerId);
889
1311
  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];
1312
+ startPointerGesture(handle, pointerId, {
1313
+ onMove(moveEvent) {
1314
+ width = applyColumnResize(session, moveEvent.clientX);
1315
+ commit(base, session.columnId, width, "move");
1316
+ },
1317
+ onEnd() {
1318
+ setActiveColumnId(null);
1319
+ if (width !== session.startWidth) {
1320
+ commit(base, session.columnId, width, "end");
920
1321
  }
921
- onColumnResize?.({
922
- columnId: session.columnId,
923
- width: session.startWidth,
924
- sizing: next,
925
- phase: "end"
1322
+ },
1323
+ /**
1324
+ * Escape puts back only the dragged column's pre-drag width, merged
1325
+ * into whatever the sizing state is *now* — not the `base` snapshot
1326
+ * taken at drag-start — so a different column resized via keyboard
1327
+ * while this drag was still open keeps its change instead of being
1328
+ * clobbered by a stale restore.
1329
+ */
1330
+ onCancel() {
1331
+ setActiveColumnId(null);
1332
+ setSizing((current) => {
1333
+ const next = revertColumnSize(current, base, session.columnId);
1334
+ onColumnResize?.({
1335
+ columnId: session.columnId,
1336
+ width: session.startWidth,
1337
+ sizing: next,
1338
+ phase: "end"
1339
+ });
1340
+ return next;
926
1341
  });
927
- return next;
928
- });
929
- }
930
- function onKeyDown(keyEvent) {
931
- if (keyEvent.key === "Escape") {
932
- cancel();
933
1342
  }
934
- }
935
- handle.addEventListener("pointermove", move);
936
- handle.addEventListener("pointerup", end);
937
- handle.addEventListener("pointercancel", cancel);
938
- window.addEventListener("keydown", onKeyDown);
1343
+ });
939
1344
  }
940
1345
  function sizeToContent(entry) {
941
1346
  const table = tableRef.current;
@@ -983,11 +1388,9 @@ function useColumnSort({
983
1388
  }) {
984
1389
  function toggle(entry, event) {
985
1390
  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 });
1391
+ commitIfChanged(sort, next, setSort, (committed) => {
1392
+ onColumnSortChange?.({ columnId: entry.id, sort: committed });
1393
+ });
991
1394
  }
992
1395
  return {
993
1396
  sort,
@@ -1018,8 +1421,62 @@ function useElementWidth(ref, enabled) {
1018
1421
  return width;
1019
1422
  }
1020
1423
 
1424
+ // src/DataGrid/useRowGrouping.ts
1425
+ import {
1426
+ collapseAllGroups,
1427
+ expandAllGroups,
1428
+ moveGroupByBefore,
1429
+ toggleGroupExpansion
1430
+ } from "@gridkitjs/core";
1431
+ function useRowGrouping({
1432
+ groupBy,
1433
+ setGroupBy,
1434
+ expansion,
1435
+ setExpansion,
1436
+ onGroupByChange,
1437
+ onGroupExpansionChange
1438
+ }) {
1439
+ function commitExpansion(next, groupId) {
1440
+ commitIfChanged(expansion, next, setExpansion, (committed) => {
1441
+ onGroupExpansionChange?.({ groupId, expansion: committed });
1442
+ });
1443
+ }
1444
+ function commitGroupBy(next, columnId) {
1445
+ commitIfChanged(groupBy, next, setGroupBy, (committed) => {
1446
+ onGroupByChange?.({ columnId, groupBy: committed });
1447
+ });
1448
+ }
1449
+ function toggleGroupBy(columnId) {
1450
+ const isGrouped = groupBy.some((entry) => entry.columnId === columnId);
1451
+ const next = isGrouped ? groupBy.filter((entry) => entry.columnId !== columnId) : [...groupBy, { columnId }];
1452
+ commitGroupBy(next, columnId);
1453
+ }
1454
+ function moveGroupBy(columnId, beforeColumnId) {
1455
+ commitGroupBy(
1456
+ moveGroupByBefore(groupBy, columnId, beforeColumnId),
1457
+ columnId
1458
+ );
1459
+ }
1460
+ return {
1461
+ groupBy,
1462
+ expansion,
1463
+ isExpanded: (groupId) => !expansion.includes(groupId),
1464
+ toggleExpansion: (groupId) => {
1465
+ commitExpansion(toggleGroupExpansion(expansion, groupId), groupId);
1466
+ },
1467
+ expandAll: () => {
1468
+ commitExpansion(expandAllGroups(expansion), null);
1469
+ },
1470
+ collapseAll: (rows) => {
1471
+ commitExpansion(collapseAllGroups(rows), null);
1472
+ },
1473
+ toggleGroupBy,
1474
+ moveGroupBy
1475
+ };
1476
+ }
1477
+
1021
1478
  // src/DataGrid/DataGrid.tsx
1022
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1479
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1023
1480
  function DataGridComponent({
1024
1481
  dataSource,
1025
1482
  columns,
@@ -1041,9 +1498,18 @@ function DataGridComponent({
1041
1498
  sortableColumns = false,
1042
1499
  defaultColumnSort,
1043
1500
  onColumnSortChange,
1501
+ groupableColumns = false,
1502
+ groupToggleIconColumns = true,
1503
+ groupByDraggableColumns = false,
1504
+ groupByBarVisibility = "auto",
1505
+ defaultGroupBy,
1506
+ onGroupByChange,
1507
+ defaultGroupExpansion,
1508
+ onGroupExpansionChange,
1044
1509
  defaultFilter,
1045
1510
  label,
1046
1511
  labelledBy,
1512
+ ref,
1047
1513
  ...callbacks
1048
1514
  }) {
1049
1515
  const hoverRows = hoverable?.rows ?? true;
@@ -1059,6 +1525,10 @@ function DataGridComponent({
1059
1525
  defaultColumnOrder ?? []
1060
1526
  );
1061
1527
  const [sort, setSort] = useState5(defaultColumnSort ?? []);
1528
+ const [groupBy, setGroupBy] = useState5(defaultGroupBy ?? []);
1529
+ const [expansion, setExpansion] = useState5(
1530
+ defaultGroupExpansion ?? []
1531
+ );
1062
1532
  const [filter] = useState5(defaultFilter ?? []);
1063
1533
  const [announcement, setAnnouncement] = useState5("");
1064
1534
  const [rowSelection, setRowSelection] = useState5(
@@ -1070,7 +1540,7 @@ function DataGridComponent({
1070
1540
  const [cellSelection, setCellSelection] = useState5(
1071
1541
  defaultCellSelection ?? null
1072
1542
  );
1073
- const rows = useMemo2(
1543
+ const rows = useMemo3(
1074
1544
  () => dataSource?.map((row, rowIndex) => ({
1075
1545
  rowId: resolveRowId(row, rowIndex, getRowId),
1076
1546
  row,
@@ -1078,21 +1548,22 @@ function DataGridComponent({
1078
1548
  })) ?? [],
1079
1549
  [dataSource, getRowId]
1080
1550
  );
1081
- const definedColumns = useMemo2(() => {
1551
+ const definedColumns = useMemo3(() => {
1082
1552
  if (columns && columns.length !== 0) return columns;
1083
1553
  if (dataSource && dataSource.length > 0)
1084
1554
  return defineColumnsFromRows(dataSource);
1085
1555
  return [];
1086
1556
  }, [columns, dataSource]);
1087
- const orderedColumns = useMemo2(
1557
+ const orderedColumns = useMemo3(
1088
1558
  () => applyColumnOrder(definedColumns, order),
1089
1559
  [definedColumns, order]
1090
1560
  );
1091
- const resolved = useMemo2(() => {
1561
+ const resolved = useMemo3(() => {
1092
1562
  const widths = resolveColumnWidths(orderedColumns, sizing, {
1093
1563
  sizes: columnSizeDefaults,
1094
1564
  resizable: resizableColumns,
1095
- reorderable: reorderableColumns
1565
+ reorderable: reorderableColumns,
1566
+ groupByDraggable: groupByDraggableColumns
1096
1567
  });
1097
1568
  return resizeMode === "fit" && viewportWidth !== null ? fitColumnsToWidth(widths, viewportWidth, columnSizeDefaults) : widths;
1098
1569
  }, [
@@ -1102,12 +1573,23 @@ function DataGridComponent({
1102
1573
  viewportWidth,
1103
1574
  columnSizeDefaults,
1104
1575
  resizableColumns,
1105
- reorderableColumns
1576
+ reorderableColumns,
1577
+ groupByDraggableColumns
1106
1578
  ]);
1107
- const shownRows = useMemo2(
1579
+ const shownRows = useMemo3(
1108
1580
  () => resolveShownRows(rows, filter, sort, resolved),
1109
1581
  [rows, filter, sort, resolved]
1110
1582
  );
1583
+ const displayRows = useMemo3(
1584
+ () => groupRows(shownRows, groupBy, expansion, resolved),
1585
+ [shownRows, groupBy, expansion, resolved]
1586
+ );
1587
+ const displayDataRows = useMemo3(
1588
+ () => groupBy.length === 0 ? shownRows : displayRows.filter(
1589
+ (entry) => !("kind" in entry)
1590
+ ),
1591
+ [displayRows, groupBy.length, shownRows]
1592
+ );
1111
1593
  function columnName(columnId) {
1112
1594
  const entry = resolved.find((candidate) => candidate.id === columnId);
1113
1595
  if (entry === void 0) {
@@ -1133,16 +1615,62 @@ function DataGridComponent({
1133
1615
  columnSizeDefaults,
1134
1616
  onColumnResize: handleColumnResize
1135
1617
  });
1618
+ function handleGroupByChange(event) {
1619
+ onGroupByChange?.(event);
1620
+ const entry = event.groupBy.find(
1621
+ (candidate) => candidate.columnId === event.columnId
1622
+ );
1623
+ if (entry === void 0) {
1624
+ announce(`${columnName(event.columnId)}, grouping removed`);
1625
+ return;
1626
+ }
1627
+ announce(
1628
+ event.groupBy.length > 1 ? `Grouped by ${columnName(event.columnId)}, level ${String(event.groupBy.indexOf(entry) + 1)} of ${String(event.groupBy.length)}` : `Grouped by ${columnName(event.columnId)}`
1629
+ );
1630
+ }
1631
+ function handleGroupExpansionChange(event) {
1632
+ onGroupExpansionChange?.(event);
1633
+ if (event.groupId === null) {
1634
+ announce(
1635
+ event.expansion.length === 0 ? "All groups expanded" : "All groups collapsed"
1636
+ );
1637
+ return;
1638
+ }
1639
+ announce(
1640
+ event.expansion.includes(event.groupId) ? "Group collapsed" : "Group expanded"
1641
+ );
1642
+ }
1643
+ const grouping = useRowGrouping({
1644
+ groupBy,
1645
+ setGroupBy,
1646
+ expansion,
1647
+ setExpansion,
1648
+ onGroupByChange: handleGroupByChange,
1649
+ onGroupExpansionChange: handleGroupExpansionChange
1650
+ });
1136
1651
  const nav = useGridNavigation({
1137
1652
  tableRef,
1138
- rowCount: shownRows.length,
1653
+ // Group headers are addressable rows too — the whole point of the flat
1654
+ // `DisplayRow[]` shape is that they share one position space with data
1655
+ // rows, so navigation counts them the same way.
1656
+ rowCount: displayRows.length,
1139
1657
  columnCount: resolved.length
1140
1658
  });
1141
- const displayedIds = useMemo2(
1659
+ const displayedIds = useMemo3(
1142
1660
  () => orderedColumns.map((column) => column.id ?? column.field),
1143
1661
  [orderedColumns]
1144
1662
  );
1663
+ const groupByDraggableIds = useMemo3(
1664
+ () => new Set(
1665
+ resolved.filter((entry) => entry.groupByDraggable).map((entry) => entry.id)
1666
+ ),
1667
+ [resolved]
1668
+ );
1145
1669
  function handleDrop(target, movedId) {
1670
+ if (target.kind === "group-by") {
1671
+ grouping.moveGroupBy(movedId, target.beforeColumnId);
1672
+ return;
1673
+ }
1146
1674
  const next = moveColumnBefore(displayedIds, movedId, target.beforeId);
1147
1675
  if (next === displayedIds) {
1148
1676
  return;
@@ -1155,7 +1683,16 @@ function DataGridComponent({
1155
1683
  );
1156
1684
  nav.focusCell(HEADER_ROW, position);
1157
1685
  }
1158
- const drag = useColumnDrag({ order: displayedIds, onDrop: handleDrop });
1686
+ const drag = useColumnDrag({
1687
+ order: displayedIds,
1688
+ groupBy,
1689
+ groupByDraggableIds,
1690
+ onDrop: handleDrop
1691
+ });
1692
+ const draggedColumnAlreadyGrouped = drag.draggedColumnId !== null && groupBy.some((entry) => entry.columnId === drag.draggedColumnId);
1693
+ const dragEligibleForGroupBy = drag.draggedColumnId !== null && groupByDraggableIds.has(drag.draggedColumnId) && !draggedColumnAlreadyGrouped;
1694
+ const showGroupByBar = groupByBarVisibility === "always" || groupByBarVisibility === "auto" && (groupBy.length > 0 || dragEligibleForGroupBy);
1695
+ const headerGroupByDropTarget = drag.dropTarget?.kind === "group-by" ? drag.dropTarget : null;
1159
1696
  function handleColumnSortChange(event) {
1160
1697
  onColumnSortChange?.(event);
1161
1698
  const entry = event.sort.find(
@@ -1176,7 +1713,7 @@ function DataGridComponent({
1176
1713
  onColumnSortChange: handleColumnSortChange
1177
1714
  });
1178
1715
  const selection = useGridSelection({
1179
- rows: shownRows,
1716
+ rows: displayDataRows,
1180
1717
  columns: resolved,
1181
1718
  selectable,
1182
1719
  rowSelection,
@@ -1186,20 +1723,77 @@ function DataGridComponent({
1186
1723
  cellSelection,
1187
1724
  setCellSelection,
1188
1725
  callbacks,
1189
- announce
1726
+ announce,
1727
+ columnName
1190
1728
  });
1191
1729
  const multiselectable = selection.rowMode === "multiple" || selection.columnMode === "multiple";
1192
- return /* @__PURE__ */ jsxs2("div", { className: "gridkit-data-grid-viewport", ref: viewportRef, children: [
1193
- /* @__PURE__ */ jsxs2(
1730
+ useImperativeHandle(ref, () => ({
1731
+ get element() {
1732
+ return viewportRef.current;
1733
+ },
1734
+ get table() {
1735
+ return tableRef.current;
1736
+ },
1737
+ getRows: () => shownRows,
1738
+ getDisplayRows: () => displayRows,
1739
+ getColumns: () => resolved,
1740
+ getColumnSizing: () => sizing,
1741
+ getColumnOrder: () => order,
1742
+ getColumnSort: () => sort,
1743
+ getGroupBy: () => groupBy,
1744
+ getGroupExpansion: () => expansion,
1745
+ getRowSelection: () => rowSelection,
1746
+ getColumnSelection: () => columnSelection,
1747
+ getCellSelection: () => cellSelection,
1748
+ getFocusedCell: () => nav.focus,
1749
+ focusCell: nav.focusCell,
1750
+ clearSelection: selection.clear,
1751
+ selectAllRows: selection.selectAllRows,
1752
+ expandAllGroups: grouping.expandAll,
1753
+ collapseAllGroups: () => {
1754
+ grouping.collapseAll(displayRows);
1755
+ },
1756
+ scrollToRow: (rowId, options) => {
1757
+ const index = displayRows.findIndex(
1758
+ (entry) => !("kind" in entry) && entry.rowId === rowId
1759
+ );
1760
+ if (index === -1) return;
1761
+ tableRef.current?.tBodies[0]?.rows[index]?.scrollIntoView(options);
1762
+ },
1763
+ scrollToColumn: (columnId, options) => {
1764
+ const cells = tableRef.current?.querySelectorAll("[data-gridkit-column]");
1765
+ const cell = cells ? Array.from(cells).find(
1766
+ (entry) => entry.getAttribute("data-gridkit-column") === columnId
1767
+ ) : void 0;
1768
+ cell?.scrollIntoView(options);
1769
+ }
1770
+ }));
1771
+ return /* @__PURE__ */ jsxs4("div", { className: "gridkit-data-grid-viewport", ref: viewportRef, children: [
1772
+ showGroupByBar && /* @__PURE__ */ jsx6(
1773
+ GroupByBar,
1774
+ {
1775
+ columns: resolved,
1776
+ grouping,
1777
+ columnName,
1778
+ headerDragEligible: dragEligibleForGroupBy,
1779
+ headerDragBlocked: draggedColumnAlreadyGrouped,
1780
+ headerDropTarget: headerGroupByDropTarget
1781
+ }
1782
+ ),
1783
+ /* @__PURE__ */ jsxs4(
1194
1784
  "table",
1195
1785
  {
1196
1786
  ref: tableRef,
1197
- role: "grid",
1198
- "aria-rowcount": shownRows.length + 1,
1787
+ role: groupBy.length > 0 ? "treegrid" : "grid",
1788
+ "aria-rowcount": displayRows.length + 1,
1199
1789
  "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 },
1790
+ ...ariaAttr(multiselectable, "aria-multiselectable", true),
1791
+ ...ariaAttr(labelledBy !== void 0, "aria-labelledby", labelledBy),
1792
+ ...ariaAttr(
1793
+ labelledBy === void 0 && label !== void 0,
1794
+ "aria-label",
1795
+ label
1796
+ ),
1203
1797
  onKeyDown: (event) => {
1204
1798
  if (event.key === "Escape") {
1205
1799
  if (drag.draggedColumnId !== null || resize.activeColumnId !== null)
@@ -1214,7 +1808,7 @@ function DataGridComponent({
1214
1808
  }
1215
1809
  },
1216
1810
  style: { width: totalColumnWidth(resolved) },
1217
- className: [
1811
+ className: classNames(
1218
1812
  "gridkit-data-grid",
1219
1813
  borders ? `borders-${borders}` : "",
1220
1814
  // Hover is on by default and selection off, so one set of classes
@@ -1226,10 +1820,10 @@ function DataGridComponent({
1226
1820
  selection.rowMode === false ? "" : "selectable-rows",
1227
1821
  selection.columnMode === false ? "" : "selectable-columns",
1228
1822
  selection.cellMode === false ? "" : "selectable-cells"
1229
- ].filter(Boolean).join(" "),
1823
+ ),
1230
1824
  children: [
1231
- /* @__PURE__ */ jsx4("colgroup", { children: resolved.map((entry) => /* @__PURE__ */ jsx4("col", { style: { width: entry.width } }, entry.id)) }),
1232
- /* @__PURE__ */ jsx4(
1825
+ /* @__PURE__ */ jsx6("colgroup", { children: resolved.map((entry) => /* @__PURE__ */ jsx6("col", { style: { width: entry.width } }, entry.id)) }),
1826
+ /* @__PURE__ */ jsx6(
1233
1827
  GridHeader,
1234
1828
  {
1235
1829
  columns: resolved,
@@ -1237,24 +1831,28 @@ function DataGridComponent({
1237
1831
  drag,
1238
1832
  sort: columnSort,
1239
1833
  sortableColumns,
1834
+ grouping,
1835
+ groupableColumns,
1836
+ groupToggleIconColumns,
1240
1837
  nav,
1241
1838
  selection
1242
1839
  }
1243
1840
  ),
1244
- /* @__PURE__ */ jsx4(
1841
+ /* @__PURE__ */ jsx6(
1245
1842
  GridBody,
1246
1843
  {
1247
1844
  columns: resolved,
1248
- rows: shownRows,
1845
+ rows: displayRows,
1249
1846
  activeColumnId: resize.activeColumnId,
1250
1847
  nav,
1251
- selection
1848
+ selection,
1849
+ grouping
1252
1850
  }
1253
1851
  )
1254
1852
  ]
1255
1853
  }
1256
1854
  ),
1257
- /* @__PURE__ */ jsx4("div", { className: "gridkit-sr-only", role: "status", "aria-live": "polite", children: announcement })
1855
+ /* @__PURE__ */ jsx6("div", { className: "gridkit-sr-only", role: "status", "aria-live": "polite", children: announcement })
1258
1856
  ] });
1259
1857
  }
1260
1858
  export {