@bendyline/squisq-grid-react 2.11.19 → 2.11.21

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.
Files changed (3) hide show
  1. package/NOTICE.md +1 -1
  2. package/dist/index.js +87 -27
  3. package/package.json +4 -4
package/NOTICE.md CHANGED
@@ -8,7 +8,7 @@ Third-party components remain under their respective license terms.
8
8
 
9
9
  | Package | Version | License | Repository |
10
10
  | ----------------------- | -------------------- | ------- | ----------------------------------- |
11
- | @tanstack/react-virtual | 3.14.10 | MIT | https://github.com/TanStack/virtual |
11
+ | @tanstack/react-virtual | 3.14.13 | MIT | https://github.com/TanStack/virtual |
12
12
  | react _(peer)_ | ^18.0.0 \|\| ^19.0.0 | MIT | https://github.com/facebook/react |
13
13
  | react-dom _(peer)_ | ^18.0.0 \|\| ^19.0.0 | MIT | https://github.com/facebook/react |
14
14
 
package/dist/index.js CHANGED
@@ -844,6 +844,8 @@ function DataGrid({
844
844
  const rootRef = useRef(null);
845
845
  const opMenuRef = useRef(null);
846
846
  const valueMenuRef = useRef(null);
847
+ const menuTriggerRef = useRef(null);
848
+ const pendingCellFocus = useRef(false);
847
849
  const focusRef = useRef(null);
848
850
  const wasEditingRef = useRef(false);
849
851
  useEffect(() => {
@@ -895,6 +897,7 @@ function DataGrid({
895
897
  await restoreRef.current;
896
898
  const result = await provider.setView(next);
897
899
  cache.current = { version: cache.current.version + 1, pages: /* @__PURE__ */ new Map() };
900
+ selectionCells.current.clear();
898
901
  inFlight.current.clear();
899
902
  setViewRowCount(result.viewRowCount);
900
903
  setStaleView(false);
@@ -951,7 +954,9 @@ function DataGrid({
951
954
  if (rowId === void 0 || !cells) return null;
952
955
  return { rowId, cells };
953
956
  }, []);
957
+ const viewVersion = cache.current.version;
954
958
  useEffect(() => {
959
+ selectionCells.current.clear();
955
960
  if (!selection || !schema) return;
956
961
  const { r0, r1 } = normalizedRange(selection);
957
962
  if ((r1 - r0 + 1) * schema.columns.length > COPY_CELL_CAP) return;
@@ -964,11 +969,13 @@ function DataGrid({
964
969
  page.cells.forEach((cells, index) => rows.set(page.start + index, cells));
965
970
  }
966
971
  selectionCells.current = rows;
967
- })();
972
+ })().catch(() => {
973
+ if (!cancelled) selectionCells.current.clear();
974
+ });
968
975
  return () => {
969
976
  cancelled = true;
970
977
  };
971
- }, [provider, schema, selection]);
978
+ }, [provider, schema, selection, viewVersion, dirtyTick]);
972
979
  const handleCopy = useCallback(
973
980
  (event) => {
974
981
  if (!selection || !schema || editing) return;
@@ -981,7 +988,7 @@ function DataGrid({
981
988
  const lines = [];
982
989
  const htmlRows = [];
983
990
  for (let row = r0; row <= lastRow; row++) {
984
- const cells = selectionCells.current.get(row) ?? cellAt(row)?.cells;
991
+ const cells = cellAt(row)?.cells ?? selectionCells.current.get(row);
985
992
  const values = [];
986
993
  for (let col = c0; col <= c1; col++) values.push(cellDisplay(cells?.[col] ?? null));
987
994
  lines.push(values.join(" "));
@@ -1265,6 +1272,7 @@ function DataGrid({
1265
1272
  (col, columnName, next) => {
1266
1273
  setFilterOps((prev) => ({ ...prev, [col]: next }));
1267
1274
  setOpMenuCol(null);
1275
+ menuTriggerRef.current?.focus();
1268
1276
  const clause = viewRef.current.filter.find((entry) => entry.column === columnName);
1269
1277
  if (next.unary) {
1270
1278
  setColumnFilter(columnName, "", next.op, false, true);
@@ -1285,6 +1293,7 @@ function DataGrid({
1285
1293
  }));
1286
1294
  setOpMenuCol(null);
1287
1295
  setColumnFilter(columnName, "", prior.op, prior.caseSensitive);
1296
+ menuTriggerRef.current?.focus();
1288
1297
  },
1289
1298
  [opStateFor, schema, setColumnFilter]
1290
1299
  );
@@ -1296,6 +1305,7 @@ function DataGrid({
1296
1305
  }, []);
1297
1306
  const toggleOpMenu = useCallback(
1298
1307
  (col, anchor) => {
1308
+ menuTriggerRef.current = anchor;
1299
1309
  setValueMenuCol(null);
1300
1310
  setOpMenuPos(menuAnchorPos(anchor));
1301
1311
  setOpMenuCol((open) => open === col ? null : col);
@@ -1304,6 +1314,7 @@ function DataGrid({
1304
1314
  );
1305
1315
  const openValueMenu = useCallback(
1306
1316
  (col, anchor) => {
1317
+ menuTriggerRef.current = anchor;
1307
1318
  setOpMenuCol(null);
1308
1319
  if (valueMenuCol === col) {
1309
1320
  setValueMenuCol(null);
@@ -1328,17 +1339,38 @@ function DataGrid({
1328
1339
  menu.style.top = `${Math.max(4, Math.min(parseFloat(menu.style.top) || 0, maxTop))}px`;
1329
1340
  }
1330
1341
  }, [opMenuCol, valueMenuCol, valueMenuData]);
1342
+ useLayoutEffect(() => {
1343
+ const menu = opMenuRef.current ?? valueMenuRef.current;
1344
+ if (!menu || menu.contains(document.activeElement)) return;
1345
+ (menu.querySelector('[aria-checked="true"]') ?? menu.querySelector('[role^="menuitem"]'))?.focus();
1346
+ }, [opMenuCol, valueMenuCol, valueMenuData]);
1347
+ const handleMenuKeyDown = (event) => {
1348
+ if (event.key === "Tab") {
1349
+ setOpMenuCol(null);
1350
+ setValueMenuCol(null);
1351
+ menuTriggerRef.current?.focus();
1352
+ return;
1353
+ }
1354
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
1355
+ event.preventDefault();
1356
+ const items = [...event.currentTarget.querySelectorAll('[role^="menuitem"]')];
1357
+ const current = items.indexOf(document.activeElement);
1358
+ const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : event.key === "ArrowUp" ? (current - 1 + items.length) % items.length : (current + 1) % items.length;
1359
+ items[next]?.focus();
1360
+ };
1331
1361
  const chooseFilterValue = useCallback(
1332
1362
  (col, columnName, value) => {
1333
1363
  setFilterOps((prev) => ({ ...prev, [col]: { op: "=", caseSensitive: false } }));
1334
1364
  setValueMenuCol(null);
1335
1365
  setColumnFilter(columnName, value, "=", false);
1366
+ menuTriggerRef.current?.focus();
1336
1367
  },
1337
1368
  [setColumnFilter]
1338
1369
  );
1339
1370
  const moveFocus = useCallback(
1340
1371
  (rowDelta, colDelta, extend, absolute) => {
1341
- if (!schema) return;
1372
+ if (!schema || viewRowCount === 0 || schema.columns.length === 0) return;
1373
+ pendingCellFocus.current = true;
1342
1374
  setSelection((prev) => {
1343
1375
  const from = prev?.focus ?? { row: 0, col: 0 };
1344
1376
  const next = {
@@ -1354,10 +1386,16 @@ function DataGrid({
1354
1386
  },
1355
1387
  [schema, viewRowCount, virtualizer]
1356
1388
  );
1389
+ useLayoutEffect(() => {
1390
+ if (!pendingCellFocus.current || editing || !focusRef.current) return;
1391
+ focusRef.current.focus();
1392
+ pendingCellFocus.current = false;
1393
+ });
1357
1394
  const handleKeyDown = useCallback(
1358
1395
  (event) => {
1359
- if (!schema) return;
1396
+ if (!schema || event.defaultPrevented) return;
1360
1397
  const target = event.target;
1398
+ if (!target?.closest('[role="gridcell"]')) return;
1361
1399
  const inTextEntry = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target !== null && target.isContentEditable;
1362
1400
  if (inTextEntry && !editing) return;
1363
1401
  const meta = event.metaKey || event.ctrlKey;
@@ -1374,7 +1412,7 @@ function DataGrid({
1374
1412
  }
1375
1413
  return;
1376
1414
  }
1377
- const pageRows = Math.max(1, Math.floor(height / ROW_HEIGHT) - 1);
1415
+ const pageRows = Math.max(1, Math.floor(height / rowHeight) - 1);
1378
1416
  switch (event.key) {
1379
1417
  case "ArrowDown":
1380
1418
  moveFocus(
@@ -1454,6 +1492,7 @@ function DataGrid({
1454
1492
  commitEdit,
1455
1493
  editing,
1456
1494
  height,
1495
+ rowHeight,
1457
1496
  moveFocus,
1458
1497
  runJournal,
1459
1498
  schema,
@@ -1483,16 +1522,13 @@ function DataGrid({
1483
1522
  ).join(" ");
1484
1523
  }, [colWidths, defaultColumnWidths, schema]);
1485
1524
  const range = selection ? normalizedRange(selection) : null;
1525
+ const focusRowRendered = selection && virtualItems.some((item) => item.index === selection.focus.row);
1486
1526
  const filterValueFor = (name) => view.filter.find((clause) => clause.column === name)?.value ?? "";
1487
1527
  return /* @__PURE__ */ jsxs(
1488
1528
  "div",
1489
1529
  {
1490
1530
  ref: rootRef,
1491
1531
  className: `squisq-grid${className ? ` ${className}` : ""}`,
1492
- role: "grid",
1493
- "aria-rowcount": viewRowCount + 1,
1494
- "aria-colcount": schema?.columns.length ?? 0,
1495
- "aria-multiselectable": "true",
1496
1532
  onKeyDown: handleKeyDown,
1497
1533
  onCopy: handleCopy,
1498
1534
  onPaste: handlePaste,
@@ -1506,15 +1542,22 @@ function DataGrid({
1506
1542
  }
1507
1543
  },
1508
1544
  onKeyDownCapture: (event) => {
1509
- if (event.key !== "Escape") return;
1545
+ if (event.key !== "Escape" || opMenuCol === null && valueMenuCol === null) return;
1546
+ event.preventDefault();
1547
+ event.stopPropagation();
1510
1548
  if (opMenuCol !== null) setOpMenuCol(null);
1511
1549
  if (valueMenuCol !== null) setValueMenuCol(null);
1550
+ menuTriggerRef.current?.focus();
1512
1551
  },
1513
1552
  children: [
1514
1553
  /* @__PURE__ */ jsxs(
1515
1554
  "div",
1516
1555
  {
1517
1556
  className: "squisq-grid-scroller",
1557
+ role: "grid",
1558
+ "aria-rowcount": viewRowCount + 1,
1559
+ "aria-colcount": schema?.columns.length ?? 0,
1560
+ "aria-multiselectable": "true",
1518
1561
  ref: bodyRef,
1519
1562
  style: { height },
1520
1563
  onScroll: () => {
@@ -1561,6 +1604,7 @@ function DataGrid({
1561
1604
  className: `squisq-grid-opbutton${opState.caseSensitive ? " squisq-grid-opbutton--cs" : ""}`,
1562
1605
  "aria-label": `Filter operator for ${column.name}`,
1563
1606
  "aria-expanded": opMenuCol === col,
1607
+ "aria-haspopup": "menu",
1564
1608
  title: `${opChoicesFor(column.kind).find((c) => choiceMatches(c, opState))?.label ?? opState.op}${opState.caseSensitive ? " (case-sensitive)" : ""}`,
1565
1609
  onClick: (event) => toggleOpMenu(col, event.currentTarget),
1566
1610
  children: [
@@ -1592,6 +1636,7 @@ function DataGrid({
1592
1636
  className: "squisq-grid-valuebutton",
1593
1637
  "aria-label": `Filter ${column.name} by value`,
1594
1638
  "aria-expanded": valueMenuCol === col,
1639
+ "aria-haspopup": "menu",
1595
1640
  title: "Filter by value",
1596
1641
  onClick: (event) => openValueMenu(col, event.currentTarget),
1597
1642
  children: "\u25BE"
@@ -1659,8 +1704,14 @@ function DataGrid({
1659
1704
  ...locked && lockedReason ? { title: lockedReason } : formula !== void 0 ? { title: `=${formula}` } : {},
1660
1705
  ...locked ? { "aria-readonly": "true" } : {},
1661
1706
  className: classes,
1662
- tabIndex: isFocus ? 0 : -1,
1707
+ tabIndex: isFocus || !focusRowRendered && item.index === virtualItems[0]?.index && col === 0 ? 0 : -1,
1663
1708
  ref: isFocus ? focusRef : void 0,
1709
+ onFocus: () => {
1710
+ const pos = { row: item.index, col };
1711
+ setSelection(
1712
+ (prev) => prev?.focus.row === pos.row && prev.focus.col === pos.col ? prev : { anchor: pos, focus: pos }
1713
+ );
1714
+ },
1664
1715
  onMouseDown: (event) => {
1665
1716
  const pos = { row: item.index, col };
1666
1717
  setSelection(
@@ -1673,6 +1724,8 @@ function DataGrid({
1673
1724
  {
1674
1725
  className: `squisq-grid-editor${editing.error ? " squisq-grid-editor--error" : ""}`,
1675
1726
  autoFocus: true,
1727
+ "aria-label": `Edit ${column.name}, row ${item.index + 1}`,
1728
+ "aria-invalid": editing.error ? true : void 0,
1676
1729
  value: editing.draft,
1677
1730
  title: editing.error,
1678
1731
  onChange: (event) => setEditing({ ...editing, draft: event.target.value, error: void 0 }),
@@ -1702,6 +1755,8 @@ function DataGrid({
1702
1755
  ref: opMenuRef,
1703
1756
  className: "squisq-grid-opmenu",
1704
1757
  role: "menu",
1758
+ "aria-label": `Filter operator for ${column.name}`,
1759
+ onKeyDown: handleMenuKeyDown,
1705
1760
  style: { left: opMenuPos.left, top: opMenuPos.top },
1706
1761
  children: [
1707
1762
  filterActive && /* @__PURE__ */ jsx(
@@ -1733,21 +1788,24 @@ function DataGrid({
1733
1788
  },
1734
1789
  `${choice.op}${choice.unary ? "0" : ""}`
1735
1790
  )),
1736
- caseCapable && /* @__PURE__ */ jsxs("label", { className: "squisq-grid-opcase", children: [
1737
- /* @__PURE__ */ jsx(
1738
- "input",
1739
- {
1740
- type: "checkbox",
1741
- checked: opState.caseSensitive,
1742
- onChange: (event) => chooseFilterOp(col, column.name, {
1743
- op: opState.op,
1744
- caseSensitive: event.target.checked,
1745
- ...opState.unary ? { unary: true } : {}
1746
- })
1747
- }
1748
- ),
1749
- "Case sensitive"
1750
- ] })
1791
+ caseCapable && /* @__PURE__ */ jsxs(
1792
+ "button",
1793
+ {
1794
+ type: "button",
1795
+ role: "menuitemcheckbox",
1796
+ "aria-checked": opState.caseSensitive,
1797
+ className: "squisq-grid-opoption squisq-grid-opcase",
1798
+ onClick: () => chooseFilterOp(col, column.name, {
1799
+ op: opState.op,
1800
+ caseSensitive: !opState.caseSensitive,
1801
+ ...opState.unary ? { unary: true } : {}
1802
+ }),
1803
+ children: [
1804
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: opState.caseSensitive ? "\u2713" : "\u25A1" }),
1805
+ "Case sensitive"
1806
+ ]
1807
+ }
1808
+ )
1751
1809
  ]
1752
1810
  }
1753
1811
  );
@@ -1763,6 +1821,8 @@ function DataGrid({
1763
1821
  ref: valueMenuRef,
1764
1822
  className: "squisq-grid-valuemenu",
1765
1823
  role: "menu",
1824
+ "aria-label": `Filter ${column.name} by value`,
1825
+ onKeyDown: handleMenuKeyDown,
1766
1826
  style: { left: valueMenuPos.left, top: valueMenuPos.top },
1767
1827
  children: valueMenuData?.col === col ? /* @__PURE__ */ jsxs(Fragment, { children: [
1768
1828
  filterActive && /* @__PURE__ */ jsx(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-grid-react",
3
- "version": "2.11.19",
3
+ "version": "2.11.21",
4
4
  "description": "Virtualized data grid for squisq data sidecars: columnar worker store, TableQueryProvider, and a TanStack-Virtual React renderer",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -53,15 +53,15 @@
53
53
  "typecheck": "tsc --noEmit"
54
54
  },
55
55
  "dependencies": {
56
- "@bendyline/squisq": "2.11.18",
57
- "@tanstack/react-virtual": "3.14.10"
56
+ "@bendyline/squisq": "2.11.19",
57
+ "@tanstack/react-virtual": "3.14.13"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "react": "^18.0.0 || ^19.0.0",
61
61
  "react-dom": "^18.0.0 || ^19.0.0"
62
62
  },
63
63
  "devDependencies": {
64
- "@types/react": "18.3.28",
64
+ "@types/react": "18.3.31",
65
65
  "@types/react-dom": "18.3.7",
66
66
  "react": "18.3.1",
67
67
  "react-dom": "18.3.1",