@ornery/ui-grid-react 0.1.8 → 0.1.10

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.
@@ -56,6 +56,7 @@ import {
56
56
  buildGridFocusCellResult,
57
57
  findNextGridCell,
58
58
  isPrintableGridKey,
59
+ isGridNavigationKey,
59
60
  isGridCellPosition,
60
61
  exportCsvRows,
61
62
  buildGridRows,
@@ -222,6 +223,7 @@ export interface UseGridStateResult {
222
223
  paginationSelectedPageSize: number;
223
224
  rowSize: number;
224
225
  viewportHeightPx: string;
226
+ autoViewportHeight: number | null;
225
227
 
226
228
  // Display helpers
227
229
  headerLabel: (column: GridColumnDef) => string;
@@ -239,6 +241,7 @@ export interface UseGridStateResult {
239
241
  groupDisclosureLabel: (item: GroupItem) => string;
240
242
  displayValue: (row: GridRow, column: GridColumnDef) => string;
241
243
  isFocusedCell: (row: GridRow, column: GridColumnDef) => boolean;
244
+ isFocusedRow: (row: GridRow) => boolean;
242
245
  isEditingCell: (row: GridRow, column: GridColumnDef) => boolean;
243
246
  editorInputType: (column: GridColumnDef) => string;
244
247
  cellContext: (row: GridRow, column: GridColumnDef) => GridCellTemplateContext;
@@ -307,6 +310,9 @@ export interface UseGridStateResult {
307
310
  toggleTreeRow: (row: GridRow, event?: React.MouseEvent) => void;
308
311
  moveColumn: (fromIndex: number, toIndex: number) => void;
309
312
  moveVisibleColumn: (columnName: string, targetColumnName: string) => void;
313
+ canResizeColumns: () => boolean;
314
+ handleHeaderResizeMouseDown: (column: GridColumnDef, event: React.MouseEvent) => void;
315
+ autoSizeColumn: (column: GridColumnDef, event: React.MouseEvent) => void;
310
316
  nextPage: () => void;
311
317
  previousPage: () => void;
312
318
  onPageSizeChange: (value: string) => void;
@@ -344,6 +350,7 @@ export function useGridState(
344
350
  });
345
351
  const [autoViewportHeight, setAutoViewportHeight] = useState<number | null>(null);
346
352
  const [pinnedColumns, setPinnedColumns] = useState<PinnedColumnState>({});
353
+ const [columnWidthOverrides, setColumnWidthOverrides] = useState<Record<string, string>>({});
347
354
 
348
355
  const gridContainerRef = useRef<HTMLDivElement | null>(null);
349
356
  const initializedGridIdRef = useRef<string | null>(null);
@@ -384,6 +391,16 @@ export function useGridState(
384
391
  currentPageRef.current = currentPage;
385
392
  const pageSizeRef = useRef(pageSize);
386
393
  pageSizeRef.current = pageSize;
394
+
395
+ const setEditingCellState = useCallback((nextEditingCell: GridCellPosition | null): void => {
396
+ editingCellRef.current = nextEditingCell;
397
+ setEditingCell(nextEditingCell);
398
+ }, []);
399
+
400
+ const setEditingValueState = useCallback((nextEditingValue: string): void => {
401
+ editingValueRef.current = nextEditingValue;
402
+ setEditingValue(nextEditingValue);
403
+ }, []);
387
404
  const infiniteScrollStateRef = useRef(infiniteScrollState);
388
405
  infiniteScrollStateRef.current = infiniteScrollState;
389
406
  const optionsRef = useRef(options);
@@ -393,9 +410,15 @@ export function useGridState(
393
410
 
394
411
  const visibleColumns = useMemo(() => {
395
412
  const orderedColumns = orderVisibleColumns(options.columnDefs, columnOrder);
413
+ const applyWidthOverrides = (columns: GridColumnDef[]): GridColumnDef[] =>
414
+ columns.map((col) => {
415
+ const override = columnWidthOverrides[col.name];
416
+ return override == null ? col : { ...col, width: override };
417
+ });
418
+
396
419
  const pinnedEntries = Object.entries(pinnedColumns);
397
420
  if (pinnedEntries.length === 0) {
398
- return orderedColumns;
421
+ return applyWidthOverrides(orderedColumns);
399
422
  }
400
423
 
401
424
  const columnByName = new Map(orderedColumns.map((column) => [column.name, column]));
@@ -411,8 +434,8 @@ export function useGridState(
411
434
  (column) => pinnedColumns[column.name] === undefined,
412
435
  );
413
436
 
414
- return [...pinnedLeft, ...centerColumns, ...pinnedRight];
415
- }, [options.columnDefs, columnOrder, pinnedColumns]);
437
+ return applyWidthOverrides([...pinnedLeft, ...centerColumns, ...pinnedRight]);
438
+ }, [options.columnDefs, columnOrder, pinnedColumns, columnWidthOverrides]);
416
439
 
417
440
  const visibleColumnsRef = useRef(visibleColumns);
418
441
  visibleColumnsRef.current = visibleColumns;
@@ -578,12 +601,15 @@ export function useGridState(
578
601
  if (retry) requestAnimationFrame(() => doFocus(false));
579
602
  return;
580
603
  }
581
- target.focus();
604
+ target.focus({ preventScroll: true });
582
605
  if (retry && container.ownerDocument.activeElement !== target) {
583
606
  requestAnimationFrame(() => doFocus(false));
584
607
  }
585
608
  };
586
609
 
610
+ // Attempt synchronous focus first to avoid the browser scrolling the viewport
611
+ // (e.g. when ArrowDown is pressed) before async focus runs.
612
+ doFocus(true);
587
613
  queueMicrotask(() => doFocus(true));
588
614
  }, []);
589
615
 
@@ -944,8 +970,8 @@ export function useGridState(
944
970
  gridApiRef.current!,
945
971
  {
946
972
  setFocusedCell: (fc) => setFocusedCell(fc),
947
- setEditingCell: (ec2) => setEditingCell(ec2),
948
- setEditingValue: (ev) => setEditingValue(ev),
973
+ setEditingCell: setEditingCellState,
974
+ setEditingValue: setEditingValueState,
949
975
  },
950
976
  row,
951
977
  column,
@@ -958,7 +984,7 @@ export function useGridState(
958
984
  queueMicrotask(() => focusEditorInput(focusToken));
959
985
  }
960
986
  },
961
- [focusEditorInput],
987
+ [focusEditorInput, setEditingCellState, setEditingValueState],
962
988
  );
963
989
 
964
990
  const commitCellEditFn = useCallback(
@@ -966,8 +992,8 @@ export function useGridState(
966
992
  const result = commitGridCellEditCommand(gridApiRef.current!, {
967
993
  getEditingCell: () => editingCellRef.current,
968
994
  getEditingValue: () => editingValueRef.current,
969
- setEditingCell: (ec) => setEditingCell(ec),
970
- setEditingValue: (ev) => setEditingValue(ev),
995
+ setEditingCell: setEditingCellState,
996
+ setEditingValue: setEditingValueState,
971
997
  findRowById: (rowId) =>
972
998
  coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId),
973
999
  findColumnByName: (columnName) =>
@@ -991,15 +1017,15 @@ export function useGridState(
991
1017
  focusRenderedCell(result.focusTarget);
992
1018
  }
993
1019
  },
994
- [buildRowsFromData, focusRenderedCell],
1020
+ [buildRowsFromData, focusRenderedCell, setEditingCellState, setEditingValueState],
995
1021
  );
996
1022
 
997
1023
  const cancelCellEditFn = useCallback((): void => {
998
1024
  const hadEditingCell = editingCellRef.current !== null;
999
1025
  const result = cancelGridCellEditCommand(gridApiRef.current!, {
1000
1026
  getEditingCell: () => editingCellRef.current,
1001
- setEditingCell: (ec) => setEditingCell(ec),
1002
- setEditingValue: (ev) => setEditingValue(ev),
1027
+ setEditingCell: setEditingCellState,
1028
+ setEditingValue: setEditingValueState,
1003
1029
  findRowById: (rowId) =>
1004
1030
  coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId),
1005
1031
  findColumnByName: (columnName) =>
@@ -1009,7 +1035,7 @@ export function useGridState(
1009
1035
  if (!hadEditingCell) return;
1010
1036
  editorFocusTokenRef.current += 1;
1011
1037
  if (result.focusTarget) focusRenderedCell(result.focusTarget);
1012
- }, [buildRowsFromData, focusRenderedCell]);
1038
+ }, [buildRowsFromData, focusRenderedCell, setEditingCellState, setEditingValueState]);
1013
1039
 
1014
1040
  const moveFocusFn = useCallback(
1015
1041
  (
@@ -1019,7 +1045,9 @@ export function useGridState(
1019
1045
  triggerEvent?: Event | KeyboardEvent | null,
1020
1046
  ): boolean => {
1021
1047
  const nextCell = findNextGridCell({
1022
- rows: pipelineRef.current.visibleRows,
1048
+ rows: pipelineRef.current.displayItems
1049
+ .filter((item) => item.kind === 'row')
1050
+ .map((item) => (item as RowItem).row),
1023
1051
  columns: visibleColumnsRef.current,
1024
1052
  rowId: row.id,
1025
1053
  columnName: column.name,
@@ -1111,8 +1139,8 @@ export function useGridState(
1111
1139
  setHiddenRowReasons({});
1112
1140
  setCollapsedGroups({});
1113
1141
  setFocusedCell(null);
1114
- setEditingCell(null);
1115
- setEditingValue('');
1142
+ setEditingCellState(null);
1143
+ setEditingValueState('');
1116
1144
  setExpandedRows({});
1117
1145
  setExpandedTreeRows({});
1118
1146
  setColumnOrder(options.columnDefs.map((column) => column.name));
@@ -1155,8 +1183,11 @@ export function useGridState(
1155
1183
 
1156
1184
  // --- Auto resize effect ---
1157
1185
 
1186
+ // Auto-resize is on by default so the grid fills its container. Setting an
1187
+ // explicit `viewportHeight` opts back into fixed sizing because the observer
1188
+ // only writes `autoViewportHeight` when `viewportHeight` is unset.
1158
1189
  useEffect(() => {
1159
- if (!FEATURE_AUTO_RESIZE || !options.enableAutoResize) return;
1190
+ if (!FEATURE_AUTO_RESIZE) return;
1160
1191
 
1161
1192
  const container = gridContainerRef.current;
1162
1193
  if (!container) return;
@@ -1283,6 +1314,12 @@ export function useGridState(
1283
1314
  return isGridCellPosition(focusedCellRef.current, row.id, column.name);
1284
1315
  }, []);
1285
1316
 
1317
+ const isFocusedRowFn = useCallback((row: GridRow): boolean => {
1318
+ return (
1319
+ focusedCellRef.current?.rowId === row.id || editingCellRef.current?.rowId === row.id
1320
+ );
1321
+ }, []);
1322
+
1286
1323
  const isEditingCellFn = useCallback((row: GridRow, column: GridColumnDef): boolean => {
1287
1324
  return isGridCellPosition(editingCellRef.current, row.id, column.name);
1288
1325
  }, []);
@@ -1445,35 +1482,46 @@ export function useGridState(
1445
1482
 
1446
1483
  const handleCellKeyDownFn = useCallback(
1447
1484
  (row: GridRow, column: GridColumnDef, event: React.KeyboardEvent): void => {
1448
- focusCellFn(row, column, event.nativeEvent);
1485
+ if (isGridNavigationKey(event.key)) {
1486
+ setFocusedCell({ rowId: row.id, columnName: column.name });
1487
+ } else {
1488
+ focusCellFn(row, column, event.nativeEvent);
1489
+ }
1449
1490
 
1450
1491
  switch (event.key) {
1451
1492
  case 'ArrowLeft':
1452
1493
  event.preventDefault();
1494
+ event.stopPropagation();
1453
1495
  moveFocusFn(row, column, 'left', event.nativeEvent);
1454
1496
  return;
1455
1497
  case 'ArrowRight':
1456
1498
  event.preventDefault();
1499
+ event.stopPropagation();
1457
1500
  moveFocusFn(row, column, 'right', event.nativeEvent);
1458
1501
  return;
1459
1502
  case 'ArrowUp':
1460
1503
  event.preventDefault();
1504
+ event.stopPropagation();
1461
1505
  moveFocusFn(row, column, 'up', event.nativeEvent);
1462
1506
  return;
1463
1507
  case 'ArrowDown':
1464
1508
  event.preventDefault();
1509
+ event.stopPropagation();
1465
1510
  moveFocusFn(row, column, 'down', event.nativeEvent);
1466
1511
  return;
1467
1512
  case 'Tab':
1468
1513
  event.preventDefault();
1514
+ event.stopPropagation();
1469
1515
  moveFocusFn(row, column, event.shiftKey ? 'left' : 'right', event.nativeEvent);
1470
1516
  return;
1471
1517
  case 'Enter':
1472
1518
  event.preventDefault();
1519
+ event.stopPropagation();
1473
1520
  moveFocusFn(row, column, event.shiftKey ? 'up' : 'down', event.nativeEvent);
1474
1521
  return;
1475
1522
  case 'F2':
1476
1523
  event.preventDefault();
1524
+ event.stopPropagation();
1477
1525
  if (isCellEditable(row, column, event.nativeEvent)) {
1478
1526
  startCellEditFn(row, column, event.nativeEvent);
1479
1527
  }
@@ -1482,6 +1530,7 @@ export function useGridState(
1482
1530
  case 'Delete':
1483
1531
  if (isCellEditable(row, column, event.nativeEvent)) {
1484
1532
  event.preventDefault();
1533
+ event.stopPropagation();
1485
1534
  startCellEditFn(row, column, event.nativeEvent, '');
1486
1535
  }
1487
1536
  return;
@@ -1494,6 +1543,7 @@ export function useGridState(
1494
1543
  isCellEditable(row, column, event.nativeEvent)
1495
1544
  ) {
1496
1545
  event.preventDefault();
1546
+ event.stopPropagation();
1497
1547
  startCellEditFn(row, column, event.nativeEvent, event.key);
1498
1548
  }
1499
1549
  },
@@ -1511,24 +1561,33 @@ export function useGridState(
1511
1561
  );
1512
1562
 
1513
1563
  const updateEditingValueFn = useCallback((value: string): void => {
1514
- setEditingValue(value);
1515
- }, []);
1564
+ setEditingValueState(value);
1565
+ }, [setEditingValueState]);
1516
1566
 
1517
1567
  const handleEditorKeyDownFn = useCallback(
1518
1568
  (event: React.KeyboardEvent): void => {
1519
1569
  if (event.key === 'Escape') {
1520
1570
  event.preventDefault();
1571
+ event.stopPropagation();
1521
1572
  cancelCellEditFn();
1522
1573
  return;
1523
1574
  }
1524
1575
  if (event.key === 'Enter') {
1525
1576
  event.preventDefault();
1577
+ event.stopPropagation();
1526
1578
  commitCellEditFn(event.shiftKey ? 'up' : 'down');
1527
1579
  return;
1528
1580
  }
1529
1581
  if (event.key === 'Tab') {
1530
1582
  event.preventDefault();
1583
+ event.stopPropagation();
1531
1584
  commitCellEditFn(event.shiftKey ? 'left' : 'right');
1585
+ return;
1586
+ }
1587
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
1588
+ event.preventDefault();
1589
+ event.stopPropagation();
1590
+ commitCellEditFn(event.key === 'ArrowUp' ? 'up' : 'down');
1532
1591
  }
1533
1592
  },
1534
1593
  [cancelCellEditFn, commitCellEditFn],
@@ -1599,6 +1658,90 @@ export function useGridState(
1599
1658
  [setPaginationPageSizeFn],
1600
1659
  );
1601
1660
 
1661
+ // --- Column resizing ---
1662
+
1663
+ const canResizeColumnsFn = useCallback((): boolean => {
1664
+ return optionsRef.current.enableColumnResizing !== false;
1665
+ }, []);
1666
+
1667
+ const setColumnWidthOverrideFn = useCallback((columnName: string, widthPx: number): void => {
1668
+ const nextWidth = `${Math.max(88, Math.round(widthPx))}px`;
1669
+ setColumnWidthOverrides((current) => ({ ...current, [columnName]: nextWidth }));
1670
+ }, []);
1671
+
1672
+ const measureAutoColumnWidthFn = useCallback((columnName: string): number => {
1673
+ const container = gridContainerRef.current;
1674
+ if (container == null) return 176;
1675
+ const escaped = CSS.escape ? CSS.escape(columnName) : columnName.replace(/([\\".#:[\](){}+~> ])/g, '\\$1');
1676
+ const selectors = [
1677
+ `.header-cell[data-col-name="${escaped}"]`,
1678
+ `.filter-cell[data-col-name="${escaped}"]`,
1679
+ `.body-cell[data-col-name="${escaped}"] .cell-shell`,
1680
+ ];
1681
+ let maxWidth = 0;
1682
+ for (const selector of selectors) {
1683
+ const elements = container.querySelectorAll<HTMLElement>(selector);
1684
+ for (const element of elements) {
1685
+ maxWidth = Math.max(maxWidth, element.scrollWidth);
1686
+ }
1687
+ }
1688
+ return maxWidth + 12;
1689
+ }, []);
1690
+
1691
+ const handleHeaderResizeMouseDownFn = useCallback(
1692
+ (column: GridColumnDef, event: React.MouseEvent): void => {
1693
+ if (!canResizeColumnsFn()) return;
1694
+ event.preventDefault();
1695
+ event.stopPropagation();
1696
+
1697
+ const headerCell = (event.currentTarget as HTMLElement).closest<HTMLElement>('.header-cell');
1698
+ if (headerCell == null) return;
1699
+
1700
+ const startX = event.clientX;
1701
+ const startWidth = headerCell.getBoundingClientRect().width;
1702
+ let lastWidth = startWidth;
1703
+
1704
+ const handleMove = (moveEvent: MouseEvent): void => {
1705
+ lastWidth = Math.max(88, startWidth + (moveEvent.clientX - startX));
1706
+
1707
+ // Compute the new column template directly — no React state, no re-render.
1708
+ // This keeps virtualized resize smooth since the pipeline never re-runs mid-drag.
1709
+ const widthStr = `${Math.round(lastWidth)}px`;
1710
+ const newTemplate = buildGridTemplateColumns(
1711
+ visibleColumnsRef.current.map((c) =>
1712
+ c.name === column.name ? { ...c, width: widthStr } : c,
1713
+ ),
1714
+ );
1715
+ gridContainerRef.current
1716
+ ?.querySelectorAll<HTMLElement>('.header-grid, .filter-grid, .body-grid')
1717
+ .forEach((el) => {
1718
+ el.style.gridTemplateColumns = newTemplate;
1719
+ });
1720
+ };
1721
+
1722
+ const handleUp = (): void => {
1723
+ window.removeEventListener('mousemove', handleMove);
1724
+ window.removeEventListener('mouseup', handleUp);
1725
+ // Commit the final width to React state once — triggers one clean re-render.
1726
+ setColumnWidthOverrideFn(column.name, lastWidth);
1727
+ };
1728
+
1729
+ window.addEventListener('mousemove', handleMove);
1730
+ window.addEventListener('mouseup', handleUp);
1731
+ },
1732
+ [canResizeColumnsFn, setColumnWidthOverrideFn],
1733
+ );
1734
+
1735
+ const autoSizeColumnFn = useCallback(
1736
+ (column: GridColumnDef, event: React.MouseEvent): void => {
1737
+ if (!canResizeColumnsFn()) return;
1738
+ event.preventDefault();
1739
+ event.stopPropagation();
1740
+ setColumnWidthOverrideFn(column.name, measureAutoColumnWidthFn(column.name));
1741
+ },
1742
+ [canResizeColumnsFn, setColumnWidthOverrideFn, measureAutoColumnWidthFn],
1743
+ );
1744
+
1602
1745
  const onViewportScrollFn = useCallback((startIndex: number): void => {
1603
1746
  if (!scrollingRef.current) {
1604
1747
  scrollingRef.current = true;
@@ -1667,6 +1810,7 @@ export function useGridState(
1667
1810
  paginationSelectedPageSize,
1668
1811
  rowSize,
1669
1812
  viewportHeightPx,
1813
+ autoViewportHeight,
1670
1814
 
1671
1815
  headerLabel: headerLabelFn,
1672
1816
  isGroupItem: isGroupItemFn,
@@ -1683,6 +1827,7 @@ export function useGridState(
1683
1827
  groupDisclosureLabel: groupDisclosureLabelFn,
1684
1828
  displayValue: displayValueFn,
1685
1829
  isFocusedCell: isFocusedCellFn,
1830
+ isFocusedRow: isFocusedRowFn,
1686
1831
  isEditingCell: isEditingCellFn,
1687
1832
  editorInputType: editorInputTypeFn,
1688
1833
  cellContext: cellContextFn,
@@ -1732,6 +1877,9 @@ export function useGridState(
1732
1877
  toggleTreeRow: toggleTreeRowFn,
1733
1878
  moveColumn: moveColumnFn,
1734
1879
  moveVisibleColumn: moveVisibleColumnFn,
1880
+ canResizeColumns: canResizeColumnsFn,
1881
+ handleHeaderResizeMouseDown: handleHeaderResizeMouseDownFn,
1882
+ autoSizeColumn: autoSizeColumnFn,
1735
1883
  nextPage: nextPageFn,
1736
1884
  previousPage: previousPageFn,
1737
1885
  onPageSizeChange: onPageSizeChangeFn,