@alaarab/ogrid-react 2.0.22 → 2.1.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.
Files changed (66) hide show
  1. package/dist/esm/components/ColumnHeaderFilterContent.js +0 -2
  2. package/dist/esm/components/MarchingAntsOverlay.js +2 -3
  3. package/dist/esm/components/SideBar.js +8 -7
  4. package/dist/esm/components/createOGrid.js +1 -4
  5. package/dist/esm/hooks/index.js +10 -0
  6. package/dist/esm/hooks/useActiveCell.js +2 -4
  7. package/dist/esm/hooks/useCellSelection.js +85 -52
  8. package/dist/esm/hooks/useClipboard.js +15 -54
  9. package/dist/esm/hooks/useColumnChooserState.js +25 -13
  10. package/dist/esm/hooks/useColumnHeaderFilterState.js +22 -11
  11. package/dist/esm/hooks/useColumnHeaderMenuState.js +1 -1
  12. package/dist/esm/hooks/useColumnMeta.js +61 -0
  13. package/dist/esm/hooks/useColumnPinning.js +11 -12
  14. package/dist/esm/hooks/useColumnReorder.js +8 -1
  15. package/dist/esm/hooks/useColumnResize.js +6 -2
  16. package/dist/esm/hooks/useDataGridContextMenu.js +24 -0
  17. package/dist/esm/hooks/useDataGridEditing.js +56 -0
  18. package/dist/esm/hooks/useDataGridInteraction.js +109 -0
  19. package/dist/esm/hooks/useDataGridLayout.js +172 -0
  20. package/dist/esm/hooks/useDataGridState.js +83 -318
  21. package/dist/esm/hooks/useDataGridTableOrchestration.js +2 -4
  22. package/dist/esm/hooks/useFillHandle.js +60 -55
  23. package/dist/esm/hooks/useFilterOptions.js +2 -4
  24. package/dist/esm/hooks/useInlineCellEditorState.js +7 -13
  25. package/dist/esm/hooks/useKeyboardNavigation.js +19 -132
  26. package/dist/esm/hooks/useMultiSelectFilterState.js +1 -1
  27. package/dist/esm/hooks/useOGrid.js +159 -301
  28. package/dist/esm/hooks/useOGridDataFetching.js +74 -0
  29. package/dist/esm/hooks/useOGridFilters.js +59 -0
  30. package/dist/esm/hooks/useOGridPagination.js +24 -0
  31. package/dist/esm/hooks/useOGridSorting.js +24 -0
  32. package/dist/esm/hooks/usePaginationControls.js +2 -5
  33. package/dist/esm/hooks/usePeopleFilterState.js +6 -1
  34. package/dist/esm/hooks/useRichSelectState.js +7 -5
  35. package/dist/esm/hooks/useRowSelection.js +6 -26
  36. package/dist/esm/hooks/useSelectState.js +2 -5
  37. package/dist/esm/hooks/useShallowEqualMemo.js +14 -0
  38. package/dist/esm/hooks/useTableLayout.js +3 -11
  39. package/dist/esm/hooks/useUndoRedo.js +16 -10
  40. package/dist/esm/index.js +1 -1
  41. package/dist/esm/utils/index.js +1 -1
  42. package/dist/types/components/ColumnChooserProps.d.ts +2 -0
  43. package/dist/types/components/ColumnHeaderFilterContent.d.ts +0 -2
  44. package/dist/types/hooks/index.d.ts +19 -0
  45. package/dist/types/hooks/useClipboard.d.ts +0 -1
  46. package/dist/types/hooks/useColumnChooserState.d.ts +2 -0
  47. package/dist/types/hooks/useColumnHeaderFilterState.d.ts +0 -2
  48. package/dist/types/hooks/useColumnHeaderMenuState.d.ts +0 -2
  49. package/dist/types/hooks/useColumnMeta.d.ts +34 -0
  50. package/dist/types/hooks/useDataGridContextMenu.d.ts +20 -0
  51. package/dist/types/hooks/useDataGridEditing.d.ts +39 -0
  52. package/dist/types/hooks/useDataGridInteraction.d.ts +95 -0
  53. package/dist/types/hooks/useDataGridLayout.d.ts +45 -0
  54. package/dist/types/hooks/useDataGridState.d.ts +7 -1
  55. package/dist/types/hooks/useDataGridTableOrchestration.d.ts +1 -2
  56. package/dist/types/hooks/useOGrid.d.ts +4 -2
  57. package/dist/types/hooks/useOGridDataFetching.d.ts +29 -0
  58. package/dist/types/hooks/useOGridFilters.d.ts +24 -0
  59. package/dist/types/hooks/useOGridPagination.d.ts +18 -0
  60. package/dist/types/hooks/useOGridSorting.d.ts +23 -0
  61. package/dist/types/hooks/usePaginationControls.d.ts +1 -1
  62. package/dist/types/hooks/useRichSelectState.d.ts +2 -0
  63. package/dist/types/hooks/useShallowEqualMemo.d.ts +7 -0
  64. package/dist/types/index.d.ts +2 -2
  65. package/dist/types/utils/index.d.ts +2 -2
  66. package/package.json +12 -4
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useMemo } from 'react';
1
+ import { useState, useCallback } from 'react';
2
2
  /**
3
3
  * Manages column pinning state (left/right sticky positioning).
4
4
  * Supports controlled and uncontrolled modes.
@@ -6,8 +6,8 @@ import { useState, useCallback, useMemo } from 'react';
6
6
  */
7
7
  export function useColumnPinning(params) {
8
8
  const { columns, pinnedColumns: controlledPinnedColumns, onColumnPinned } = params;
9
- // Initialize internal state from column.pinned definitions
10
- const initialPinnedColumns = useMemo(() => {
9
+ // Initialize internal state from column.pinned definitions (mount only)
10
+ const [internalPinnedColumns, setInternalPinnedColumns] = useState(() => {
11
11
  const initial = {};
12
12
  for (const col of columns) {
13
13
  if (col.pinned) {
@@ -15,22 +15,21 @@ export function useColumnPinning(params) {
15
15
  }
16
16
  }
17
17
  return initial;
18
- // eslint-disable-next-line react-hooks/exhaustive-deps
19
- }, []); // Only on mount
20
- const [internalPinnedColumns, setInternalPinnedColumns] = useState(initialPinnedColumns);
18
+ });
21
19
  // Use controlled state if provided, otherwise internal
22
20
  const pinnedColumns = controlledPinnedColumns ?? internalPinnedColumns;
23
21
  const pinColumn = useCallback((columnId, side) => {
24
22
  const next = { ...pinnedColumns, [columnId]: side };
25
- setInternalPinnedColumns(next);
23
+ if (!controlledPinnedColumns)
24
+ setInternalPinnedColumns(next);
26
25
  onColumnPinned?.(columnId, side);
27
- }, [pinnedColumns, onColumnPinned]);
26
+ }, [pinnedColumns, controlledPinnedColumns, onColumnPinned]);
28
27
  const unpinColumn = useCallback((columnId) => {
29
- const next = { ...pinnedColumns };
30
- delete next[columnId];
31
- setInternalPinnedColumns(next);
28
+ const { [columnId]: _, ...next } = pinnedColumns;
29
+ if (!controlledPinnedColumns)
30
+ setInternalPinnedColumns(next);
32
31
  onColumnPinned?.(columnId, null);
33
- }, [pinnedColumns, onColumnPinned]);
32
+ }, [pinnedColumns, controlledPinnedColumns, onColumnPinned]);
34
33
  const isPinned = useCallback((columnId) => {
35
34
  return pinnedColumns[columnId];
36
35
  }, [pinnedColumns]);
@@ -98,7 +98,14 @@ export function useColumnReorder(params) {
98
98
  if (!wrapper)
99
99
  return;
100
100
  const currentOrder = columnOrderRef.current ?? columnsRef.current.map((c) => c.columnId);
101
- const result = calculateDropTarget(moveEvent.clientX, currentOrder, columnId, draggedPinState, wrapper, pinnedShape);
101
+ const result = calculateDropTarget({
102
+ mouseX: moveEvent.clientX,
103
+ columnOrder: currentOrder,
104
+ draggedColumnId: columnId,
105
+ draggedPinState,
106
+ tableElement: wrapper,
107
+ pinnedColumns: pinnedShape,
108
+ });
102
109
  if (result) {
103
110
  latestDropTargetIndex = result.targetIndex;
104
111
  setDropIndicatorX(result.indicatorX);
@@ -23,6 +23,11 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
23
23
  const handleResizeStart = useCallback((e, col) => {
24
24
  e.preventDefault();
25
25
  e.stopPropagation();
26
+ // Clean up any in-progress drag before starting a new one
27
+ if (cleanupRef.current) {
28
+ cleanupRef.current();
29
+ cleanupRef.current = null;
30
+ }
26
31
  const startX = e.clientX;
27
32
  const columnId = col.columnId;
28
33
  // Measure the actual rendered width from the DOM. With table-layout: auto,
@@ -111,8 +116,7 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
111
116
  document.addEventListener('mousemove', onMove);
112
117
  document.addEventListener('mouseup', onUp);
113
118
  cleanupRef.current = cleanup;
114
- // eslint-disable-next-line react-hooks/exhaustive-deps
115
- }, [defaultWidth, minWidth, setColumnSizingOverrides]); // columnSizingOverrides read via ref
119
+ }, [defaultWidth, minWidth, setColumnSizingOverrides, columnSizingOverridesRef]);
116
120
  const getColumnWidth = useCallback((col) => {
117
121
  return columnSizingOverrides[col.columnId]?.widthPx
118
122
  ?? col.idealWidth
@@ -0,0 +1,24 @@
1
+ import { useMemo } from 'react';
2
+ import { useContextMenu } from './useContextMenu';
3
+ // Stable no-op handlers used when cellSelection is disabled
4
+ const NOOP = () => { };
5
+ const NOOP_CTX = (_e) => { };
6
+ /**
7
+ * Manages context menu position and handlers.
8
+ * Extracted from useDataGridState for modularity.
9
+ */
10
+ export function useDataGridContextMenu(params) {
11
+ const { cellSelection } = params;
12
+ const { contextMenuPosition, setContextMenuPosition, handleCellContextMenu, closeContextMenu } = useContextMenu();
13
+ const contextMenuState = useMemo(() => ({
14
+ menuPosition: cellSelection ? contextMenuPosition : null,
15
+ setMenuPosition: cellSelection ? setContextMenuPosition : NOOP,
16
+ handleCellContextMenu: cellSelection ? handleCellContextMenu : NOOP_CTX,
17
+ closeContextMenu: cellSelection ? closeContextMenu : NOOP,
18
+ }), [cellSelection, contextMenuPosition, setContextMenuPosition, handleCellContextMenu, closeContextMenu]);
19
+ return {
20
+ contextMenu: contextMenuState,
21
+ contextMenuPosition,
22
+ setContextMenuPosition,
23
+ };
24
+ }
@@ -0,0 +1,56 @@
1
+ import { useMemo, useCallback, useState } from 'react';
2
+ import { parseValue } from '../utils';
3
+ import { useLatestRef } from './useLatestRef';
4
+ /**
5
+ * Manages cell editing commit/cancel logic and popover editor state.
6
+ * Extracted from useDataGridState for modularity.
7
+ *
8
+ * The editingCell/setEditingCell/pendingEditorValue/setPendingEditorValue are
9
+ * passed in from useCellEditing() (called at the orchestrator level) to avoid
10
+ * circular dependencies with useDataGridInteraction.
11
+ */
12
+ export function useDataGridEditing(params) {
13
+ const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, onCellValueChanged, setActiveCell, } = params;
14
+ const [popoverAnchorEl, setPopoverAnchorEl] = useState(null);
15
+ const visibleColsRef = useLatestRef(params.visibleCols);
16
+ const itemsLengthRef = useLatestRef(params.itemsLength);
17
+ const commitCellEdit = useCallback((item, columnId, oldValue, newValue, rowIndex, globalColIndex) => {
18
+ // Validate via valueParser before committing
19
+ const col = visibleColsRef.current.find((c) => c.columnId === columnId);
20
+ if (col) {
21
+ const result = parseValue(newValue, oldValue, item, col);
22
+ if (!result.valid) {
23
+ // Reject -- cancel the edit
24
+ setEditingCell(null);
25
+ setPopoverAnchorEl(null);
26
+ setPendingEditorValue(undefined);
27
+ return;
28
+ }
29
+ newValue = result.value;
30
+ }
31
+ onCellValueChanged?.({
32
+ item,
33
+ columnId,
34
+ oldValue,
35
+ newValue,
36
+ rowIndex,
37
+ });
38
+ setEditingCell(null);
39
+ setPopoverAnchorEl(null);
40
+ setPendingEditorValue(undefined);
41
+ // Advance to next row for inline editors
42
+ if (rowIndex < itemsLengthRef.current - 1) {
43
+ setActiveCell({ rowIndex: rowIndex + 1, columnIndex: globalColIndex });
44
+ }
45
+ }, [onCellValueChanged, setEditingCell, setPendingEditorValue, setActiveCell, visibleColsRef, itemsLengthRef]);
46
+ const cancelPopoverEdit = useCallback(() => {
47
+ setEditingCell(null);
48
+ setPopoverAnchorEl(null);
49
+ setPendingEditorValue(undefined);
50
+ }, [setEditingCell, setPendingEditorValue]);
51
+ const editingState = useMemo(() => ({
52
+ editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue,
53
+ commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl,
54
+ }), [editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl]);
55
+ return { editing: editingState };
56
+ }
@@ -0,0 +1,109 @@
1
+ import { useMemo, useCallback } from 'react';
2
+ import { useCellSelection } from './useCellSelection';
3
+ import { useClipboard } from './useClipboard';
4
+ import { useKeyboardNavigation } from './useKeyboardNavigation';
5
+ import { useFillHandle } from './useFillHandle';
6
+ import { useUndoRedo } from './useUndoRedo';
7
+ // Stable no-op handlers used when cellSelection is disabled (module-scope = no re-renders)
8
+ const NOOP = () => { };
9
+ const NOOP_ASYNC = async () => { };
10
+ const NOOP_MOUSE = (_e, _r, _c) => { };
11
+ const NOOP_KEY = (_e) => { };
12
+ /**
13
+ * Manages cell selection, keyboard navigation, clipboard, fill handle, and undo/redo.
14
+ * Extracted from useDataGridState for modularity.
15
+ *
16
+ * activeCell/setActiveCell and editingCell/setEditingCell are passed in from the
17
+ * orchestrator level to avoid circular dependencies with useDataGridEditing.
18
+ */
19
+ export function useDataGridInteraction(params) {
20
+ const { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId, editable, onCellValueChangedProp, cellSelection, rowSelection, selectedRowIds, editingCell, setEditingCell, activeCell, setActiveCell, handleRowCheckboxChange, setContextMenuPosition, wrapperRef, } = params;
21
+ // Wrap onCellValueChanged with undo/redo tracking
22
+ const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp });
23
+ const onCellValueChanged = undoRedo.onCellValueChanged;
24
+ const { selectionRange, setSelectionRange, handleCellMouseDown: handleCellMouseDownBase, handleSelectAllCells, isDragging, } = useCellSelection({
25
+ colOffset,
26
+ rowCount: items.length,
27
+ visibleColCount: visibleCols.length,
28
+ setActiveCell,
29
+ wrapperRef,
30
+ });
31
+ const { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges } = useClipboard({
32
+ items,
33
+ visibleCols,
34
+ colOffset,
35
+ selectionRange,
36
+ activeCell,
37
+ editable,
38
+ onCellValueChanged,
39
+ beginBatch: undoRedo.beginBatch,
40
+ endBatch: undoRedo.endBatch,
41
+ });
42
+ const handleCellMouseDown = useCallback((e, rowIndex, globalColIndex) => {
43
+ if (e.button !== 0)
44
+ return;
45
+ wrapperRef.current?.focus({ preventScroll: true });
46
+ clearClipboardRanges();
47
+ handleCellMouseDownBase(e, rowIndex, globalColIndex);
48
+ }, [handleCellMouseDownBase, clearClipboardRanges, wrapperRef]);
49
+ const { handleGridKeyDown } = useKeyboardNavigation({
50
+ data: { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId },
51
+ state: { activeCell, selectionRange, editingCell, selectedRowIds },
52
+ handlers: { setActiveCell, setSelectionRange, setEditingCell, handleRowCheckboxChange, handleCopy, handleCut, handlePaste, setContextMenu: setContextMenuPosition, onUndo: undoRedo.undo, onRedo: undoRedo.redo, clearClipboardRanges },
53
+ features: { editable, onCellValueChanged, rowSelection: rowSelection ?? 'none', wrapperRef },
54
+ });
55
+ const { handleFillHandleMouseDown } = useFillHandle({
56
+ items,
57
+ visibleCols,
58
+ editable,
59
+ onCellValueChanged,
60
+ selectionRange,
61
+ setSelectionRange,
62
+ setActiveCell,
63
+ colOffset,
64
+ wrapperRef,
65
+ beginBatch: undoRedo.beginBatch,
66
+ endBatch: undoRedo.endBatch,
67
+ });
68
+ const hasCellSelection = selectionRange != null || activeCell != null;
69
+ const interactionState = useMemo(() => ({
70
+ activeCell: cellSelection ? activeCell : null,
71
+ setActiveCell: cellSelection ? setActiveCell : NOOP,
72
+ selectionRange: cellSelection ? selectionRange : null,
73
+ setSelectionRange: cellSelection ? setSelectionRange : NOOP,
74
+ handleCellMouseDown: cellSelection ? handleCellMouseDown : NOOP_MOUSE,
75
+ handleSelectAllCells: cellSelection ? handleSelectAllCells : NOOP,
76
+ hasCellSelection: cellSelection ? hasCellSelection : false,
77
+ handleGridKeyDown: cellSelection ? handleGridKeyDown : NOOP_KEY,
78
+ handleFillHandleMouseDown: cellSelection ? handleFillHandleMouseDown : NOOP,
79
+ handleCopy: cellSelection ? handleCopy : NOOP,
80
+ handleCut: cellSelection ? handleCut : NOOP,
81
+ handlePaste: cellSelection ? handlePaste : NOOP_ASYNC,
82
+ cutRange: cellSelection ? cutRange : null,
83
+ copyRange: cellSelection ? copyRange : null,
84
+ clearClipboardRanges: cellSelection ? clearClipboardRanges : NOOP,
85
+ canUndo: undoRedo.canUndo,
86
+ canRedo: undoRedo.canRedo,
87
+ onUndo: undoRedo.undo,
88
+ onRedo: undoRedo.redo,
89
+ isDragging: cellSelection ? isDragging : false,
90
+ }), [
91
+ cellSelection, activeCell, setActiveCell, selectionRange, setSelectionRange,
92
+ handleCellMouseDown, handleSelectAllCells, hasCellSelection, handleGridKeyDown,
93
+ handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange,
94
+ clearClipboardRanges, undoRedo.canUndo, undoRedo.canRedo, undoRedo.undo, undoRedo.redo,
95
+ isDragging,
96
+ ]);
97
+ return {
98
+ interaction: interactionState,
99
+ selectionRange,
100
+ setSelectionRange,
101
+ cutRange,
102
+ copyRange,
103
+ clearClipboardRanges,
104
+ isDragging,
105
+ onCellValueChanged,
106
+ canUndo: undoRedo.canUndo,
107
+ canRedo: undoRedo.canRedo,
108
+ };
109
+ }
@@ -0,0 +1,172 @@
1
+ import { useMemo, useState, useLayoutEffect, useCallback } from 'react';
2
+ import { flattenColumns } from '../utils';
3
+ import { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
4
+ import { useTableLayout } from './useTableLayout';
5
+ import { useColumnPinning } from './useColumnPinning';
6
+ import { useColumnHeaderMenuState } from './useColumnHeaderMenuState';
7
+ import { useLatestRef } from './useLatestRef';
8
+ /**
9
+ * Manages column layout, visibility, sizing, pinning, and header menu state.
10
+ * Extracted from useDataGridState for modularity.
11
+ */
12
+ export function useDataGridLayout(params) {
13
+ const { columns, items, getRowId, visibleColumns, columnOrder, rowSelection = 'none', showRowNumbers, initialColumnWidths, onColumnResized, onAutosizeColumn, pinnedColumns, onColumnPinned, sortBy, sortDirection, onColumnSort, wrapperRef, } = params;
14
+ // Cast is safe: input columns are React.IColumnDef instances; flattenColumns only extracts leaves.
15
+ const flatColumnsRaw = useMemo(() => flattenColumns(columns), [columns]);
16
+ // Apply runtime pin overrides (from applyColumnState or programmatic changes)
17
+ const flatColumns = useMemo(() => {
18
+ if (!pinnedColumns || Object.keys(pinnedColumns).length === 0)
19
+ return flatColumnsRaw;
20
+ return flatColumnsRaw.map((col) => {
21
+ const override = pinnedColumns[col.columnId];
22
+ if (override && col.pinned !== override) {
23
+ return { ...col, pinned: override };
24
+ }
25
+ return col;
26
+ });
27
+ }, [flatColumnsRaw, pinnedColumns]);
28
+ const visibleCols = useMemo(() => {
29
+ const filtered = visibleColumns
30
+ ? flatColumns.filter((c) => visibleColumns.has(c.columnId))
31
+ : flatColumns;
32
+ if (!columnOrder?.length)
33
+ return filtered;
34
+ const orderMap = new Map();
35
+ for (let i = 0; i < columnOrder.length; i++) {
36
+ orderMap.set(columnOrder[i], i);
37
+ }
38
+ return [...filtered].sort((a, b) => {
39
+ const ia = orderMap.get(a.columnId) ?? -1;
40
+ const ib = orderMap.get(b.columnId) ?? -1;
41
+ if (ia === -1 && ib === -1)
42
+ return 0;
43
+ if (ia === -1)
44
+ return 1;
45
+ if (ib === -1)
46
+ return -1;
47
+ return ia - ib;
48
+ });
49
+ }, [flatColumns, visibleColumns, columnOrder]);
50
+ const visibleColumnCount = visibleCols.length;
51
+ const hasCheckboxCol = rowSelection === 'multiple';
52
+ const hasRowNumbersCol = !!showRowNumbers;
53
+ const specialColsCount = (hasCheckboxCol ? 1 : 0) + (hasRowNumbersCol ? 1 : 0);
54
+ const totalColCount = visibleColumnCount + specialColsCount;
55
+ const colOffset = specialColsCount;
56
+ const rowIndexByRowId = useMemo(() => {
57
+ const m = new Map();
58
+ items.forEach((item, idx) => m.set(getRowId(item), idx));
59
+ return m;
60
+ }, [items, getRowId]);
61
+ const { containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, } = useTableLayout({
62
+ wrapperRef,
63
+ visibleCols,
64
+ flatColumns,
65
+ hasCheckboxCol,
66
+ initialColumnWidths,
67
+ onColumnResized,
68
+ });
69
+ const pinningResult = useColumnPinning({
70
+ columns: flatColumns,
71
+ pinnedColumns,
72
+ onColumnPinned,
73
+ });
74
+ // Measure actual column widths from the DOM for accurate pinning offsets.
75
+ const [measuredColumnWidths, setMeasuredColumnWidths] = useState({});
76
+ useLayoutEffect(() => {
77
+ const wrapper = wrapperRef.current;
78
+ if (!wrapper)
79
+ return;
80
+ const headerCells = wrapper.querySelectorAll('th[data-column-id]');
81
+ if (headerCells.length === 0)
82
+ return;
83
+ const measured = {};
84
+ headerCells.forEach((cell) => {
85
+ const colId = cell.getAttribute('data-column-id');
86
+ if (colId)
87
+ measured[colId] = cell.offsetWidth;
88
+ });
89
+ setMeasuredColumnWidths((prev) => {
90
+ for (const key in measured) {
91
+ if (prev[key] !== measured[key])
92
+ return measured;
93
+ }
94
+ if (Object.keys(prev).length !== Object.keys(measured).length)
95
+ return measured;
96
+ return prev;
97
+ });
98
+ }, [visibleCols, containerWidth, columnSizingOverrides, wrapperRef]);
99
+ // Build column width map for pinning offset computation
100
+ const columnWidthMap = useMemo(() => {
101
+ const map = {};
102
+ for (const col of visibleCols) {
103
+ const override = columnSizingOverrides[col.columnId];
104
+ map[col.columnId] = override
105
+ ? override.widthPx
106
+ : (measuredColumnWidths[col.columnId] ?? col.idealWidth ?? col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
107
+ }
108
+ return map;
109
+ }, [visibleCols, columnSizingOverrides, measuredColumnWidths]);
110
+ const leftOffsets = useMemo(() => pinningResult.computeLeftOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH, hasCheckboxCol, CHECKBOX_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap, hasCheckboxCol]);
111
+ const rightOffsets = useMemo(() => pinningResult.computeRightOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap]);
112
+ // Stabilize onColumnSort via ref
113
+ const onColumnSortRef = useLatestRef(onColumnSort);
114
+ const stableOnColumnSort = useCallback((columnKey, direction) => onColumnSortRef.current?.(columnKey, direction), [onColumnSortRef]);
115
+ // Autosize callback
116
+ const handleAutosizeColumn = useCallback((columnId, width) => {
117
+ setColumnSizingOverrides((prev) => ({ ...prev, [columnId]: { widthPx: width } }));
118
+ (onAutosizeColumn ?? onColumnResized)?.(columnId, width);
119
+ }, [setColumnSizingOverrides, onAutosizeColumn, onColumnResized]);
120
+ const headerMenuResult = useColumnHeaderMenuState({
121
+ pinnedColumns: pinningResult.pinnedColumns,
122
+ onPinColumn: pinningResult.pinColumn,
123
+ onUnpinColumn: pinningResult.unpinColumn,
124
+ sortBy,
125
+ sortDirection: sortDirection ?? 'asc',
126
+ onColumnSort: stableOnColumnSort,
127
+ onColumnResized,
128
+ onAutosizeColumn: handleAutosizeColumn,
129
+ columns: flatColumns,
130
+ });
131
+ // Memoize layout sub-object
132
+ const layoutState = useMemo(() => ({
133
+ flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
134
+ hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
135
+ desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
136
+ measuredColumnWidths,
137
+ }), [
138
+ flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
139
+ hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
140
+ desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
141
+ measuredColumnWidths,
142
+ ]);
143
+ // Memoize pinning sub-object
144
+ const pinningState = useMemo(() => ({
145
+ pinnedColumns: pinningResult.pinnedColumns,
146
+ pinColumn: pinningResult.pinColumn,
147
+ unpinColumn: pinningResult.unpinColumn,
148
+ isPinned: pinningResult.isPinned,
149
+ leftOffsets,
150
+ rightOffsets,
151
+ headerMenu: headerMenuResult,
152
+ }), [
153
+ pinningResult.pinnedColumns, pinningResult.pinColumn, pinningResult.unpinColumn,
154
+ pinningResult.isPinned, leftOffsets, rightOffsets,
155
+ headerMenuResult,
156
+ ]);
157
+ return {
158
+ layout: layoutState,
159
+ pinning: pinningState,
160
+ flatColumns,
161
+ visibleCols,
162
+ visibleColumnCount,
163
+ totalColCount,
164
+ colOffset,
165
+ hasCheckboxCol,
166
+ hasRowNumbersCol,
167
+ columnSizingOverrides,
168
+ setColumnSizingOverrides,
169
+ handleAutosizeColumn,
170
+ stableOnColumnSort,
171
+ };
172
+ }