@gridkitjs/react 0.3.1 → 0.5.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,38 +1,487 @@
1
1
  // src/DataGrid/DataGrid.tsx
2
- import { useMemo, useRef, useState as useState4 } from "react";
2
+ import { useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
3
3
  import {
4
4
  applyColumnOrder,
5
5
  defineColumnsFromRows,
6
6
  fitColumnsToWidth,
7
7
  moveColumnBefore,
8
8
  resolveColumnWidths,
9
+ resolveRowId,
10
+ resolveShownRows,
9
11
  totalColumnWidth
10
12
  } from "@gridkitjs/core";
11
13
 
12
14
  // src/DataGrid/components/GridHeader.tsx
13
15
  import { createPortal } from "react-dom";
14
16
  import { KEYBOARD_STEP } from "@gridkitjs/core";
17
+
18
+ // src/DataGrid/useGridNavigation.ts
19
+ import {
20
+ useEffect,
21
+ useRef,
22
+ useState
23
+ } from "react";
24
+ var HEADER_ROW = -1;
25
+ 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
+ };
34
+ }
35
+ function cellAt(table, focus) {
36
+ 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;
38
+ }
39
+ function useGridNavigation({
40
+ tableRef,
41
+ rowCount,
42
+ columnCount
43
+ }) {
44
+ const [stored, setStored] = useState({
45
+ rowIndex: HEADER_ROW,
46
+ columnIndex: 0
47
+ });
48
+ const focus = clampFocus(stored, rowCount, columnCount);
49
+ const pending = useRef(false);
50
+ useEffect(() => {
51
+ if (!pending.current) {
52
+ return;
53
+ }
54
+ pending.current = false;
55
+ const table = tableRef.current;
56
+ if (table === null) {
57
+ return;
58
+ }
59
+ cellAt(table, { rowIndex: focus.rowIndex, columnIndex: focus.columnIndex })?.focus();
60
+ }, [tableRef, focus.rowIndex, focus.columnIndex]);
61
+ function pageSize() {
62
+ const table = tableRef.current;
63
+ const firstRow = table?.tBodies[0]?.rows[0];
64
+ const viewport = table?.parentElement;
65
+ if (firstRow === void 0 || viewport === null || viewport === void 0 || firstRow.offsetHeight === 0) {
66
+ return FALLBACK_PAGE;
67
+ }
68
+ return Math.max(
69
+ 1,
70
+ Math.floor(viewport.clientHeight / firstRow.offsetHeight)
71
+ );
72
+ }
73
+ function focusCell(rowIndex, columnIndex) {
74
+ const next = clampFocus({ rowIndex, columnIndex }, rowCount, columnCount);
75
+ if (next.rowIndex === focus.rowIndex && next.columnIndex === focus.columnIndex) {
76
+ return;
77
+ }
78
+ pending.current = true;
79
+ setStored(next);
80
+ }
81
+ function tabIndexFor(rowIndex, columnIndex) {
82
+ return rowIndex === focus.rowIndex && columnIndex === focus.columnIndex ? 0 : -1;
83
+ }
84
+ function onKeyDown(event) {
85
+ if (event.altKey) {
86
+ return;
87
+ }
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
+ event.preventDefault();
123
+ focusCell(next.rowIndex, next.columnIndex);
124
+ }
125
+ return { focus, tabIndexFor, focusCell, onKeyDown };
126
+ }
127
+
128
+ // src/DataGrid/useGridSelection.ts
129
+ import { useMemo, useRef as useRef2 } from "react";
130
+ import {
131
+ accessDotted,
132
+ clearSelection,
133
+ diffSelection,
134
+ selectAll,
135
+ selectCell,
136
+ selectOnly,
137
+ selectRange,
138
+ toggleCellSelection,
139
+ toggleSelection
140
+ } from "@gridkitjs/core";
141
+ function intentOf(event) {
142
+ if (event.shiftKey) {
143
+ return "range";
144
+ }
145
+ return event.ctrlKey || event.metaKey ? "toggle" : "replace";
146
+ }
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
+ }
158
+ }
159
+ function countMessage(count, noun) {
160
+ if (count === 0) {
161
+ return `No ${noun}s selected`;
162
+ }
163
+ return count === 1 ? `1 ${noun} selected` : `${String(count)} ${noun}s selected`;
164
+ }
165
+ function useGridSelection({
166
+ rows,
167
+ columns,
168
+ selectable,
169
+ rowSelection,
170
+ setRowSelection,
171
+ columnSelection,
172
+ setColumnSelection,
173
+ cellSelection,
174
+ setCellSelection,
175
+ callbacks,
176
+ announce
177
+ }) {
178
+ const rowMode = selectable?.rows ?? false;
179
+ const columnMode = selectable?.columns ?? false;
180
+ 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
+ );
192
+ const rowAnchor = useRef2(null);
193
+ 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
+ function announceCount(changed, total, noun) {
227
+ if (changed > 1) {
228
+ announce(countMessage(total, noun));
229
+ }
230
+ }
231
+ 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
256
+ });
257
+ announceCount(added.length + removed.length, next.length, "row");
258
+ }
259
+ 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
280
+ });
281
+ }
282
+ callbacks.onColumnSelectionChange?.({
283
+ added: addedColumns,
284
+ removed: removedColumns,
285
+ selected: resolveColumns(next),
286
+ selection: next
287
+ });
288
+ announceCount(added.length + removed.length, next.length, "column");
289
+ }
290
+ 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
307
+ });
308
+ }
309
+ function selectRow(rowId, intent) {
310
+ const next = applyIntent(
311
+ rowSelection,
312
+ rowIds,
313
+ rowAnchor.current,
314
+ rowId,
315
+ intent,
316
+ rowMode
317
+ );
318
+ if (intent !== "range") {
319
+ rowAnchor.current = rowId;
320
+ }
321
+ commitRows(next);
322
+ }
323
+ function selectColumn(columnId, intent) {
324
+ const next = applyIntent(
325
+ columnSelection,
326
+ columnIds,
327
+ columnAnchor.current,
328
+ columnId,
329
+ intent,
330
+ columnMode
331
+ );
332
+ if (intent !== "range") {
333
+ columnAnchor.current = columnId;
334
+ }
335
+ commitColumns(next);
336
+ }
337
+ function selectCellAt(cell, intent) {
338
+ commitCell(
339
+ intent === "toggle" ? toggleCellSelection(cellSelection, cell, cellMode) : selectCell(cellSelection, cell, cellMode)
340
+ );
341
+ }
342
+ function selectAllRows() {
343
+ commitRows(selectAll(rowSelection, rowIds, rowMode));
344
+ }
345
+ function clear() {
346
+ commitRows(clearSelection(rowSelection));
347
+ commitColumns(clearSelection(columnSelection));
348
+ commitCell(null);
349
+ }
350
+ return {
351
+ rowMode,
352
+ columnMode,
353
+ cellMode,
354
+ selectedRowIds,
355
+ selectedColumnIds,
356
+ selectedCell: cellSelection,
357
+ selectRow,
358
+ selectColumn,
359
+ selectCell: selectCellAt,
360
+ selectAllRows,
361
+ clear
362
+ };
363
+ }
364
+
365
+ // src/DataGrid/components/GridHeader.tsx
15
366
  import { jsx, jsxs } from "react/jsx-runtime";
367
+ function ChevronsUpDownIcon() {
368
+ return /* @__PURE__ */ jsxs(
369
+ "svg",
370
+ {
371
+ viewBox: "0 0 24 24",
372
+ fill: "none",
373
+ stroke: "currentColor",
374
+ strokeWidth: 2,
375
+ strokeLinecap: "round",
376
+ strokeLinejoin: "round",
377
+ "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
+ ]
382
+ }
383
+ );
384
+ }
385
+ 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
+ );
399
+ }
16
400
  function GridHeader({
17
401
  columns,
18
402
  resize,
19
- drag
403
+ drag,
404
+ sort,
405
+ sortableColumns,
406
+ nav,
407
+ selection
20
408
  }) {
21
409
  const { beforeId } = drag.dropTarget ?? {};
22
410
  const draggedEntry = columns.find((entry) => entry.id === drag.draggedColumnId) ?? null;
23
411
  return /* @__PURE__ */ jsxs("thead", { children: [
24
- /* @__PURE__ */ jsx("tr", { className: "grid-header", children: columns.map((entry, index) => {
412
+ /* @__PURE__ */ jsx("tr", { className: "grid-header", role: "row", "aria-rowindex": 1, children: columns.map((entry, index) => {
25
413
  const { column } = entry;
26
414
  const resizing = resize.activeColumnId === entry.id;
27
415
  const dragging = drag.draggedColumnId === entry.id;
28
416
  const dropBefore = beforeId !== void 0 && beforeId === entry.id;
29
417
  const dropAfter = beforeId === null && index === columns.length - 1;
418
+ const selected = selection.selectedColumnIds.has(entry.id);
419
+ const sortable = column.sortable ?? sortableColumns;
420
+ const sortDirection = sort.directionFor(entry.id);
421
+ 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(" ");
30
427
  return /* @__PURE__ */ jsxs(
31
428
  "th",
32
429
  {
430
+ scope: "col",
33
431
  "data-gridkit-column": entry.id,
432
+ "aria-colindex": index + 1,
433
+ tabIndex: nav.tabIndexFor(HEADER_ROW, index),
434
+ ...selection.columnMode !== false && {
435
+ "aria-selected": selected
436
+ },
437
+ onFocus: () => {
438
+ nav.focusCell(HEADER_ROW, index);
439
+ },
440
+ onClick: (event) => {
441
+ if (drag.justDragged()) {
442
+ return;
443
+ }
444
+ if (event.target instanceof Element && (event.target.closest(".header-resize-handle") !== null || event.target.closest(".header-sort-toggle") !== null)) {
445
+ return;
446
+ }
447
+ selection.selectColumn(entry.id, intentOf(event));
448
+ },
449
+ onKeyDown: (event) => {
450
+ const horizontal = event.key === "ArrowLeft" || event.key === "ArrowRight";
451
+ const direction = event.key === "ArrowLeft" ? -1 : 1;
452
+ if (horizontal && event.ctrlKey && entry.reorderable) {
453
+ event.preventDefault();
454
+ drag.moveByKeyboard(entry, direction);
455
+ return;
456
+ }
457
+ if (horizontal && event.altKey && entry.resizable) {
458
+ event.preventDefault();
459
+ resize.nudge(entry, direction * KEYBOARD_STEP);
460
+ return;
461
+ }
462
+ if (event.key === "ArrowUp" && event.altKey && sortable) {
463
+ event.preventDefault();
464
+ sort.toggle(entry, { shiftKey: event.shiftKey });
465
+ return;
466
+ }
467
+ if (event.key === " " || event.key === "Enter") {
468
+ event.preventDefault();
469
+ selection.selectColumn(
470
+ entry.id,
471
+ intentOf({
472
+ ctrlKey: event.key === " " || event.ctrlKey,
473
+ metaKey: event.metaKey,
474
+ shiftKey: event.shiftKey
475
+ })
476
+ );
477
+ return;
478
+ }
479
+ nav.onKeyDown(event);
480
+ },
34
481
  className: [
35
482
  "header-cell",
483
+ entry.reorderable ? "is-reorderable" : "",
484
+ selected ? "is-selected" : "",
36
485
  resizing ? "is-resizing" : "",
37
486
  dragging ? "is-dragging" : "",
38
487
  dropBefore ? "is-drop-before" : "",
@@ -40,46 +489,61 @@ function GridHeader({
40
489
  column.wrap?.header ? "is-wrapped" : "",
41
490
  column.headerClassName ?? ""
42
491
  ].filter(Boolean).join(" "),
492
+ ...shortcuts !== "" && { "aria-keyshortcuts": shortcuts },
493
+ ...sortable && sortDirection !== null && {
494
+ "aria-sort": sortDirection === "asc" ? "ascending" : "descending"
495
+ },
43
496
  ...entry.reorderable && {
44
- tabIndex: 0,
45
497
  onPointerDown: (event) => {
46
498
  drag.startDrag(entry, event);
47
- },
48
- onKeyDown: (event) => {
49
- if (!event.ctrlKey) return;
50
- if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
51
- return;
52
- event.preventDefault();
53
- drag.moveByKeyboard(
54
- entry,
55
- event.key === "ArrowLeft" ? -1 : 1
56
- );
57
499
  }
58
500
  },
59
501
  children: [
60
502
  entry.label,
61
- entry.resizable && /* @__PURE__ */ jsx(
503
+ sortable && /*
504
+ * A pointer affordance only, and hidden from assistive
505
+ * technology like the resize handle: the keyboard equivalent
506
+ * lives on the header itself, under Alt+ArrowUp (plain) /
507
+ * Alt+Shift+ArrowUp (stacking).
508
+ */
509
+ /* @__PURE__ */ jsxs(
510
+ "span",
511
+ {
512
+ className: [
513
+ "header-sort-toggle",
514
+ sortDirection !== null ? `is-sorted-${sortDirection}` : ""
515
+ ].filter(Boolean).join(" "),
516
+ "aria-hidden": "true",
517
+ tabIndex: -1,
518
+ onPointerDown: (event) => {
519
+ event.stopPropagation();
520
+ },
521
+ onClick: (event) => {
522
+ sort.toggle(entry, { shiftKey: event.shiftKey });
523
+ },
524
+ children: [
525
+ sortDirection === null ? /* @__PURE__ */ jsx(ChevronsUpDownIcon, {}) : /* @__PURE__ */ jsx(ChevronUpIcon, {}),
526
+ sortPriority !== null && sortPriority > 1 && /* @__PURE__ */ jsx("span", { className: "header-sort-priority", children: sortPriority })
527
+ ]
528
+ }
529
+ ),
530
+ entry.resizable && /*
531
+ * A pointer affordance only, and hidden from assistive
532
+ * technology accordingly: a focusable handle per column would
533
+ * put N tab stops inside a widget that is meant to have one.
534
+ * The keyboard resizes from the header itself, under Alt.
535
+ */
536
+ /* @__PURE__ */ jsx(
62
537
  "span",
63
538
  {
64
539
  className: "header-resize-handle",
65
- role: "separator",
66
- "aria-orientation": "vertical",
67
- "aria-label": `Resize ${column.field}`,
68
- tabIndex: 0,
540
+ "aria-hidden": "true",
541
+ tabIndex: -1,
69
542
  onPointerDown: (event) => {
70
543
  resize.startResize(entry, event);
71
544
  },
72
545
  onDoubleClick: () => {
73
546
  resize.sizeToContent(entry);
74
- },
75
- onKeyDown: (event) => {
76
- if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
77
- return;
78
- event.preventDefault();
79
- resize.nudge(
80
- entry,
81
- event.key === "ArrowLeft" ? -KEYBOARD_STEP : KEYBOARD_STEP
82
- );
83
547
  }
84
548
  }
85
549
  )
@@ -106,37 +570,154 @@ function GridHeader({
106
570
  ] });
107
571
  }
108
572
 
109
- // src/DataGrid/components/GridBody.tsx
110
- import { accessDotted } from "@gridkitjs/core";
111
- import "react";
573
+ // src/DataGrid/components/GridRow.tsx
574
+ import { accessDotted as accessDotted2 } from "@gridkitjs/core";
575
+ import { memo } from "react";
112
576
  import { jsx as jsx2 } from "react/jsx-runtime";
577
+ function GridRowComponent({
578
+ columns,
579
+ rowId,
580
+ row,
581
+ rowIndex,
582
+ activeColumnId,
583
+ selectedColumnIds,
584
+ selected,
585
+ selectedColumnId,
586
+ focusedColumnIndex,
587
+ rowsSelectable,
588
+ cellsSelectable
589
+ }) {
590
+ return /* @__PURE__ */ jsx2(
591
+ "tr",
592
+ {
593
+ role: "row",
594
+ "aria-rowindex": rowIndex + 2,
595
+ ...rowsSelectable && { "aria-selected": selected },
596
+ className: ["grid-row", selected ? "is-selected" : ""].filter(Boolean).join(" "),
597
+ children: columns.map(({ id, column, alignment }, columnIndex) => {
598
+ const value = accessDotted2(row, column.field);
599
+ const cellSelected = id === selectedColumnId;
600
+ return /* @__PURE__ */ jsx2(
601
+ "td",
602
+ {
603
+ role: "gridcell",
604
+ "data-gridkit-column": id,
605
+ "aria-colindex": columnIndex + 1,
606
+ tabIndex: columnIndex === focusedColumnIndex ? 0 : -1,
607
+ ...cellsSelectable && { "aria-selected": cellSelected },
608
+ className: [
609
+ "grid-cell",
610
+ id === activeColumnId ? "is-resizing" : "",
611
+ cellSelected || selectedColumnIds.has(id) ? "is-selected" : "",
612
+ column.wrap?.cells ? "is-wrapped" : "",
613
+ column.cellClassName ?? ""
614
+ ].filter(Boolean).join(" "),
615
+ style: { textAlign: alignment },
616
+ children: column.cellTemplate ? column.cellTemplate({ value, row, rowIndex, rowId, selected }) : value
617
+ },
618
+ id
619
+ );
620
+ })
621
+ }
622
+ );
623
+ }
624
+ var GridRow = memo(GridRowComponent);
625
+ var GridRow_default = GridRow;
626
+
627
+ // src/DataGrid/components/GridBody.tsx
628
+ import { jsx as jsx3 } from "react/jsx-runtime";
629
+ function cellFrom(target) {
630
+ if (!(target instanceof Element)) {
631
+ return null;
632
+ }
633
+ const cell = target.closest("td[data-gridkit-column]");
634
+ const row = cell?.parentElement;
635
+ const columnId = cell?.getAttribute("data-gridkit-column");
636
+ if (!(cell instanceof HTMLTableCellElement) || !(row instanceof HTMLTableRowElement) || columnId === null || columnId === void 0) {
637
+ return null;
638
+ }
639
+ return {
640
+ rowIndex: row.sectionRowIndex,
641
+ columnIndex: cell.cellIndex,
642
+ columnId
643
+ };
644
+ }
113
645
  function GridBody({
114
646
  columns,
115
- dataSource,
116
- activeColumnId
647
+ rows,
648
+ activeColumnId,
649
+ nav,
650
+ selection
117
651
  }) {
118
- return /* @__PURE__ */ jsx2("tbody", { className: "grid-body", children: dataSource?.map((row, index) => /* @__PURE__ */ jsx2("tr", { className: "grid-row", children: columns.map(({ id, column, alignment }) => {
119
- const value = accessDotted(row, column.field);
120
- return /* @__PURE__ */ jsx2(
121
- "td",
122
- {
123
- "data-gridkit-column": id,
124
- className: [
125
- "grid-cell",
126
- id === activeColumnId ? "is-resizing" : "",
127
- column.wrap?.cells ? "is-wrapped" : "",
128
- column.cellClassName ?? ""
129
- ].filter(Boolean).join(" "),
130
- style: { textAlign: alignment },
131
- children: column.cellTemplate ? column.cellTemplate({ value, row, rowIndex: index }) : value
652
+ const { selectedCell, rowMode, cellMode } = selection;
653
+ function targetOf(target) {
654
+ const position = cellFrom(target);
655
+ const entry = position === null ? void 0 : rows[position.rowIndex];
656
+ return position === null || entry === void 0 ? null : { position, rowId: entry.rowId };
657
+ }
658
+ function select(rowId, columnId, modifiers) {
659
+ const intent = intentOf(modifiers);
660
+ if (rowMode !== false) {
661
+ selection.selectRow(rowId, intent);
662
+ }
663
+ if (cellMode !== false) {
664
+ selection.selectCell({ rowId, columnId }, intent);
665
+ }
666
+ }
667
+ return /* @__PURE__ */ jsx3(
668
+ "tbody",
669
+ {
670
+ className: "grid-body",
671
+ onFocus: (event) => {
672
+ const found = targetOf(event.target);
673
+ if (found === null) return;
674
+ nav.focusCell(found.position.rowIndex, found.position.columnIndex);
132
675
  },
133
- id
134
- );
135
- }) }, index)) });
676
+ onClick: (event) => {
677
+ const found = targetOf(event.target);
678
+ if (found === null) return;
679
+ select(found.rowId, found.position.columnId, event);
680
+ },
681
+ onKeyDown: (event) => {
682
+ if (event.key === " " || event.key === "Enter") {
683
+ const found = targetOf(event.target);
684
+ if (found === null) return;
685
+ event.preventDefault();
686
+ select(found.rowId, found.position.columnId, {
687
+ ctrlKey: event.key === " " || event.ctrlKey,
688
+ metaKey: event.metaKey,
689
+ shiftKey: event.shiftKey
690
+ });
691
+ return;
692
+ }
693
+ nav.onKeyDown(event);
694
+ },
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
+ ))
712
+ }
713
+ );
136
714
  }
137
715
 
138
716
  // src/DataGrid/useColumnDrag.ts
139
- import { useState } from "react";
717
+ import {
718
+ useRef as useRef3,
719
+ useState as useState2
720
+ } from "react";
140
721
  import {
141
722
  movesColumn,
142
723
  resolveDropBefore
@@ -154,9 +735,15 @@ function useColumnDrag({
154
735
  order,
155
736
  onDrop
156
737
  }) {
157
- const [draggedColumnId, setDraggedColumnId] = useState(null);
158
- const [dropTarget, setDropTarget] = useState(null);
159
- const [ghostTransform, setGhostTransform] = useState(null);
738
+ const [draggedColumnId, setDraggedColumnId] = useState2(null);
739
+ const [dropTarget, setDropTarget] = useState2(null);
740
+ const [ghostTransform, setGhostTransform] = useState2(null);
741
+ const dragged = useRef3(false);
742
+ function justDragged() {
743
+ const answer = dragged.current;
744
+ dragged.current = false;
745
+ return answer;
746
+ }
160
747
  function startDrag(entry, event) {
161
748
  const header = event.currentTarget;
162
749
  const { pointerId, clientX: startX } = event;
@@ -174,6 +761,7 @@ function useColumnDrag({
174
761
  return;
175
762
  }
176
763
  dragging = true;
764
+ dragged.current = true;
177
765
  setDraggedColumnId(entry.id);
178
766
  }
179
767
  placeGhost(clientX, clientY);
@@ -235,13 +823,14 @@ function useColumnDrag({
235
823
  dropTarget,
236
824
  ghostTransform,
237
825
  startDrag,
238
- moveByKeyboard
826
+ moveByKeyboard,
827
+ justDragged
239
828
  };
240
829
  }
241
830
 
242
831
  // src/DataGrid/useColumnResize.ts
243
832
  import {
244
- useState as useState2
833
+ useState as useState3
245
834
  } from "react";
246
835
  import {
247
836
  applyColumnResize,
@@ -254,7 +843,7 @@ import {
254
843
  // src/DataGrid/measureColumnContent.ts
255
844
  function measureColumnContent(table, columnId) {
256
845
  const cells = table.querySelectorAll(
257
- `[data-gridkit-column="${CSS.escape(columnId)}"]`
846
+ `td[data-gridkit-column="${CSS.escape(columnId)}"]`
258
847
  );
259
848
  const range = document.createRange();
260
849
  let widest = null;
@@ -277,7 +866,7 @@ function useColumnResize({
277
866
  columnSizeDefaults,
278
867
  onColumnResize
279
868
  }) {
280
- const [activeColumnId, setActiveColumnId] = useState2(null);
869
+ const [activeColumnId, setActiveColumnId] = useState3(null);
281
870
  function commit(base, columnId, width, phase) {
282
871
  const next = { ...base, [columnId]: width };
283
872
  setSizing(next);
@@ -314,16 +903,28 @@ function useColumnResize({
314
903
  }
315
904
  function end() {
316
905
  stop();
317
- commit(base, session.columnId, width, "end");
906
+ if (width !== session.startWidth) {
907
+ commit(base, session.columnId, width, "end");
908
+ }
318
909
  }
319
910
  function cancel() {
320
911
  stop();
321
- setSizing(base);
322
- onColumnResize?.({
323
- columnId: session.columnId,
324
- width: session.startWidth,
325
- sizing: base,
326
- phase: "end"
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];
920
+ }
921
+ onColumnResize?.({
922
+ columnId: session.columnId,
923
+ width: session.startWidth,
924
+ sizing: next,
925
+ phase: "end"
926
+ });
927
+ return next;
327
928
  });
328
929
  }
329
930
  function onKeyDown(keyEvent) {
@@ -369,10 +970,37 @@ function useColumnResize({
369
970
  return { activeColumnId, startResize, sizeToContent, nudge };
370
971
  }
371
972
 
973
+ // src/DataGrid/useColumnSort.ts
974
+ import {
975
+ sortDirectionFor,
976
+ sortPriorityFor,
977
+ toggleColumnSort
978
+ } from "@gridkitjs/core";
979
+ function useColumnSort({
980
+ sort,
981
+ setSort,
982
+ onColumnSortChange
983
+ }) {
984
+ function toggle(entry, event) {
985
+ 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 });
991
+ }
992
+ return {
993
+ sort,
994
+ directionFor: (columnId) => sortDirectionFor(sort, columnId),
995
+ priorityFor: (columnId) => sortPriorityFor(sort, columnId),
996
+ toggle
997
+ };
998
+ }
999
+
372
1000
  // src/DataGrid/useElementWidth.ts
373
- import { useLayoutEffect, useState as useState3 } from "react";
1001
+ import { useLayoutEffect, useState as useState4 } from "react";
374
1002
  function useElementWidth(ref, enabled) {
375
- const [width, setWidth] = useState3(null);
1003
+ const [width, setWidth] = useState4(null);
376
1004
  useLayoutEffect(() => {
377
1005
  const element = ref.current;
378
1006
  if (element === null || !enabled) {
@@ -391,12 +1019,17 @@ function useElementWidth(ref, enabled) {
391
1019
  }
392
1020
 
393
1021
  // src/DataGrid/DataGrid.tsx
394
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1022
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
395
1023
  function DataGridComponent({
396
1024
  dataSource,
397
1025
  columns,
1026
+ getRowId,
398
1027
  borders,
399
1028
  hoverable,
1029
+ selectable,
1030
+ defaultRowSelection,
1031
+ defaultColumnSelection,
1032
+ defaultCellSelection,
400
1033
  resizableColumns = false,
401
1034
  reorderableColumns = false,
402
1035
  resizeMode = "fit",
@@ -404,31 +1037,58 @@ function DataGridComponent({
404
1037
  defaultColumnOrder,
405
1038
  columnSizeDefaults,
406
1039
  onColumnResize,
407
- onColumnOrderChange
1040
+ onColumnOrderChange,
1041
+ sortableColumns = false,
1042
+ defaultColumnSort,
1043
+ onColumnSortChange,
1044
+ defaultFilter,
1045
+ label,
1046
+ labelledBy,
1047
+ ...callbacks
408
1048
  }) {
409
1049
  const hoverRows = hoverable?.rows ?? true;
410
1050
  const hoverColumns = hoverable?.columns ?? true;
411
1051
  const hoverCells = hoverable?.cells ?? true;
412
- const viewportRef = useRef(null);
413
- const tableRef = useRef(null);
1052
+ const viewportRef = useRef4(null);
1053
+ const tableRef = useRef4(null);
414
1054
  const viewportWidth = useElementWidth(viewportRef, resizeMode === "fit");
415
- const [sizing, setSizing] = useState4(
1055
+ const [sizing, setSizing] = useState5(
416
1056
  defaultColumnSizing ?? {}
417
1057
  );
418
- const [order, setOrder] = useState4(
1058
+ const [order, setOrder] = useState5(
419
1059
  defaultColumnOrder ?? []
420
1060
  );
421
- const definedColumns = useMemo(() => {
1061
+ const [sort, setSort] = useState5(defaultColumnSort ?? []);
1062
+ const [filter] = useState5(defaultFilter ?? []);
1063
+ const [announcement, setAnnouncement] = useState5("");
1064
+ const [rowSelection, setRowSelection] = useState5(
1065
+ defaultRowSelection ?? []
1066
+ );
1067
+ const [columnSelection, setColumnSelection] = useState5(
1068
+ defaultColumnSelection ?? []
1069
+ );
1070
+ const [cellSelection, setCellSelection] = useState5(
1071
+ defaultCellSelection ?? null
1072
+ );
1073
+ const rows = useMemo2(
1074
+ () => dataSource?.map((row, rowIndex) => ({
1075
+ rowId: resolveRowId(row, rowIndex, getRowId),
1076
+ row,
1077
+ rowIndex
1078
+ })) ?? [],
1079
+ [dataSource, getRowId]
1080
+ );
1081
+ const definedColumns = useMemo2(() => {
422
1082
  if (columns && columns.length !== 0) return columns;
423
1083
  if (dataSource && dataSource.length > 0)
424
1084
  return defineColumnsFromRows(dataSource);
425
1085
  return [];
426
1086
  }, [columns, dataSource]);
427
- const orderedColumns = useMemo(
1087
+ const orderedColumns = useMemo2(
428
1088
  () => applyColumnOrder(definedColumns, order),
429
1089
  [definedColumns, order]
430
1090
  );
431
- const resolved = useMemo(() => {
1091
+ const resolved = useMemo2(() => {
432
1092
  const widths = resolveColumnWidths(orderedColumns, sizing, {
433
1093
  sizes: columnSizeDefaults,
434
1094
  resizable: resizableColumns,
@@ -444,14 +1104,41 @@ function DataGridComponent({
444
1104
  resizableColumns,
445
1105
  reorderableColumns
446
1106
  ]);
1107
+ const shownRows = useMemo2(
1108
+ () => resolveShownRows(rows, filter, sort, resolved),
1109
+ [rows, filter, sort, resolved]
1110
+ );
1111
+ function columnName(columnId) {
1112
+ const entry = resolved.find((candidate) => candidate.id === columnId);
1113
+ if (entry === void 0) {
1114
+ return columnId;
1115
+ }
1116
+ return typeof entry.label === "string" ? entry.label : entry.column.field;
1117
+ }
1118
+ function announce(message) {
1119
+ setAnnouncement(message);
1120
+ }
1121
+ function handleColumnResize(event) {
1122
+ onColumnResize?.(event);
1123
+ if (event.phase === "end") {
1124
+ announce(
1125
+ `${columnName(event.columnId)}, ${String(Math.round(event.width))} pixels wide`
1126
+ );
1127
+ }
1128
+ }
447
1129
  const resize = useColumnResize({
448
1130
  tableRef,
449
1131
  sizing,
450
1132
  setSizing,
451
1133
  columnSizeDefaults,
452
- onColumnResize
1134
+ onColumnResize: handleColumnResize
1135
+ });
1136
+ const nav = useGridNavigation({
1137
+ tableRef,
1138
+ rowCount: shownRows.length,
1139
+ columnCount: resolved.length
453
1140
  });
454
- const displayedIds = useMemo(
1141
+ const displayedIds = useMemo2(
455
1142
  () => orderedColumns.map((column) => column.id ?? column.field),
456
1143
  [orderedColumns]
457
1144
  );
@@ -460,36 +1147,115 @@ function DataGridComponent({
460
1147
  if (next === displayedIds) {
461
1148
  return;
462
1149
  }
1150
+ const position = next.indexOf(movedId);
463
1151
  setOrder(next);
464
1152
  onColumnOrderChange?.({ columnId: movedId, order: next });
1153
+ announce(
1154
+ `${columnName(movedId)}, column ${String(position + 1)} of ${String(next.length)}`
1155
+ );
1156
+ nav.focusCell(HEADER_ROW, position);
465
1157
  }
466
1158
  const drag = useColumnDrag({ order: displayedIds, onDrop: handleDrop });
467
- return /* @__PURE__ */ jsx3("div", { className: "gridkit-data-grid-viewport", ref: viewportRef, children: /* @__PURE__ */ jsxs2(
468
- "table",
469
- {
470
- ref: tableRef,
471
- style: { width: totalColumnWidth(resolved) },
472
- className: [
473
- "gridkit-data-grid",
474
- borders ? `borders-${borders}` : "",
475
- hoverRows ? "" : "no-hover-rows",
476
- hoverColumns ? "" : "no-hover-columns",
477
- hoverCells ? "" : "no-hover-cells"
478
- ].filter(Boolean).join(" "),
479
- children: [
480
- /* @__PURE__ */ jsx3("colgroup", { children: resolved.map((entry) => /* @__PURE__ */ jsx3("col", { style: { width: entry.width } }, entry.id)) }),
481
- /* @__PURE__ */ jsx3(GridHeader, { columns: resolved, resize, drag }),
482
- /* @__PURE__ */ jsx3(
483
- GridBody,
484
- {
485
- columns: resolved,
486
- dataSource,
487
- activeColumnId: resize.activeColumnId
488
- }
489
- )
490
- ]
1159
+ function handleColumnSortChange(event) {
1160
+ onColumnSortChange?.(event);
1161
+ const entry = event.sort.find(
1162
+ (candidate) => candidate.columnId === event.columnId
1163
+ );
1164
+ if (entry === void 0) {
1165
+ announce(`${columnName(event.columnId)}, sort cleared`);
1166
+ return;
491
1167
  }
492
- ) });
1168
+ const direction = entry.direction === "asc" ? "ascending" : "descending";
1169
+ announce(
1170
+ 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}`
1171
+ );
1172
+ }
1173
+ const columnSort = useColumnSort({
1174
+ sort,
1175
+ setSort,
1176
+ onColumnSortChange: handleColumnSortChange
1177
+ });
1178
+ const selection = useGridSelection({
1179
+ rows: shownRows,
1180
+ columns: resolved,
1181
+ selectable,
1182
+ rowSelection,
1183
+ setRowSelection,
1184
+ columnSelection,
1185
+ setColumnSelection,
1186
+ cellSelection,
1187
+ setCellSelection,
1188
+ callbacks,
1189
+ announce
1190
+ });
1191
+ const multiselectable = selection.rowMode === "multiple" || selection.columnMode === "multiple";
1192
+ return /* @__PURE__ */ jsxs2("div", { className: "gridkit-data-grid-viewport", ref: viewportRef, children: [
1193
+ /* @__PURE__ */ jsxs2(
1194
+ "table",
1195
+ {
1196
+ ref: tableRef,
1197
+ role: "grid",
1198
+ "aria-rowcount": shownRows.length + 1,
1199
+ "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 },
1203
+ onKeyDown: (event) => {
1204
+ if (event.key === "Escape") {
1205
+ if (drag.draggedColumnId !== null || resize.activeColumnId !== null)
1206
+ return;
1207
+ selection.clear();
1208
+ return;
1209
+ }
1210
+ if ((event.ctrlKey || event.metaKey) && (event.key === "a" || event.key === "A")) {
1211
+ if (selection.rowMode !== "multiple") return;
1212
+ event.preventDefault();
1213
+ selection.selectAllRows();
1214
+ }
1215
+ },
1216
+ style: { width: totalColumnWidth(resolved) },
1217
+ className: [
1218
+ "gridkit-data-grid",
1219
+ borders ? `borders-${borders}` : "",
1220
+ // Hover is on by default and selection off, so one set of classes
1221
+ // turns styling off and the other turns it on. The polarity differs
1222
+ // because the defaults do.
1223
+ hoverRows ? "" : "no-hover-rows",
1224
+ hoverColumns ? "" : "no-hover-columns",
1225
+ hoverCells ? "" : "no-hover-cells",
1226
+ selection.rowMode === false ? "" : "selectable-rows",
1227
+ selection.columnMode === false ? "" : "selectable-columns",
1228
+ selection.cellMode === false ? "" : "selectable-cells"
1229
+ ].filter(Boolean).join(" "),
1230
+ children: [
1231
+ /* @__PURE__ */ jsx4("colgroup", { children: resolved.map((entry) => /* @__PURE__ */ jsx4("col", { style: { width: entry.width } }, entry.id)) }),
1232
+ /* @__PURE__ */ jsx4(
1233
+ GridHeader,
1234
+ {
1235
+ columns: resolved,
1236
+ resize,
1237
+ drag,
1238
+ sort: columnSort,
1239
+ sortableColumns,
1240
+ nav,
1241
+ selection
1242
+ }
1243
+ ),
1244
+ /* @__PURE__ */ jsx4(
1245
+ GridBody,
1246
+ {
1247
+ columns: resolved,
1248
+ rows: shownRows,
1249
+ activeColumnId: resize.activeColumnId,
1250
+ nav,
1251
+ selection
1252
+ }
1253
+ )
1254
+ ]
1255
+ }
1256
+ ),
1257
+ /* @__PURE__ */ jsx4("div", { className: "gridkit-sr-only", role: "status", "aria-live": "polite", children: announcement })
1258
+ ] });
493
1259
  }
494
1260
  export {
495
1261
  DataGridComponent