@gridkitjs/react 0.3.0 → 0.4.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,442 @@
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,
9
10
  totalColumnWidth
10
11
  } from "@gridkitjs/core";
11
12
 
12
13
  // src/DataGrid/components/GridHeader.tsx
13
14
  import { createPortal } from "react-dom";
14
15
  import { KEYBOARD_STEP } from "@gridkitjs/core";
16
+
17
+ // src/DataGrid/useGridNavigation.ts
18
+ import {
19
+ useEffect,
20
+ useRef,
21
+ useState
22
+ } from "react";
23
+ var HEADER_ROW = -1;
24
+ 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
+ };
33
+ }
34
+ function cellAt(table, focus) {
35
+ const row = focus.rowIndex === HEADER_ROW ? table.tHead?.rows[0] ?? null : table.tBodies[0]?.rows[focus.rowIndex] ?? null;
36
+ return row?.cells[focus.columnIndex] ?? null;
37
+ }
38
+ function useGridNavigation({
39
+ tableRef,
40
+ rowCount,
41
+ columnCount
42
+ }) {
43
+ const [stored, setStored] = useState({
44
+ rowIndex: HEADER_ROW,
45
+ columnIndex: 0
46
+ });
47
+ const focus = clampFocus(stored, rowCount, columnCount);
48
+ const pending = useRef(false);
49
+ useEffect(() => {
50
+ if (!pending.current) {
51
+ return;
52
+ }
53
+ pending.current = false;
54
+ const table = tableRef.current;
55
+ if (table === null) {
56
+ return;
57
+ }
58
+ cellAt(table, { rowIndex: focus.rowIndex, columnIndex: focus.columnIndex })?.focus();
59
+ }, [tableRef, focus.rowIndex, focus.columnIndex]);
60
+ function pageSize() {
61
+ const table = tableRef.current;
62
+ const firstRow = table?.tBodies[0]?.rows[0];
63
+ const viewport = table?.parentElement;
64
+ if (firstRow === void 0 || viewport === null || viewport === void 0 || firstRow.offsetHeight === 0) {
65
+ return FALLBACK_PAGE;
66
+ }
67
+ return Math.max(
68
+ 1,
69
+ Math.floor(viewport.clientHeight / firstRow.offsetHeight)
70
+ );
71
+ }
72
+ function focusCell(rowIndex, columnIndex) {
73
+ const next = clampFocus({ rowIndex, columnIndex }, rowCount, columnCount);
74
+ if (next.rowIndex === focus.rowIndex && next.columnIndex === focus.columnIndex) {
75
+ return;
76
+ }
77
+ pending.current = true;
78
+ setStored(next);
79
+ }
80
+ function tabIndexFor(rowIndex, columnIndex) {
81
+ return rowIndex === focus.rowIndex && columnIndex === focus.columnIndex ? 0 : -1;
82
+ }
83
+ function onKeyDown(event) {
84
+ if (event.altKey) {
85
+ return;
86
+ }
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
+ event.preventDefault();
122
+ focusCell(next.rowIndex, next.columnIndex);
123
+ }
124
+ return { focus, tabIndexFor, focusCell, onKeyDown };
125
+ }
126
+
127
+ // src/DataGrid/useGridSelection.ts
128
+ import { useMemo, useRef as useRef2 } from "react";
129
+ import {
130
+ accessDotted,
131
+ clearSelection,
132
+ diffSelection,
133
+ selectAll,
134
+ selectCell,
135
+ selectOnly,
136
+ selectRange,
137
+ toggleCellSelection,
138
+ toggleSelection
139
+ } from "@gridkitjs/core";
140
+ function intentOf(event) {
141
+ if (event.shiftKey) {
142
+ return "range";
143
+ }
144
+ return event.ctrlKey || event.metaKey ? "toggle" : "replace";
145
+ }
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
+ }
157
+ }
158
+ function countMessage(count, noun) {
159
+ if (count === 0) {
160
+ return `No ${noun}s selected`;
161
+ }
162
+ return count === 1 ? `1 ${noun} selected` : `${String(count)} ${noun}s selected`;
163
+ }
164
+ function useGridSelection({
165
+ rows,
166
+ columns,
167
+ selectable,
168
+ rowSelection,
169
+ setRowSelection,
170
+ columnSelection,
171
+ setColumnSelection,
172
+ cellSelection,
173
+ setCellSelection,
174
+ callbacks,
175
+ announce
176
+ }) {
177
+ const rowMode = selectable?.rows ?? false;
178
+ const columnMode = selectable?.columns ?? false;
179
+ 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
+ );
191
+ const rowAnchor = useRef2(null);
192
+ 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
+ function announceCount(changed, total, noun) {
226
+ if (changed > 1) {
227
+ announce(countMessage(total, noun));
228
+ }
229
+ }
230
+ 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
255
+ });
256
+ announceCount(added.length + removed.length, next.length, "row");
257
+ }
258
+ 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
279
+ });
280
+ }
281
+ callbacks.onColumnSelectionChange?.({
282
+ added: addedColumns,
283
+ removed: removedColumns,
284
+ selected: resolveColumns(next),
285
+ selection: next
286
+ });
287
+ announceCount(added.length + removed.length, next.length, "column");
288
+ }
289
+ 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
306
+ });
307
+ }
308
+ function selectRow(rowId, intent) {
309
+ const next = applyIntent(
310
+ rowSelection,
311
+ rowIds,
312
+ rowAnchor.current,
313
+ rowId,
314
+ intent,
315
+ rowMode
316
+ );
317
+ if (intent !== "range") {
318
+ rowAnchor.current = rowId;
319
+ }
320
+ commitRows(next);
321
+ }
322
+ function selectColumn(columnId, intent) {
323
+ const next = applyIntent(
324
+ columnSelection,
325
+ columnIds,
326
+ columnAnchor.current,
327
+ columnId,
328
+ intent,
329
+ columnMode
330
+ );
331
+ if (intent !== "range") {
332
+ columnAnchor.current = columnId;
333
+ }
334
+ commitColumns(next);
335
+ }
336
+ function selectCellAt(cell, intent) {
337
+ commitCell(
338
+ intent === "toggle" ? toggleCellSelection(cellSelection, cell, cellMode) : selectCell(cellSelection, cell, cellMode)
339
+ );
340
+ }
341
+ function selectAllRows() {
342
+ commitRows(selectAll(rowSelection, rowIds, rowMode));
343
+ }
344
+ function clear() {
345
+ commitRows(clearSelection(rowSelection));
346
+ commitColumns(clearSelection(columnSelection));
347
+ commitCell(null);
348
+ }
349
+ return {
350
+ rowMode,
351
+ columnMode,
352
+ cellMode,
353
+ selectedRowIds,
354
+ selectedColumnIds,
355
+ selectedCell: cellSelection,
356
+ selectRow,
357
+ selectColumn,
358
+ selectCell: selectCellAt,
359
+ selectAllRows,
360
+ clear
361
+ };
362
+ }
363
+
364
+ // src/DataGrid/components/GridHeader.tsx
15
365
  import { jsx, jsxs } from "react/jsx-runtime";
16
366
  function GridHeader({
17
367
  columns,
18
368
  resize,
19
- drag
369
+ drag,
370
+ nav,
371
+ selection
20
372
  }) {
21
373
  const { beforeId } = drag.dropTarget ?? {};
22
374
  const draggedEntry = columns.find((entry) => entry.id === drag.draggedColumnId) ?? null;
23
375
  return /* @__PURE__ */ jsxs("thead", { children: [
24
- /* @__PURE__ */ jsx("tr", { className: "grid-header", children: columns.map((entry, index) => {
376
+ /* @__PURE__ */ jsx("tr", { className: "grid-header", role: "row", "aria-rowindex": 1, children: columns.map((entry, index) => {
25
377
  const { column } = entry;
26
378
  const resizing = resize.activeColumnId === entry.id;
27
379
  const dragging = drag.draggedColumnId === entry.id;
28
380
  const dropBefore = beforeId !== void 0 && beforeId === entry.id;
29
381
  const dropAfter = beforeId === null && index === columns.length - 1;
382
+ 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(" ");
30
387
  return /* @__PURE__ */ jsxs(
31
388
  "th",
32
389
  {
390
+ scope: "col",
33
391
  "data-gridkit-column": entry.id,
392
+ "aria-colindex": index + 1,
393
+ tabIndex: nav.tabIndexFor(HEADER_ROW, index),
394
+ ...selection.columnMode !== false && {
395
+ "aria-selected": selected
396
+ },
397
+ onFocus: () => {
398
+ nav.focusCell(HEADER_ROW, index);
399
+ },
400
+ onClick: (event) => {
401
+ if (drag.justDragged()) {
402
+ return;
403
+ }
404
+ if (event.target instanceof Element && event.target.closest(".header-resize-handle") !== null) {
405
+ return;
406
+ }
407
+ selection.selectColumn(entry.id, intentOf(event));
408
+ },
409
+ onKeyDown: (event) => {
410
+ const horizontal = event.key === "ArrowLeft" || event.key === "ArrowRight";
411
+ const direction = event.key === "ArrowLeft" ? -1 : 1;
412
+ if (horizontal && event.ctrlKey && entry.reorderable) {
413
+ event.preventDefault();
414
+ drag.moveByKeyboard(entry, direction);
415
+ return;
416
+ }
417
+ if (horizontal && event.altKey && entry.resizable) {
418
+ event.preventDefault();
419
+ resize.nudge(entry, direction * KEYBOARD_STEP);
420
+ return;
421
+ }
422
+ if (event.key === " " || event.key === "Enter") {
423
+ event.preventDefault();
424
+ selection.selectColumn(
425
+ entry.id,
426
+ intentOf({
427
+ ctrlKey: event.key === " " || event.ctrlKey,
428
+ metaKey: event.metaKey,
429
+ shiftKey: event.shiftKey
430
+ })
431
+ );
432
+ return;
433
+ }
434
+ nav.onKeyDown(event);
435
+ },
34
436
  className: [
35
437
  "header-cell",
438
+ entry.reorderable ? "is-reorderable" : "",
439
+ selected ? "is-selected" : "",
36
440
  resizing ? "is-resizing" : "",
37
441
  dragging ? "is-dragging" : "",
38
442
  dropBefore ? "is-drop-before" : "",
@@ -40,46 +444,31 @@ function GridHeader({
40
444
  column.wrap?.header ? "is-wrapped" : "",
41
445
  column.headerClassName ?? ""
42
446
  ].filter(Boolean).join(" "),
447
+ ...shortcuts !== "" && { "aria-keyshortcuts": shortcuts },
43
448
  ...entry.reorderable && {
44
- tabIndex: 0,
45
449
  onPointerDown: (event) => {
46
450
  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
451
  }
58
452
  },
59
453
  children: [
60
454
  entry.label,
61
- entry.resizable && /* @__PURE__ */ jsx(
455
+ entry.resizable && /*
456
+ * A pointer affordance only, and hidden from assistive
457
+ * technology accordingly: a focusable handle per column would
458
+ * put N tab stops inside a widget that is meant to have one.
459
+ * The keyboard resizes from the header itself, under Alt.
460
+ */
461
+ /* @__PURE__ */ jsx(
62
462
  "span",
63
463
  {
64
464
  className: "header-resize-handle",
65
- role: "separator",
66
- "aria-orientation": "vertical",
67
- "aria-label": `Resize ${column.field}`,
68
- tabIndex: 0,
465
+ "aria-hidden": "true",
466
+ tabIndex: -1,
69
467
  onPointerDown: (event) => {
70
468
  resize.startResize(entry, event);
71
469
  },
72
470
  onDoubleClick: () => {
73
471
  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
472
  }
84
473
  }
85
474
  )
@@ -106,37 +495,154 @@ function GridHeader({
106
495
  ] });
107
496
  }
108
497
 
109
- // src/DataGrid/components/GridBody.tsx
110
- import { accessDotted } from "@gridkitjs/core";
111
- import "react";
498
+ // src/DataGrid/components/GridRow.tsx
499
+ import { accessDotted as accessDotted2 } from "@gridkitjs/core";
500
+ import { memo } from "react";
112
501
  import { jsx as jsx2 } from "react/jsx-runtime";
502
+ function GridRowComponent({
503
+ columns,
504
+ rowId,
505
+ row,
506
+ rowIndex,
507
+ activeColumnId,
508
+ selectedColumnIds,
509
+ selected,
510
+ selectedColumnId,
511
+ focusedColumnIndex,
512
+ rowsSelectable,
513
+ cellsSelectable
514
+ }) {
515
+ return /* @__PURE__ */ jsx2(
516
+ "tr",
517
+ {
518
+ role: "row",
519
+ "aria-rowindex": rowIndex + 2,
520
+ ...rowsSelectable && { "aria-selected": selected },
521
+ className: ["grid-row", selected ? "is-selected" : ""].filter(Boolean).join(" "),
522
+ children: columns.map(({ id, column, alignment }, columnIndex) => {
523
+ const value = accessDotted2(row, column.field);
524
+ const cellSelected = id === selectedColumnId;
525
+ return /* @__PURE__ */ jsx2(
526
+ "td",
527
+ {
528
+ role: "gridcell",
529
+ "data-gridkit-column": id,
530
+ "aria-colindex": columnIndex + 1,
531
+ tabIndex: columnIndex === focusedColumnIndex ? 0 : -1,
532
+ ...cellsSelectable && { "aria-selected": cellSelected },
533
+ className: [
534
+ "grid-cell",
535
+ id === activeColumnId ? "is-resizing" : "",
536
+ cellSelected || selectedColumnIds.has(id) ? "is-selected" : "",
537
+ column.wrap?.cells ? "is-wrapped" : "",
538
+ column.cellClassName ?? ""
539
+ ].filter(Boolean).join(" "),
540
+ style: { textAlign: alignment },
541
+ children: column.cellTemplate ? column.cellTemplate({ value, row, rowIndex, rowId, selected }) : value
542
+ },
543
+ id
544
+ );
545
+ })
546
+ }
547
+ );
548
+ }
549
+ var GridRow = memo(GridRowComponent);
550
+ var GridRow_default = GridRow;
551
+
552
+ // src/DataGrid/components/GridBody.tsx
553
+ import { jsx as jsx3 } from "react/jsx-runtime";
554
+ function cellFrom(target) {
555
+ if (!(target instanceof Element)) {
556
+ return null;
557
+ }
558
+ const cell = target.closest("td[data-gridkit-column]");
559
+ const row = cell?.parentElement;
560
+ const columnId = cell?.getAttribute("data-gridkit-column");
561
+ if (!(cell instanceof HTMLTableCellElement) || !(row instanceof HTMLTableRowElement) || columnId === null || columnId === void 0) {
562
+ return null;
563
+ }
564
+ return {
565
+ rowIndex: row.sectionRowIndex,
566
+ columnIndex: cell.cellIndex,
567
+ columnId
568
+ };
569
+ }
113
570
  function GridBody({
114
571
  columns,
115
- dataSource,
116
- activeColumnId
572
+ rows,
573
+ activeColumnId,
574
+ nav,
575
+ selection
117
576
  }) {
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
577
+ const { selectedCell, rowMode, cellMode } = selection;
578
+ function targetOf(target) {
579
+ const position = cellFrom(target);
580
+ const entry = position === null ? void 0 : rows[position.rowIndex];
581
+ return position === null || entry === void 0 ? null : { position, rowId: entry.rowId };
582
+ }
583
+ function select(rowId, columnId, modifiers) {
584
+ const intent = intentOf(modifiers);
585
+ if (rowMode !== false) {
586
+ selection.selectRow(rowId, intent);
587
+ }
588
+ if (cellMode !== false) {
589
+ selection.selectCell({ rowId, columnId }, intent);
590
+ }
591
+ }
592
+ return /* @__PURE__ */ jsx3(
593
+ "tbody",
594
+ {
595
+ className: "grid-body",
596
+ onFocus: (event) => {
597
+ const found = targetOf(event.target);
598
+ if (found === null) return;
599
+ nav.focusCell(found.position.rowIndex, found.position.columnIndex);
132
600
  },
133
- id
134
- );
135
- }) }, index)) });
601
+ onClick: (event) => {
602
+ const found = targetOf(event.target);
603
+ if (found === null) return;
604
+ select(found.rowId, found.position.columnId, event);
605
+ },
606
+ onKeyDown: (event) => {
607
+ if (event.key === " " || event.key === "Enter") {
608
+ const found = targetOf(event.target);
609
+ if (found === null) return;
610
+ event.preventDefault();
611
+ select(found.rowId, found.position.columnId, {
612
+ ctrlKey: event.key === " " || event.ctrlKey,
613
+ metaKey: event.metaKey,
614
+ shiftKey: event.shiftKey
615
+ });
616
+ return;
617
+ }
618
+ nav.onKeyDown(event);
619
+ },
620
+ children: rows.map(({ rowId, row, rowIndex }) => /* @__PURE__ */ jsx3(
621
+ GridRow_default,
622
+ {
623
+ columns,
624
+ rowId,
625
+ row,
626
+ rowIndex,
627
+ activeColumnId,
628
+ selectedColumnIds: selection.selectedColumnIds,
629
+ selected: selection.selectedRowIds.has(rowId),
630
+ selectedColumnId: selectedCell?.rowId === rowId ? selectedCell.columnId : null,
631
+ focusedColumnIndex: nav.focus.rowIndex === rowIndex ? nav.focus.columnIndex : null,
632
+ rowsSelectable: rowMode !== false,
633
+ cellsSelectable: cellMode !== false
634
+ },
635
+ rowId
636
+ ))
637
+ }
638
+ );
136
639
  }
137
640
 
138
641
  // src/DataGrid/useColumnDrag.ts
139
- import { useState } from "react";
642
+ import {
643
+ useRef as useRef3,
644
+ useState as useState2
645
+ } from "react";
140
646
  import {
141
647
  movesColumn,
142
648
  resolveDropBefore
@@ -154,9 +660,15 @@ function useColumnDrag({
154
660
  order,
155
661
  onDrop
156
662
  }) {
157
- const [draggedColumnId, setDraggedColumnId] = useState(null);
158
- const [dropTarget, setDropTarget] = useState(null);
159
- const [ghostTransform, setGhostTransform] = useState(null);
663
+ const [draggedColumnId, setDraggedColumnId] = useState2(null);
664
+ const [dropTarget, setDropTarget] = useState2(null);
665
+ const [ghostTransform, setGhostTransform] = useState2(null);
666
+ const dragged = useRef3(false);
667
+ function justDragged() {
668
+ const answer = dragged.current;
669
+ dragged.current = false;
670
+ return answer;
671
+ }
160
672
  function startDrag(entry, event) {
161
673
  const header = event.currentTarget;
162
674
  const { pointerId, clientX: startX } = event;
@@ -174,6 +686,7 @@ function useColumnDrag({
174
686
  return;
175
687
  }
176
688
  dragging = true;
689
+ dragged.current = true;
177
690
  setDraggedColumnId(entry.id);
178
691
  }
179
692
  placeGhost(clientX, clientY);
@@ -235,13 +748,14 @@ function useColumnDrag({
235
748
  dropTarget,
236
749
  ghostTransform,
237
750
  startDrag,
238
- moveByKeyboard
751
+ moveByKeyboard,
752
+ justDragged
239
753
  };
240
754
  }
241
755
 
242
756
  // src/DataGrid/useColumnResize.ts
243
757
  import {
244
- useState as useState2
758
+ useState as useState3
245
759
  } from "react";
246
760
  import {
247
761
  applyColumnResize,
@@ -277,7 +791,7 @@ function useColumnResize({
277
791
  columnSizeDefaults,
278
792
  onColumnResize
279
793
  }) {
280
- const [activeColumnId, setActiveColumnId] = useState2(null);
794
+ const [activeColumnId, setActiveColumnId] = useState3(null);
281
795
  function commit(base, columnId, width, phase) {
282
796
  const next = { ...base, [columnId]: width };
283
797
  setSizing(next);
@@ -370,9 +884,9 @@ function useColumnResize({
370
884
  }
371
885
 
372
886
  // src/DataGrid/useElementWidth.ts
373
- import { useLayoutEffect, useState as useState3 } from "react";
887
+ import { useLayoutEffect, useState as useState4 } from "react";
374
888
  function useElementWidth(ref, enabled) {
375
- const [width, setWidth] = useState3(null);
889
+ const [width, setWidth] = useState4(null);
376
890
  useLayoutEffect(() => {
377
891
  const element = ref.current;
378
892
  if (element === null || !enabled) {
@@ -391,12 +905,17 @@ function useElementWidth(ref, enabled) {
391
905
  }
392
906
 
393
907
  // src/DataGrid/DataGrid.tsx
394
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
908
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
395
909
  function DataGridComponent({
396
910
  dataSource,
397
911
  columns,
912
+ getRowId,
398
913
  borders,
399
914
  hoverable,
915
+ selectable,
916
+ defaultRowSelection,
917
+ defaultColumnSelection,
918
+ defaultCellSelection,
400
919
  resizableColumns = false,
401
920
  reorderableColumns = false,
402
921
  resizeMode = "fit",
@@ -404,31 +923,52 @@ function DataGridComponent({
404
923
  defaultColumnOrder,
405
924
  columnSizeDefaults,
406
925
  onColumnResize,
407
- onColumnOrderChange
926
+ onColumnOrderChange,
927
+ label,
928
+ labelledBy,
929
+ ...callbacks
408
930
  }) {
409
931
  const hoverRows = hoverable?.rows ?? true;
410
932
  const hoverColumns = hoverable?.columns ?? true;
411
933
  const hoverCells = hoverable?.cells ?? true;
412
- const viewportRef = useRef(null);
413
- const tableRef = useRef(null);
934
+ const viewportRef = useRef4(null);
935
+ const tableRef = useRef4(null);
414
936
  const viewportWidth = useElementWidth(viewportRef, resizeMode === "fit");
415
- const [sizing, setSizing] = useState4(
937
+ const [sizing, setSizing] = useState5(
416
938
  defaultColumnSizing ?? {}
417
939
  );
418
- const [order, setOrder] = useState4(
940
+ const [order, setOrder] = useState5(
419
941
  defaultColumnOrder ?? []
420
942
  );
421
- const definedColumns = useMemo(() => {
943
+ const [announcement, setAnnouncement] = useState5("");
944
+ const [rowSelection, setRowSelection] = useState5(
945
+ defaultRowSelection ?? []
946
+ );
947
+ const [columnSelection, setColumnSelection] = useState5(
948
+ defaultColumnSelection ?? []
949
+ );
950
+ const [cellSelection, setCellSelection] = useState5(
951
+ defaultCellSelection ?? null
952
+ );
953
+ const rows = useMemo2(
954
+ () => dataSource?.map((row, rowIndex) => ({
955
+ rowId: resolveRowId(row, rowIndex, getRowId),
956
+ row,
957
+ rowIndex
958
+ })) ?? [],
959
+ [dataSource, getRowId]
960
+ );
961
+ const definedColumns = useMemo2(() => {
422
962
  if (columns && columns.length !== 0) return columns;
423
963
  if (dataSource && dataSource.length > 0)
424
964
  return defineColumnsFromRows(dataSource);
425
965
  return [];
426
966
  }, [columns, dataSource]);
427
- const orderedColumns = useMemo(
967
+ const orderedColumns = useMemo2(
428
968
  () => applyColumnOrder(definedColumns, order),
429
969
  [definedColumns, order]
430
970
  );
431
- const resolved = useMemo(() => {
971
+ const resolved = useMemo2(() => {
432
972
  const widths = resolveColumnWidths(orderedColumns, sizing, {
433
973
  sizes: columnSizeDefaults,
434
974
  resizable: resizableColumns,
@@ -444,14 +984,37 @@ function DataGridComponent({
444
984
  resizableColumns,
445
985
  reorderableColumns
446
986
  ]);
987
+ function columnName(columnId) {
988
+ const entry = resolved.find((candidate) => candidate.id === columnId);
989
+ if (entry === void 0) {
990
+ return columnId;
991
+ }
992
+ return typeof entry.label === "string" ? entry.label : entry.column.field;
993
+ }
994
+ function announce(message) {
995
+ setAnnouncement(message);
996
+ }
997
+ function handleColumnResize(event) {
998
+ onColumnResize?.(event);
999
+ if (event.phase === "end") {
1000
+ announce(
1001
+ `${columnName(event.columnId)}, ${String(Math.round(event.width))} pixels wide`
1002
+ );
1003
+ }
1004
+ }
447
1005
  const resize = useColumnResize({
448
1006
  tableRef,
449
1007
  sizing,
450
1008
  setSizing,
451
1009
  columnSizeDefaults,
452
- onColumnResize
1010
+ onColumnResize: handleColumnResize
453
1011
  });
454
- const displayedIds = useMemo(
1012
+ const nav = useGridNavigation({
1013
+ tableRef,
1014
+ rowCount: rows.length,
1015
+ columnCount: resolved.length
1016
+ });
1017
+ const displayedIds = useMemo2(
455
1018
  () => orderedColumns.map((column) => column.id ?? column.field),
456
1019
  [orderedColumns]
457
1020
  );
@@ -460,36 +1023,94 @@ function DataGridComponent({
460
1023
  if (next === displayedIds) {
461
1024
  return;
462
1025
  }
1026
+ const position = next.indexOf(movedId);
463
1027
  setOrder(next);
464
1028
  onColumnOrderChange?.({ columnId: movedId, order: next });
1029
+ announce(
1030
+ `${columnName(movedId)}, column ${String(position + 1)} of ${String(next.length)}`
1031
+ );
1032
+ nav.focusCell(HEADER_ROW, position);
465
1033
  }
466
1034
  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
1035
+ const selection = useGridSelection({
1036
+ rows,
1037
+ columns: resolved,
1038
+ selectable,
1039
+ rowSelection,
1040
+ setRowSelection,
1041
+ columnSelection,
1042
+ setColumnSelection,
1043
+ cellSelection,
1044
+ setCellSelection,
1045
+ callbacks,
1046
+ announce
1047
+ });
1048
+ const multiselectable = selection.rowMode === "multiple" || selection.columnMode === "multiple";
1049
+ return /* @__PURE__ */ jsxs2("div", { className: "gridkit-data-grid-viewport", ref: viewportRef, children: [
1050
+ /* @__PURE__ */ jsxs2(
1051
+ "table",
1052
+ {
1053
+ ref: tableRef,
1054
+ role: "grid",
1055
+ "aria-rowcount": rows.length + 1,
1056
+ "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 },
1060
+ onKeyDown: (event) => {
1061
+ if (event.key === "Escape") {
1062
+ if (drag.draggedColumnId !== null || resize.activeColumnId !== null)
1063
+ return;
1064
+ selection.clear();
1065
+ return;
488
1066
  }
489
- )
490
- ]
491
- }
492
- ) });
1067
+ if ((event.ctrlKey || event.metaKey) && (event.key === "a" || event.key === "A")) {
1068
+ if (selection.rowMode !== "multiple") return;
1069
+ event.preventDefault();
1070
+ selection.selectAllRows();
1071
+ }
1072
+ },
1073
+ style: { width: totalColumnWidth(resolved) },
1074
+ className: [
1075
+ "gridkit-data-grid",
1076
+ borders ? `borders-${borders}` : "",
1077
+ // Hover is on by default and selection off, so one set of classes
1078
+ // turns styling off and the other turns it on. The polarity differs
1079
+ // because the defaults do.
1080
+ hoverRows ? "" : "no-hover-rows",
1081
+ hoverColumns ? "" : "no-hover-columns",
1082
+ hoverCells ? "" : "no-hover-cells",
1083
+ selection.rowMode === false ? "" : "selectable-rows",
1084
+ selection.columnMode === false ? "" : "selectable-columns",
1085
+ selection.cellMode === false ? "" : "selectable-cells"
1086
+ ].filter(Boolean).join(" "),
1087
+ children: [
1088
+ /* @__PURE__ */ jsx4("colgroup", { children: resolved.map((entry) => /* @__PURE__ */ jsx4("col", { style: { width: entry.width } }, entry.id)) }),
1089
+ /* @__PURE__ */ jsx4(
1090
+ GridHeader,
1091
+ {
1092
+ columns: resolved,
1093
+ resize,
1094
+ drag,
1095
+ nav,
1096
+ selection
1097
+ }
1098
+ ),
1099
+ /* @__PURE__ */ jsx4(
1100
+ GridBody,
1101
+ {
1102
+ columns: resolved,
1103
+ rows,
1104
+ activeColumnId: resize.activeColumnId,
1105
+ nav,
1106
+ selection
1107
+ }
1108
+ )
1109
+ ]
1110
+ }
1111
+ ),
1112
+ /* @__PURE__ */ jsx4("div", { className: "gridkit-sr-only", role: "status", "aria-live": "polite", children: announcement })
1113
+ ] });
493
1114
  }
494
1115
  export {
495
1116
  DataGridComponent