@gridkitjs/react 0.4.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,
@@ -7,12 +12,23 @@ import {
7
12
  moveColumnBefore,
8
13
  resolveColumnWidths,
9
14
  resolveRowId,
15
+ resolveShownRows,
10
16
  totalColumnWidth
11
17
  } from "@gridkitjs/core";
12
18
 
13
19
  // src/DataGrid/components/GridHeader.tsx
14
20
  import { createPortal } from "react-dom";
15
- 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
+ }
16
32
 
17
33
  // src/DataGrid/useGridNavigation.ts
18
34
  import {
@@ -20,16 +36,14 @@ import {
20
36
  useRef,
21
37
  useState
22
38
  } from "react";
23
- var HEADER_ROW = -1;
39
+ import {
40
+ clampFocus,
41
+ nextFocusForKey,
42
+ HEADER_ROW
43
+ } from "@gridkitjs/core";
24
44
  var FALLBACK_PAGE = 10;
25
- function clampFocus(focus, rowCount, columnCount) {
26
- return {
27
- rowIndex: Math.min(Math.max(focus.rowIndex, HEADER_ROW), rowCount - 1),
28
- columnIndex: Math.min(
29
- Math.max(focus.columnIndex, 0),
30
- Math.max(columnCount - 1, 0)
31
- )
32
- };
45
+ function tabIndexFor(columnIndex, focusedColumnIndex) {
46
+ return columnIndex === focusedColumnIndex ? 0 : -1;
33
47
  }
34
48
  function cellAt(table, focus) {
35
49
  const row = focus.rowIndex === HEADER_ROW ? table.tHead?.rows[0] ?? null : table.tBodies[0]?.rows[focus.rowIndex] ?? null;
@@ -77,83 +91,71 @@ function useGridNavigation({
77
91
  pending.current = true;
78
92
  setStored(next);
79
93
  }
80
- function tabIndexFor(rowIndex, columnIndex) {
81
- return rowIndex === focus.rowIndex && columnIndex === focus.columnIndex ? 0 : -1;
82
- }
83
94
  function onKeyDown(event) {
84
- 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) {
85
109
  return;
86
110
  }
87
- const { rowIndex, columnIndex } = focus;
88
- let next;
89
- switch (event.key) {
90
- case "ArrowLeft":
91
- if (event.ctrlKey) return;
92
- next = { rowIndex, columnIndex: columnIndex - 1 };
93
- break;
94
- case "ArrowRight":
95
- if (event.ctrlKey) return;
96
- next = { rowIndex, columnIndex: columnIndex + 1 };
97
- break;
98
- case "ArrowUp":
99
- next = { rowIndex: rowIndex - 1, columnIndex };
100
- break;
101
- case "ArrowDown":
102
- next = { rowIndex: rowIndex + 1, columnIndex };
103
- break;
104
- // Ctrl takes Home and End to the grid's ends rather than the row's, which
105
- // is the one place Ctrl is navigation rather than reorder.
106
- case "Home":
107
- next = event.ctrlKey ? { rowIndex: HEADER_ROW, columnIndex: 0 } : { rowIndex, columnIndex: 0 };
108
- break;
109
- case "End":
110
- next = event.ctrlKey ? { rowIndex: rowCount - 1, columnIndex: columnCount - 1 } : { rowIndex, columnIndex: columnCount - 1 };
111
- break;
112
- case "PageUp":
113
- next = { rowIndex: rowIndex - pageSize(), columnIndex };
114
- break;
115
- case "PageDown":
116
- next = { rowIndex: rowIndex + pageSize(), columnIndex };
117
- break;
118
- default:
119
- return;
120
- }
121
111
  event.preventDefault();
122
112
  focusCell(next.rowIndex, next.columnIndex);
123
113
  }
124
- 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
+ };
125
123
  }
126
124
 
127
125
  // src/DataGrid/useGridSelection.ts
128
- import { useMemo, useRef as useRef2 } from "react";
129
126
  import {
130
- accessDotted,
127
+ useMemo,
128
+ useRef as useRef2
129
+ } from "react";
130
+ import {
131
+ applySelectionIntent,
131
132
  clearSelection,
132
133
  diffSelection,
134
+ resolveCell,
135
+ resolveColumns,
136
+ resolveRows,
133
137
  selectAll,
134
138
  selectCell,
135
- selectOnly,
136
- selectRange,
137
- toggleCellSelection,
138
- toggleSelection
139
+ toggleCellSelection
139
140
  } from "@gridkitjs/core";
140
- function intentOf(event) {
141
- if (event.shiftKey) {
142
- return "range";
141
+
142
+ // src/DataGrid/commitIfChanged.ts
143
+ function commitIfChanged(current, next, setState, onCommit) {
144
+ if (next === current) {
145
+ return false;
143
146
  }
144
- return event.ctrlKey || event.metaKey ? "toggle" : "replace";
147
+ setState(next);
148
+ onCommit?.(next);
149
+ return true;
145
150
  }
146
- function applyIntent(selection, orderedIds, anchorId, id, intent, mode) {
147
- switch (intent) {
148
- case "toggle":
149
- return toggleSelection(selection, id, mode);
150
- // A range with no anchor yet spans from the id to itself, which is the
151
- // plain click a first Shift-click may as well be.
152
- case "range":
153
- return selectRange(selection, orderedIds, anchorId ?? id, id, mode);
154
- default:
155
- return selectOnly(selection, id, mode);
156
- }
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
+ };
157
159
  }
158
160
  function countMessage(count, noun) {
159
161
  if (count === 0) {
@@ -161,6 +163,18 @@ function countMessage(count, noun) {
161
163
  }
162
164
  return count === 1 ? `1 ${noun} selected` : `${String(count)} ${noun}s selected`;
163
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
+ }
164
178
  function useGridSelection({
165
179
  rows,
166
180
  columns,
@@ -172,141 +186,106 @@ function useGridSelection({
172
186
  cellSelection,
173
187
  setCellSelection,
174
188
  callbacks,
175
- announce
189
+ announce,
190
+ columnName
176
191
  }) {
177
192
  const rowMode = selectable?.rows ?? false;
178
193
  const columnMode = selectable?.columns ?? false;
179
194
  const cellMode = selectable?.cells ?? false;
180
- const rowsById = useMemo(
181
- () => new Map(rows.map((entry) => [entry.rowId, entry])),
182
- [rows]
183
- );
184
- const rowIds = useMemo(() => rows.map((entry) => entry.rowId), [rows]);
185
- const columnIds = useMemo(() => columns.map((entry) => entry.id), [columns]);
186
- const selectedRowIds = useMemo(() => new Set(rowSelection), [rowSelection]);
187
- const selectedColumnIds = useMemo(
188
- () => new Set(columnSelection),
189
- [columnSelection]
190
- );
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);
191
200
  const rowAnchor = useRef2(null);
192
201
  const columnAnchor = useRef2(null);
193
- function resolveRows(ids) {
194
- return ids.map((id) => rowsById.get(id)).filter((row) => row !== void 0);
195
- }
196
- function resolveColumns(ids) {
197
- return ids.map((id) => {
198
- const columnIndex = columns.findIndex((entry) => entry.id === id);
199
- const column = columns[columnIndex];
200
- return column === void 0 ? void 0 : { columnId: id, column, columnIndex };
201
- }).filter((column) => column !== void 0);
202
- }
203
- function resolveCell(ref) {
204
- if (ref === null) {
205
- return null;
206
- }
207
- const entry = rowsById.get(ref.rowId);
208
- const columnIndex = columns.findIndex(
209
- (candidate) => candidate.id === ref.columnId
210
- );
211
- const column = columns[columnIndex];
212
- if (entry === void 0 || column === void 0) {
213
- return null;
214
- }
215
- return {
216
- rowId: ref.rowId,
217
- columnId: ref.columnId,
218
- row: entry.row,
219
- column,
220
- rowIndex: entry.rowIndex,
221
- columnIndex,
222
- value: accessDotted(entry.row, column.column.field)
223
- };
224
- }
225
202
  function announceCount(changed, total, noun) {
226
203
  if (changed > 1) {
227
204
  announce(countMessage(total, noun));
228
205
  }
229
206
  }
230
207
  function commitRows(next) {
231
- if (next === rowSelection) {
232
- return;
233
- }
234
- setRowSelection(next);
235
- const { added, removed } = diffSelection(rowSelection, next);
236
- const addedRows = resolveRows(added);
237
- const removedRows = resolveRows(removed);
238
- for (const row of addedRows) {
239
- callbacks.onRowSelect?.({ row, selection: next });
240
- }
241
- if (addedRows.length > 0) {
242
- callbacks.onRowsSelect?.({ rows: addedRows, selection: next });
243
- }
244
- for (const row of removedRows) {
245
- callbacks.onRowDeselect?.({ row, selection: next });
246
- }
247
- if (removedRows.length > 0) {
248
- callbacks.onRowsDeselect?.({ rows: removedRows, selection: next });
249
- }
250
- callbacks.onRowSelectionChange?.({
251
- added: addedRows,
252
- removed: removedRows,
253
- selected: resolveRows(next),
254
- 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");
255
231
  });
256
- announceCount(added.length + removed.length, next.length, "row");
257
232
  }
258
233
  function commitColumns(next) {
259
- if (next === columnSelection) {
260
- return;
261
- }
262
- setColumnSelection(next);
263
- const { added, removed } = diffSelection(columnSelection, next);
264
- const addedColumns = resolveColumns(added);
265
- const removedColumns = resolveColumns(removed);
266
- for (const column of addedColumns) {
267
- callbacks.onColumnSelect?.({ column, selection: next });
268
- }
269
- if (addedColumns.length > 0) {
270
- callbacks.onColumnsSelect?.({ columns: addedColumns, selection: next });
271
- }
272
- for (const column of removedColumns) {
273
- callbacks.onColumnDeselect?.({ column, selection: next });
274
- }
275
- if (removedColumns.length > 0) {
276
- callbacks.onColumnsDeselect?.({
277
- columns: removedColumns,
278
- 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
279
261
  });
280
- }
281
- callbacks.onColumnSelectionChange?.({
282
- added: addedColumns,
283
- removed: removedColumns,
284
- selected: resolveColumns(next),
285
- selection: next
262
+ announceCount(added.length + removed.length, committed.length, "column");
286
263
  });
287
- announceCount(added.length + removed.length, next.length, "column");
288
264
  }
289
265
  function commitCell(next) {
290
- if (next === cellSelection) {
291
- return;
292
- }
293
- setCellSelection(next);
294
- const selected = resolveCell(next);
295
- const deselected = resolveCell(cellSelection);
296
- if (deselected !== null) {
297
- callbacks.onCellDeselect?.({ cell: deselected, selection: next });
298
- }
299
- if (selected !== null) {
300
- callbacks.onCellSelect?.({ cell: selected, selection: next });
301
- }
302
- callbacks.onCellSelectionChange?.({
303
- selected,
304
- deselected,
305
- 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
+ }
306
285
  });
307
286
  }
308
287
  function selectRow(rowId, intent) {
309
- const next = applyIntent(
288
+ const next = applySelectionIntent(
310
289
  rowSelection,
311
290
  rowIds,
312
291
  rowAnchor.current,
@@ -320,7 +299,7 @@ function useGridSelection({
320
299
  commitRows(next);
321
300
  }
322
301
  function selectColumn(columnId, intent) {
323
- const next = applyIntent(
302
+ const next = applySelectionIntent(
324
303
  columnSelection,
325
304
  columnIds,
326
305
  columnAnchor.current,
@@ -363,10 +342,36 @@ function useGridSelection({
363
342
 
364
343
  // src/DataGrid/components/GridHeader.tsx
365
344
  import { jsx, jsxs } from "react/jsx-runtime";
345
+ function Icon({ children }) {
346
+ return /* @__PURE__ */ jsx(
347
+ "svg",
348
+ {
349
+ viewBox: "0 0 24 24",
350
+ fill: "none",
351
+ stroke: "currentColor",
352
+ strokeWidth: 2,
353
+ strokeLinecap: "round",
354
+ strokeLinejoin: "round",
355
+ "aria-hidden": "true",
356
+ children
357
+ }
358
+ );
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
+ }
366
+ function ChevronUpIcon() {
367
+ return /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" }) });
368
+ }
366
369
  function GridHeader({
367
370
  columns,
368
371
  resize,
369
372
  drag,
373
+ sort,
374
+ sortableColumns,
370
375
  nav,
371
376
  selection
372
377
  }) {
@@ -380,20 +385,27 @@ function GridHeader({
380
385
  const dropBefore = beforeId !== void 0 && beforeId === entry.id;
381
386
  const dropAfter = beforeId === null && index === columns.length - 1;
382
387
  const selected = selection.selectedColumnIds.has(entry.id);
383
- const shortcuts = [
384
- entry.reorderable ? "Control+ArrowLeft Control+ArrowRight" : "",
385
- entry.resizable ? "Alt+ArrowLeft Alt+ArrowRight" : ""
386
- ].filter(Boolean).join(" ");
388
+ const sortable = column.sortable ?? sortableColumns;
389
+ const sortDirection = sort.directionFor(entry.id);
390
+ const sortPriority = sort.priorityFor(entry.id);
391
+ const shortcuts = buildKeyShortcuts({
392
+ reorderable: entry.reorderable,
393
+ resizable: entry.resizable,
394
+ sortable
395
+ });
387
396
  return /* @__PURE__ */ jsxs(
388
397
  "th",
389
398
  {
390
399
  scope: "col",
400
+ role: "columnheader",
391
401
  "data-gridkit-column": entry.id,
392
402
  "aria-colindex": index + 1,
393
403
  tabIndex: nav.tabIndexFor(HEADER_ROW, index),
394
- ...selection.columnMode !== false && {
395
- "aria-selected": selected
396
- },
404
+ ...ariaAttr(
405
+ selection.columnMode !== false,
406
+ "aria-selected",
407
+ selected
408
+ ),
397
409
  onFocus: () => {
398
410
  nav.focusCell(HEADER_ROW, index);
399
411
  },
@@ -401,7 +413,7 @@ function GridHeader({
401
413
  if (drag.justDragged()) {
402
414
  return;
403
415
  }
404
- if (event.target instanceof Element && event.target.closest(".header-resize-handle") !== null) {
416
+ if (event.target instanceof Element && (event.target.closest(".header-resize-handle") !== null || event.target.closest(".header-sort-toggle") !== null)) {
405
417
  return;
406
418
  }
407
419
  selection.selectColumn(entry.id, intentOf(event));
@@ -419,21 +431,27 @@ function GridHeader({
419
431
  resize.nudge(entry, direction * KEYBOARD_STEP);
420
432
  return;
421
433
  }
434
+ if (event.key === "Enter" && event.altKey && entry.resizable) {
435
+ event.preventDefault();
436
+ resize.sizeToContent(entry);
437
+ return;
438
+ }
439
+ if (event.key === "ArrowUp" && event.altKey && sortable) {
440
+ event.preventDefault();
441
+ sort.toggle(entry, { shiftKey: event.shiftKey });
442
+ return;
443
+ }
422
444
  if (event.key === " " || event.key === "Enter") {
423
445
  event.preventDefault();
424
446
  selection.selectColumn(
425
447
  entry.id,
426
- intentOf({
427
- ctrlKey: event.key === " " || event.ctrlKey,
428
- metaKey: event.metaKey,
429
- shiftKey: event.shiftKey
430
- })
448
+ intentOf(keyboardSelectIntent(event))
431
449
  );
432
450
  return;
433
451
  }
434
452
  nav.onKeyDown(event);
435
453
  },
436
- className: [
454
+ className: classNames(
437
455
  "header-cell",
438
456
  entry.reorderable ? "is-reorderable" : "",
439
457
  selected ? "is-selected" : "",
@@ -443,8 +461,13 @@ function GridHeader({
443
461
  dropAfter ? "is-drop-after" : "",
444
462
  column.wrap?.header ? "is-wrapped" : "",
445
463
  column.headerClassName ?? ""
446
- ].filter(Boolean).join(" "),
447
- ...shortcuts !== "" && { "aria-keyshortcuts": shortcuts },
464
+ ),
465
+ ...ariaAttr(shortcuts !== "", "aria-keyshortcuts", shortcuts),
466
+ ...ariaAttr(
467
+ sortable && sortDirection !== null,
468
+ "aria-sort",
469
+ sortDirection === "asc" ? "ascending" : "descending"
470
+ ),
448
471
  ...entry.reorderable && {
449
472
  onPointerDown: (event) => {
450
473
  drag.startDrag(entry, event);
@@ -452,6 +475,33 @@ function GridHeader({
452
475
  },
453
476
  children: [
454
477
  entry.label,
478
+ sortable && /*
479
+ * A pointer affordance only, and hidden from assistive
480
+ * technology like the resize handle: the keyboard equivalent
481
+ * lives on the header itself, under Alt+ArrowUp (plain) /
482
+ * Alt+Shift+ArrowUp (stacking).
483
+ */
484
+ /* @__PURE__ */ jsxs(
485
+ "span",
486
+ {
487
+ className: classNames(
488
+ "header-sort-toggle",
489
+ sortDirection !== null ? `is-sorted-${sortDirection}` : ""
490
+ ),
491
+ "aria-hidden": "true",
492
+ tabIndex: -1,
493
+ onPointerDown: (event) => {
494
+ event.stopPropagation();
495
+ },
496
+ onClick: (event) => {
497
+ sort.toggle(entry, { shiftKey: event.shiftKey });
498
+ },
499
+ children: [
500
+ sortDirection === null ? /* @__PURE__ */ jsx(ChevronsUpDownIcon, {}) : /* @__PURE__ */ jsx(ChevronUpIcon, {}),
501
+ sortPriority !== null && sortPriority > 1 && /* @__PURE__ */ jsx("span", { className: "header-sort-priority", children: sortPriority })
502
+ ]
503
+ }
504
+ ),
455
505
  entry.resizable && /*
456
506
  * A pointer affordance only, and hidden from assistive
457
507
  * technology accordingly: a focusable handle per column would
@@ -495,8 +545,11 @@ function GridHeader({
495
545
  ] });
496
546
  }
497
547
 
548
+ // src/DataGrid/components/GridBody.tsx
549
+ import { intentOf as intentOf2 } from "@gridkitjs/core";
550
+
498
551
  // src/DataGrid/components/GridRow.tsx
499
- import { accessDotted as accessDotted2 } from "@gridkitjs/core";
552
+ import { accessDotted } from "@gridkitjs/core";
500
553
  import { memo } from "react";
501
554
  import { jsx as jsx2 } from "react/jsx-runtime";
502
555
  function GridRowComponent({
@@ -517,10 +570,10 @@ function GridRowComponent({
517
570
  {
518
571
  role: "row",
519
572
  "aria-rowindex": rowIndex + 2,
520
- ...rowsSelectable && { "aria-selected": selected },
521
- className: ["grid-row", selected ? "is-selected" : ""].filter(Boolean).join(" "),
573
+ ...ariaAttr(rowsSelectable, "aria-selected", selected),
574
+ className: classNames("grid-row", selected ? "is-selected" : ""),
522
575
  children: columns.map(({ id, column, alignment }, columnIndex) => {
523
- const value = accessDotted2(row, column.field);
576
+ const value = accessDotted(row, column.field);
524
577
  const cellSelected = id === selectedColumnId;
525
578
  return /* @__PURE__ */ jsx2(
526
579
  "td",
@@ -528,15 +581,16 @@ function GridRowComponent({
528
581
  role: "gridcell",
529
582
  "data-gridkit-column": id,
530
583
  "aria-colindex": columnIndex + 1,
531
- tabIndex: columnIndex === focusedColumnIndex ? 0 : -1,
532
- ...cellsSelectable && { "aria-selected": cellSelected },
533
- className: [
584
+ tabIndex: tabIndexFor(columnIndex, focusedColumnIndex),
585
+ ...ariaAttr(cellsSelectable, "aria-selected", cellSelected),
586
+ ...ariaAttr(cellsSelectable, "aria-keyshortcuts", "Space Enter"),
587
+ className: classNames(
534
588
  "grid-cell",
535
589
  id === activeColumnId ? "is-resizing" : "",
536
590
  cellSelected || selectedColumnIds.has(id) ? "is-selected" : "",
537
591
  column.wrap?.cells ? "is-wrapped" : "",
538
592
  column.cellClassName ?? ""
539
- ].filter(Boolean).join(" "),
593
+ ),
540
594
  style: { textAlign: alignment },
541
595
  children: column.cellTemplate ? column.cellTemplate({ value, row, rowIndex, rowId, selected }) : value
542
596
  },
@@ -581,7 +635,7 @@ function GridBody({
581
635
  return position === null || entry === void 0 ? null : { position, rowId: entry.rowId };
582
636
  }
583
637
  function select(rowId, columnId, modifiers) {
584
- const intent = intentOf(modifiers);
638
+ const intent = intentOf2(modifiers);
585
639
  if (rowMode !== false) {
586
640
  selection.selectRow(rowId, intent);
587
641
  }
@@ -608,11 +662,11 @@ function GridBody({
608
662
  const found = targetOf(event.target);
609
663
  if (found === null) return;
610
664
  event.preventDefault();
611
- select(found.rowId, found.position.columnId, {
612
- ctrlKey: event.key === " " || event.ctrlKey,
613
- metaKey: event.metaKey,
614
- shiftKey: event.shiftKey
615
- });
665
+ select(
666
+ found.rowId,
667
+ found.position.columnId,
668
+ keyboardSelectIntent(event)
669
+ );
616
670
  return;
617
671
  }
618
672
  nav.onKeyDown(event);
@@ -645,8 +699,45 @@ import {
645
699
  } from "react";
646
700
  import {
647
701
  movesColumn,
648
- resolveDropBefore
702
+ resolveDropBefore,
703
+ resolveKeyboardDropTarget
649
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
650
741
  var DRAG_THRESHOLD = 4;
651
742
  function headerAt(x, y) {
652
743
  const element = document.elementFromPoint(x, y);
@@ -678,70 +769,49 @@ function useColumnDrag({
678
769
  function placeGhost(x, y) {
679
770
  setGhostTransform(`translate(${String(x - grabX)}px, ${String(y)}px)`);
680
771
  }
681
- header.setPointerCapture(pointerId);
682
- function move(moveEvent) {
683
- const { clientX, clientY } = moveEvent;
684
- if (!dragging) {
685
- if (Math.abs(clientX - startX) < DRAG_THRESHOLD) {
686
- return;
687
- }
688
- dragging = true;
689
- dragged.current = true;
690
- setDraggedColumnId(entry.id);
691
- }
692
- placeGhost(clientX, clientY);
693
- const over = headerAt(clientX, clientY);
694
- const overId = over?.getAttribute("data-gridkit-column");
695
- const beforeId = over && overId ? resolveDropBefore(order, overId, sideOf(over, clientX)) : void 0;
696
- target = beforeId !== void 0 && movesColumn(order, entry.id, beforeId) ? { kind: "column-order", beforeId } : null;
697
- setDropTarget(target);
698
- }
699
- function stop() {
700
- header.removeEventListener("pointermove", move);
701
- header.removeEventListener("pointerup", end);
702
- header.removeEventListener("pointercancel", cancel);
703
- window.removeEventListener("keydown", onKeyDown);
704
- if (header.hasPointerCapture(pointerId)) {
705
- header.releasePointerCapture(pointerId);
706
- }
772
+ function reset() {
707
773
  setGhostTransform(null);
708
774
  setDraggedColumnId(null);
709
775
  setDropTarget(null);
710
776
  }
711
- function end() {
712
- const dropped = target;
713
- stop();
714
- if (dragging && dropped !== null) {
715
- onDrop(dropped, entry.id);
716
- }
717
- }
718
- function cancel() {
719
- target = null;
720
- stop();
721
- }
722
- function onKeyDown(keyEvent) {
723
- if (keyEvent.key === "Escape") {
724
- 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();
725
806
  }
726
- }
727
- header.addEventListener("pointermove", move);
728
- header.addEventListener("pointerup", end);
729
- header.addEventListener("pointercancel", cancel);
730
- window.addEventListener("keydown", onKeyDown);
807
+ });
731
808
  }
732
809
  function moveByKeyboard(entry, direction) {
733
- const index = order.indexOf(entry.id);
734
- if (index === -1) {
735
- return;
736
- }
737
- const targetIndex = direction === -1 ? index - 1 : index + 2;
738
- if (targetIndex < 0) {
810
+ const beforeId = resolveKeyboardDropTarget(order, entry.id, direction);
811
+ if (beforeId === void 0) {
739
812
  return;
740
813
  }
741
- onDrop(
742
- { kind: "column-order", beforeId: order[targetIndex] ?? null },
743
- entry.id
744
- );
814
+ onDrop({ kind: "column-order", beforeId }, entry.id);
745
815
  }
746
816
  return {
747
817
  draggedColumnId,
@@ -762,13 +832,14 @@ import {
762
832
  beginColumnResize,
763
833
  clampColumnWidth,
764
834
  resolveColumnConstraints,
835
+ revertColumnSize,
765
836
  sizeColumnToContent
766
837
  } from "@gridkitjs/core";
767
838
 
768
839
  // src/DataGrid/measureColumnContent.ts
769
840
  function measureColumnContent(table, columnId) {
770
841
  const cells = table.querySelectorAll(
771
- `[data-gridkit-column="${CSS.escape(columnId)}"]`
842
+ `td[data-gridkit-column="${CSS.escape(columnId)}"]`
772
843
  );
773
844
  const range = document.createRange();
774
845
  let widest = null;
@@ -810,45 +881,39 @@ function useColumnResize({
810
881
  let width = session.startWidth;
811
882
  event.preventDefault();
812
883
  event.stopPropagation();
813
- handle.setPointerCapture(pointerId);
814
884
  setActiveColumnId(session.columnId);
815
- function move(moveEvent) {
816
- width = applyColumnResize(session, moveEvent.clientX);
817
- commit(base, session.columnId, width, "move");
818
- }
819
- function stop() {
820
- handle.removeEventListener("pointermove", move);
821
- handle.removeEventListener("pointerup", end);
822
- handle.removeEventListener("pointercancel", cancel);
823
- window.removeEventListener("keydown", onKeyDown);
824
- if (handle.hasPointerCapture(pointerId)) {
825
- handle.releasePointerCapture(pointerId);
826
- }
827
- setActiveColumnId(null);
828
- }
829
- function end() {
830
- stop();
831
- commit(base, session.columnId, width, "end");
832
- }
833
- function cancel() {
834
- stop();
835
- setSizing(base);
836
- onColumnResize?.({
837
- columnId: session.columnId,
838
- width: session.startWidth,
839
- sizing: base,
840
- phase: "end"
841
- });
842
- }
843
- function onKeyDown(keyEvent) {
844
- if (keyEvent.key === "Escape") {
845
- cancel();
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");
894
+ }
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;
914
+ });
846
915
  }
847
- }
848
- handle.addEventListener("pointermove", move);
849
- handle.addEventListener("pointerup", end);
850
- handle.addEventListener("pointercancel", cancel);
851
- window.addEventListener("keydown", onKeyDown);
916
+ });
852
917
  }
853
918
  function sizeToContent(entry) {
854
919
  const table = tableRef.current;
@@ -883,6 +948,31 @@ function useColumnResize({
883
948
  return { activeColumnId, startResize, sizeToContent, nudge };
884
949
  }
885
950
 
951
+ // src/DataGrid/useColumnSort.ts
952
+ import {
953
+ sortDirectionFor,
954
+ sortPriorityFor,
955
+ toggleColumnSort
956
+ } from "@gridkitjs/core";
957
+ function useColumnSort({
958
+ sort,
959
+ setSort,
960
+ onColumnSortChange
961
+ }) {
962
+ function toggle(entry, event) {
963
+ const next = toggleColumnSort(sort, entry.id, { stack: event.shiftKey });
964
+ commitIfChanged(sort, next, setSort, (committed) => {
965
+ onColumnSortChange?.({ columnId: entry.id, sort: committed });
966
+ });
967
+ }
968
+ return {
969
+ sort,
970
+ directionFor: (columnId) => sortDirectionFor(sort, columnId),
971
+ priorityFor: (columnId) => sortPriorityFor(sort, columnId),
972
+ toggle
973
+ };
974
+ }
975
+
886
976
  // src/DataGrid/useElementWidth.ts
887
977
  import { useLayoutEffect, useState as useState4 } from "react";
888
978
  function useElementWidth(ref, enabled) {
@@ -924,8 +1014,13 @@ function DataGridComponent({
924
1014
  columnSizeDefaults,
925
1015
  onColumnResize,
926
1016
  onColumnOrderChange,
1017
+ sortableColumns = false,
1018
+ defaultColumnSort,
1019
+ onColumnSortChange,
1020
+ defaultFilter,
927
1021
  label,
928
1022
  labelledBy,
1023
+ ref,
929
1024
  ...callbacks
930
1025
  }) {
931
1026
  const hoverRows = hoverable?.rows ?? true;
@@ -940,6 +1035,8 @@ function DataGridComponent({
940
1035
  const [order, setOrder] = useState5(
941
1036
  defaultColumnOrder ?? []
942
1037
  );
1038
+ const [sort, setSort] = useState5(defaultColumnSort ?? []);
1039
+ const [filter] = useState5(defaultFilter ?? []);
943
1040
  const [announcement, setAnnouncement] = useState5("");
944
1041
  const [rowSelection, setRowSelection] = useState5(
945
1042
  defaultRowSelection ?? []
@@ -984,6 +1081,10 @@ function DataGridComponent({
984
1081
  resizableColumns,
985
1082
  reorderableColumns
986
1083
  ]);
1084
+ const shownRows = useMemo2(
1085
+ () => resolveShownRows(rows, filter, sort, resolved),
1086
+ [rows, filter, sort, resolved]
1087
+ );
987
1088
  function columnName(columnId) {
988
1089
  const entry = resolved.find((candidate) => candidate.id === columnId);
989
1090
  if (entry === void 0) {
@@ -1011,7 +1112,7 @@ function DataGridComponent({
1011
1112
  });
1012
1113
  const nav = useGridNavigation({
1013
1114
  tableRef,
1014
- rowCount: rows.length,
1115
+ rowCount: shownRows.length,
1015
1116
  columnCount: resolved.length
1016
1117
  });
1017
1118
  const displayedIds = useMemo2(
@@ -1032,8 +1133,27 @@ function DataGridComponent({
1032
1133
  nav.focusCell(HEADER_ROW, position);
1033
1134
  }
1034
1135
  const drag = useColumnDrag({ order: displayedIds, onDrop: handleDrop });
1136
+ function handleColumnSortChange(event) {
1137
+ onColumnSortChange?.(event);
1138
+ const entry = event.sort.find(
1139
+ (candidate) => candidate.columnId === event.columnId
1140
+ );
1141
+ if (entry === void 0) {
1142
+ announce(`${columnName(event.columnId)}, sort cleared`);
1143
+ return;
1144
+ }
1145
+ const direction = entry.direction === "asc" ? "ascending" : "descending";
1146
+ announce(
1147
+ event.sort.length > 1 ? `${columnName(event.columnId)} sorted ${direction}, key ${String(event.sort.indexOf(entry) + 1)} of ${String(event.sort.length)}` : `${columnName(event.columnId)} sorted ${direction}`
1148
+ );
1149
+ }
1150
+ const columnSort = useColumnSort({
1151
+ sort,
1152
+ setSort,
1153
+ onColumnSortChange: handleColumnSortChange
1154
+ });
1035
1155
  const selection = useGridSelection({
1036
- rows,
1156
+ rows: shownRows,
1037
1157
  columns: resolved,
1038
1158
  selectable,
1039
1159
  rowSelection,
@@ -1043,20 +1163,57 @@ function DataGridComponent({
1043
1163
  cellSelection,
1044
1164
  setCellSelection,
1045
1165
  callbacks,
1046
- announce
1166
+ announce,
1167
+ columnName
1047
1168
  });
1048
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
+ }));
1049
1202
  return /* @__PURE__ */ jsxs2("div", { className: "gridkit-data-grid-viewport", ref: viewportRef, children: [
1050
1203
  /* @__PURE__ */ jsxs2(
1051
1204
  "table",
1052
1205
  {
1053
1206
  ref: tableRef,
1054
1207
  role: "grid",
1055
- "aria-rowcount": rows.length + 1,
1208
+ "aria-rowcount": shownRows.length + 1,
1056
1209
  "aria-colcount": resolved.length,
1057
- ...multiselectable && { "aria-multiselectable": true },
1058
- ...labelledBy !== void 0 && { "aria-labelledby": labelledBy },
1059
- ...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
+ ),
1060
1217
  onKeyDown: (event) => {
1061
1218
  if (event.key === "Escape") {
1062
1219
  if (drag.draggedColumnId !== null || resize.activeColumnId !== null)
@@ -1071,7 +1228,7 @@ function DataGridComponent({
1071
1228
  }
1072
1229
  },
1073
1230
  style: { width: totalColumnWidth(resolved) },
1074
- className: [
1231
+ className: classNames(
1075
1232
  "gridkit-data-grid",
1076
1233
  borders ? `borders-${borders}` : "",
1077
1234
  // Hover is on by default and selection off, so one set of classes
@@ -1083,7 +1240,7 @@ function DataGridComponent({
1083
1240
  selection.rowMode === false ? "" : "selectable-rows",
1084
1241
  selection.columnMode === false ? "" : "selectable-columns",
1085
1242
  selection.cellMode === false ? "" : "selectable-cells"
1086
- ].filter(Boolean).join(" "),
1243
+ ),
1087
1244
  children: [
1088
1245
  /* @__PURE__ */ jsx4("colgroup", { children: resolved.map((entry) => /* @__PURE__ */ jsx4("col", { style: { width: entry.width } }, entry.id)) }),
1089
1246
  /* @__PURE__ */ jsx4(
@@ -1092,6 +1249,8 @@ function DataGridComponent({
1092
1249
  columns: resolved,
1093
1250
  resize,
1094
1251
  drag,
1252
+ sort: columnSort,
1253
+ sortableColumns,
1095
1254
  nav,
1096
1255
  selection
1097
1256
  }
@@ -1100,7 +1259,7 @@ function DataGridComponent({
1100
1259
  GridBody,
1101
1260
  {
1102
1261
  columns: resolved,
1103
- rows,
1262
+ rows: shownRows,
1104
1263
  activeColumnId: resize.activeColumnId,
1105
1264
  nav,
1106
1265
  selection