@alaarab/ogrid-core 1.8.2 → 1.9.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 (71) hide show
  1. package/README.md +42 -31
  2. package/dist/esm/components/BaseInlineCellEditor.js +112 -0
  3. package/dist/esm/components/CellErrorBoundary.js +43 -0
  4. package/dist/esm/components/EmptyState.js +19 -0
  5. package/dist/esm/components/GridContextMenu.js +4 -3
  6. package/dist/esm/components/MarchingAntsOverlay.js +6 -5
  7. package/dist/esm/components/OGridLayout.js +7 -6
  8. package/dist/esm/components/SideBar.js +66 -44
  9. package/dist/esm/constants.js +11 -0
  10. package/dist/esm/hooks/index.js +6 -0
  11. package/dist/esm/hooks/useActiveCell.js +25 -9
  12. package/dist/esm/hooks/useCellEditing.js +4 -0
  13. package/dist/esm/hooks/useCellSelection.js +7 -1
  14. package/dist/esm/hooks/useClipboard.js +36 -36
  15. package/dist/esm/hooks/useColumnHeaderFilterState.js +71 -119
  16. package/dist/esm/hooks/useColumnResize.js +27 -4
  17. package/dist/esm/hooks/useContextMenu.js +9 -5
  18. package/dist/esm/hooks/useDataGridState.js +110 -162
  19. package/dist/esm/hooks/useDateFilterState.js +34 -0
  20. package/dist/esm/hooks/useFillHandle.js +7 -2
  21. package/dist/esm/hooks/useFilterOptions.js +5 -5
  22. package/dist/esm/hooks/useKeyboardNavigation.js +18 -34
  23. package/dist/esm/hooks/useLatestRef.js +11 -0
  24. package/dist/esm/hooks/useMultiSelectFilterState.js +59 -0
  25. package/dist/esm/hooks/useOGrid.js +71 -18
  26. package/dist/esm/hooks/usePeopleFilterState.js +68 -0
  27. package/dist/esm/hooks/useRichSelectState.js +5 -0
  28. package/dist/esm/hooks/useRowSelection.js +14 -4
  29. package/dist/esm/hooks/useSideBarState.js +5 -0
  30. package/dist/esm/hooks/useTableLayout.js +77 -0
  31. package/dist/esm/hooks/useTextFilterState.js +25 -0
  32. package/dist/esm/hooks/useUndoRedo.js +6 -5
  33. package/dist/esm/index.js +7 -2
  34. package/dist/esm/utils/clientSideData.js +25 -12
  35. package/dist/esm/utils/columnUtils.js +6 -0
  36. package/dist/esm/utils/gridRowComparator.js +68 -0
  37. package/dist/esm/utils/index.js +1 -0
  38. package/dist/esm/utils/ogridHelpers.js +2 -1
  39. package/dist/esm/utils/paginationHelpers.js +7 -1
  40. package/dist/types/components/BaseInlineCellEditor.d.ts +33 -0
  41. package/dist/types/components/CellErrorBoundary.d.ts +25 -0
  42. package/dist/types/components/EmptyState.d.ts +26 -0
  43. package/dist/types/constants.d.ts +11 -0
  44. package/dist/types/hooks/index.d.ts +12 -1
  45. package/dist/types/hooks/useCellEditing.d.ts +4 -0
  46. package/dist/types/hooks/useCellSelection.d.ts +5 -0
  47. package/dist/types/hooks/useClipboard.d.ts +5 -0
  48. package/dist/types/hooks/useColumnHeaderFilterState.d.ts +1 -0
  49. package/dist/types/hooks/useColumnResize.d.ts +5 -0
  50. package/dist/types/hooks/useContextMenu.d.ts +6 -2
  51. package/dist/types/hooks/useDataGridState.d.ts +1 -0
  52. package/dist/types/hooks/useDateFilterState.d.ts +19 -0
  53. package/dist/types/hooks/useFillHandle.d.ts +5 -0
  54. package/dist/types/hooks/useKeyboardNavigation.d.ts +38 -25
  55. package/dist/types/hooks/useLatestRef.d.ts +6 -0
  56. package/dist/types/hooks/useMultiSelectFilterState.d.ts +24 -0
  57. package/dist/types/hooks/useOGrid.d.ts +30 -9
  58. package/dist/types/hooks/usePeopleFilterState.d.ts +25 -0
  59. package/dist/types/hooks/useRichSelectState.d.ts +5 -0
  60. package/dist/types/hooks/useRowSelection.d.ts +5 -0
  61. package/dist/types/hooks/useSideBarState.d.ts +5 -0
  62. package/dist/types/hooks/useTableLayout.d.ts +27 -0
  63. package/dist/types/hooks/useTextFilterState.d.ts +16 -0
  64. package/dist/types/hooks/useUndoRedo.d.ts +3 -1
  65. package/dist/types/index.d.ts +11 -4
  66. package/dist/types/types/columnTypes.d.ts +2 -3
  67. package/dist/types/types/dataGridTypes.d.ts +32 -4
  68. package/dist/types/types/index.d.ts +1 -1
  69. package/dist/types/utils/gridRowComparator.d.ts +49 -0
  70. package/dist/types/utils/index.d.ts +2 -0
  71. package/package.json +1 -1
@@ -2,26 +2,41 @@ import { useCallback, useRef, useState } from 'react';
2
2
  import { getCellValue } from '../utils';
3
3
  import { parseValue } from '../utils/valueParsers';
4
4
  import { normalizeSelectionRange } from '../types';
5
+ import { useLatestRef } from './useLatestRef';
6
+ /**
7
+ * Manages copy, cut, and paste operations for cell ranges with TSV clipboard format.
8
+ * @param params - Items, columns, selection, editability, and value change callback.
9
+ * @returns Copy/cut/paste handlers, cut/copy ranges, and range clear function.
10
+ */
5
11
  export function useClipboard(params) {
6
- const { items, visibleCols, colOffset, selectionRange, activeCell, editable, onCellValueChanged, beginBatch, endBatch, } = params;
12
+ const { colOffset, beginBatch, endBatch, } = params;
13
+ // Volatile values accessed via refs — keeps callbacks stable
14
+ const itemsRef = useLatestRef(params.items);
15
+ const visibleColsRef = useLatestRef(params.visibleCols);
16
+ const selectionRangeRef = useLatestRef(params.selectionRange);
17
+ const activeCellRef = useLatestRef(params.activeCell);
18
+ const editableRef = useLatestRef(params.editable);
19
+ const onCellValueChangedRef = useLatestRef(params.onCellValueChanged);
7
20
  const cutRangeRef = useRef(null);
8
21
  const [cutRange, setCutRange] = useState(null);
9
22
  const [copyRange, setCopyRange] = useState(null);
10
23
  /** In-page clipboard fallback when system clipboard is unavailable. */
11
24
  const internalClipboardRef = useRef(null);
25
+ /** Resolve current effective range from selection or active cell. */
26
+ const getEffectiveRange = useCallback(() => {
27
+ const sel = selectionRangeRef.current;
28
+ const ac = activeCellRef.current;
29
+ return sel ?? (ac != null
30
+ ? { startRow: ac.rowIndex, startCol: ac.columnIndex - colOffset, endRow: ac.rowIndex, endCol: ac.columnIndex - colOffset }
31
+ : null);
32
+ }, [colOffset, selectionRangeRef, activeCellRef]);
12
33
  const handleCopy = useCallback(() => {
13
- const range = selectionRange ??
14
- (activeCell != null
15
- ? {
16
- startRow: activeCell.rowIndex,
17
- startCol: activeCell.columnIndex - colOffset,
18
- endRow: activeCell.rowIndex,
19
- endCol: activeCell.columnIndex - colOffset,
20
- }
21
- : null);
34
+ const range = getEffectiveRange();
22
35
  if (range == null)
23
36
  return;
24
37
  const norm = normalizeSelectionRange(range);
38
+ const items = itemsRef.current;
39
+ const visibleCols = visibleColsRef.current;
25
40
  const rows = [];
26
41
  for (let r = norm.startRow; r <= norm.endRow; r++) {
27
42
  const cells = [];
@@ -40,20 +55,12 @@ export function useClipboard(params) {
40
55
  internalClipboardRef.current = tsv;
41
56
  setCopyRange(norm);
42
57
  void navigator.clipboard.writeText(tsv).catch(() => { });
43
- }, [selectionRange, activeCell, colOffset, items, visibleCols]);
58
+ }, [getEffectiveRange, itemsRef, visibleColsRef]);
44
59
  const handleCut = useCallback(() => {
45
- if (editable === false)
60
+ if (editableRef.current === false)
46
61
  return;
47
- const range = selectionRange ??
48
- (activeCell != null
49
- ? {
50
- startRow: activeCell.rowIndex,
51
- startCol: activeCell.columnIndex - colOffset,
52
- endRow: activeCell.rowIndex,
53
- endCol: activeCell.columnIndex - colOffset,
54
- }
55
- : null);
56
- if (range == null || onCellValueChanged == null)
62
+ const range = getEffectiveRange();
63
+ if (range == null || onCellValueChangedRef.current == null)
57
64
  return;
58
65
  const norm = normalizeSelectionRange(range);
59
66
  cutRangeRef.current = norm;
@@ -62,10 +69,11 @@ export function useClipboard(params) {
62
69
  handleCopy();
63
70
  // handleCopy sets copyRange — override it back since this is a cut
64
71
  setCopyRange(null);
65
- }, [selectionRange, activeCell, colOffset, handleCopy, editable, onCellValueChanged]);
72
+ }, [getEffectiveRange, handleCopy, editableRef, onCellValueChangedRef]);
66
73
  const handlePaste = useCallback(async () => {
67
- if (editable === false)
74
+ if (editableRef.current === false)
68
75
  return;
76
+ const onCellValueChanged = onCellValueChangedRef.current;
69
77
  if (onCellValueChanged == null)
70
78
  return;
71
79
  let text;
@@ -80,17 +88,11 @@ export function useClipboard(params) {
80
88
  }
81
89
  if (!text.trim())
82
90
  return;
83
- const norm = selectionRange ??
84
- (activeCell != null
85
- ? {
86
- startRow: activeCell.rowIndex,
87
- startCol: activeCell.columnIndex - colOffset,
88
- endRow: activeCell.rowIndex,
89
- endCol: activeCell.columnIndex - colOffset,
90
- }
91
- : null);
91
+ const norm = getEffectiveRange();
92
92
  const anchorRow = norm ? norm.startRow : 0;
93
93
  const anchorCol = norm ? norm.startCol : 0;
94
+ const items = itemsRef.current;
95
+ const visibleCols = visibleColsRef.current;
94
96
  const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
95
97
  beginBatch?.();
96
98
  for (let r = 0; r < lines.length; r++) {
@@ -114,7 +116,6 @@ export function useClipboard(params) {
114
116
  onCellValueChanged({
115
117
  item,
116
118
  columnId: col.columnId,
117
- field: col.columnId,
118
119
  oldValue,
119
120
  newValue: result.value,
120
121
  rowIndex: targetRow,
@@ -140,7 +141,6 @@ export function useClipboard(params) {
140
141
  onCellValueChanged({
141
142
  item,
142
143
  columnId: col.columnId,
143
- field: col.columnId,
144
144
  oldValue,
145
145
  newValue: result.value,
146
146
  rowIndex: r,
@@ -152,7 +152,7 @@ export function useClipboard(params) {
152
152
  }
153
153
  endBatch?.();
154
154
  setCopyRange(null);
155
- }, [selectionRange, activeCell, colOffset, items, visibleCols, editable, onCellValueChanged, beginBatch, endBatch]);
155
+ }, [getEffectiveRange, itemsRef, visibleColsRef, editableRef, onCellValueChangedRef, beginBatch, endBatch]);
156
156
  const clearClipboardRanges = useCallback(() => {
157
157
  setCopyRange(null);
158
158
  setCutRange(null);
@@ -1,48 +1,52 @@
1
1
  /**
2
2
  * Headless column header filter state and handlers for Fluent, Material, and Radix.
3
3
  * UI packages use this hook and render only presentation (popover, inputs, buttons).
4
+ * Composes 4 sub-hooks for each filter type's state management.
4
5
  */
5
6
  import { useState, useCallback, useRef, useEffect, useMemo, } from 'react';
6
- import { useDebounce } from './useDebounce';
7
- const SEARCH_DEBOUNCE_MS = 150;
7
+ import { useTextFilterState } from './useTextFilterState';
8
+ import { useMultiSelectFilterState } from './useMultiSelectFilterState';
9
+ import { usePeopleFilterState } from './usePeopleFilterState';
10
+ import { useDateFilterState } from './useDateFilterState';
8
11
  const EMPTY_OPTIONS = [];
9
12
  export function useColumnHeaderFilterState(params) {
10
13
  const { filterType, onSort, selectedValues, onFilterChange, options, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, dateValue, onDateChange, } = params;
11
14
  const safeSelectedValues = selectedValues ?? EMPTY_OPTIONS;
12
- const safeOptions = options ?? EMPTY_OPTIONS;
15
+ // Shared state
13
16
  const headerRef = useRef(null);
14
17
  const popoverRef = useRef(null);
15
- const peopleInputRef = useRef(null);
16
- const peopleSearchTimeoutRef = useRef(undefined);
17
18
  const [isFilterOpen, setFilterOpen] = useState(false);
18
- const [tempSelected, setTempSelected] = useState(() => new Set(safeSelectedValues));
19
- const [tempTextValue, setTempTextValue] = useState(textValue);
20
- const [searchText, setSearchText] = useState('');
21
- const debouncedSearchText = useDebounce(searchText, SEARCH_DEBOUNCE_MS);
22
- const [peopleSuggestions, setPeopleSuggestions] = useState([]);
23
- const [isPeopleLoading, setIsPeopleLoading] = useState(false);
24
- const [peopleSearchText, setPeopleSearchText] = useState('');
25
- const [tempDateFrom, setTempDateFrom] = useState(dateValue?.from ?? '');
26
- const [tempDateTo, setTempDateTo] = useState(dateValue?.to ?? '');
27
19
  const [popoverPosition, setPopoverPosition] = useState(null);
28
- // Sync temp state when popover opens
20
+ // Compose sub-hooks for each filter type
21
+ const textFilterState = useTextFilterState({
22
+ textValue,
23
+ onTextChange,
24
+ isFilterOpen,
25
+ });
26
+ const multiSelectFilterState = useMultiSelectFilterState({
27
+ selectedValues,
28
+ onFilterChange,
29
+ options,
30
+ isFilterOpen,
31
+ });
32
+ const peopleFilterState = usePeopleFilterState({
33
+ selectedUser,
34
+ onUserChange,
35
+ peopleSearch,
36
+ isFilterOpen,
37
+ filterType,
38
+ });
39
+ const dateFilterState = useDateFilterState({
40
+ dateValue,
41
+ onDateChange,
42
+ isFilterOpen,
43
+ });
44
+ // Close popover resets position
29
45
  useEffect(() => {
30
- if (isFilterOpen) {
31
- setTempSelected(new Set(safeSelectedValues));
32
- setTempTextValue(textValue);
33
- setTempDateFrom(dateValue?.from ?? '');
34
- setTempDateTo(dateValue?.to ?? '');
35
- setSearchText('');
36
- setPeopleSearchText('');
37
- setPeopleSuggestions([]);
38
- if (filterType === 'people') {
39
- setTimeout(() => peopleInputRef.current?.focus(), 50);
40
- }
41
- }
42
- else {
46
+ if (!isFilterOpen) {
43
47
  setPopoverPosition(null);
44
48
  }
45
- }, [isFilterOpen, filterType, safeSelectedValues, textValue, dateValue]);
49
+ }, [isFilterOpen]);
46
50
  // Click outside and Escape to close
47
51
  useEffect(() => {
48
52
  if (!isFilterOpen)
@@ -71,41 +75,7 @@ export function useColumnHeaderFilterState(params) {
71
75
  document.removeEventListener('keydown', handleKeyDown, true);
72
76
  };
73
77
  }, [isFilterOpen]);
74
- // Filtered options for multiSelect (search within options)
75
- const filteredOptions = useMemo(() => {
76
- if (!debouncedSearchText.trim())
77
- return safeOptions;
78
- const searchLower = debouncedSearchText.toLowerCase().trim();
79
- return safeOptions.filter((opt) => opt.toLowerCase().includes(searchLower));
80
- }, [safeOptions, debouncedSearchText]);
81
- // People search
82
- useEffect(() => {
83
- if (!peopleSearch || !isFilterOpen || filterType !== 'people')
84
- return;
85
- if (peopleSearchTimeoutRef.current)
86
- window.clearTimeout(peopleSearchTimeoutRef.current);
87
- if (!peopleSearchText.trim()) {
88
- setPeopleSuggestions([]);
89
- return;
90
- }
91
- setIsPeopleLoading(true);
92
- peopleSearchTimeoutRef.current = window.setTimeout(async () => {
93
- try {
94
- const results = await peopleSearch(peopleSearchText);
95
- setPeopleSuggestions(results.slice(0, 10));
96
- }
97
- catch {
98
- setPeopleSuggestions([]);
99
- }
100
- finally {
101
- setIsPeopleLoading(false);
102
- }
103
- }, 300);
104
- return () => {
105
- if (peopleSearchTimeoutRef.current)
106
- window.clearTimeout(peopleSearchTimeoutRef.current);
107
- };
108
- }, [peopleSearchText, peopleSearch, isFilterOpen, filterType]);
78
+ // Shared handlers
109
79
  const handleFilterIconClick = useCallback((e) => {
110
80
  e.stopPropagation();
111
81
  e.preventDefault();
@@ -121,47 +91,28 @@ export function useColumnHeaderFilterState(params) {
121
91
  e.stopPropagation();
122
92
  onSort?.();
123
93
  }, [onSort]);
124
- const handleCheckboxChange = useCallback((option, checked) => {
125
- setTempSelected((prev) => {
126
- const next = new Set(prev);
127
- if (checked)
128
- next.add(option);
129
- else
130
- next.delete(option);
131
- return next;
132
- });
133
- }, []);
134
- const handleSelectAll = useCallback(() => {
135
- setTempSelected(new Set(filteredOptions));
136
- }, [filteredOptions]);
137
- const handleClearSelection = useCallback(() => setTempSelected(new Set()), []);
94
+ // Wrap sub-hook handlers to close popover
138
95
  const handleApplyMultiSelect = useCallback(() => {
139
- onFilterChange?.(Array.from(tempSelected));
96
+ multiSelectFilterState.handleApplyMultiSelect();
140
97
  setFilterOpen(false);
141
- }, [onFilterChange, tempSelected]);
98
+ }, [multiSelectFilterState]);
142
99
  const handleTextApply = useCallback(() => {
143
- onTextChange?.(tempTextValue.trim());
100
+ textFilterState.handleTextApply();
144
101
  setFilterOpen(false);
145
- }, [onTextChange, tempTextValue]);
146
- const handleTextClear = useCallback(() => setTempTextValue(''), []);
102
+ }, [textFilterState]);
147
103
  const handleUserSelect = useCallback((user) => {
148
- onUserChange?.(user);
104
+ peopleFilterState.handleUserSelect(user);
149
105
  setFilterOpen(false);
150
- }, [onUserChange]);
151
- const handleDateApply = useCallback(() => {
152
- const from = tempDateFrom || undefined;
153
- const to = tempDateTo || undefined;
154
- onDateChange?.(from || to ? { from, to } : undefined);
155
- setFilterOpen(false);
156
- }, [onDateChange, tempDateFrom, tempDateTo]);
157
- const handleDateClear = useCallback(() => {
158
- setTempDateFrom('');
159
- setTempDateTo('');
160
- }, []);
106
+ }, [peopleFilterState]);
161
107
  const handleClearUser = useCallback(() => {
162
- onUserChange?.(undefined);
108
+ peopleFilterState.handleClearUser();
109
+ setFilterOpen(false);
110
+ }, [peopleFilterState]);
111
+ const handleDateApply = useCallback(() => {
112
+ dateFilterState.handleDateApply();
163
113
  setFilterOpen(false);
164
- }, [onUserChange]);
114
+ }, [dateFilterState]);
115
+ // Event propagation stoppers
165
116
  const handlePopoverClick = useCallback((e) => e.stopPropagation(), []);
166
117
  const handleInputFocus = useCallback((e) => e.stopPropagation(), []);
167
118
  const handleInputMouseDown = useCallback((e) => e.stopPropagation(), []);
@@ -170,6 +121,7 @@ export function useColumnHeaderFilterState(params) {
170
121
  if (e.key !== 'Escape' && e.key !== 'Esc')
171
122
  e.stopPropagation();
172
123
  }, []);
124
+ // Compute hasActiveFilter from all sub-hooks
173
125
  const hasActiveFilter = useMemo(() => {
174
126
  if (filterType === 'multiSelect')
175
127
  return safeSelectedValues.length > 0;
@@ -184,39 +136,39 @@ export function useColumnHeaderFilterState(params) {
184
136
  return {
185
137
  headerRef,
186
138
  popoverRef,
187
- peopleInputRef,
139
+ peopleInputRef: peopleFilterState.peopleInputRef,
188
140
  isFilterOpen,
189
141
  setFilterOpen,
190
- tempSelected,
191
- setTempSelected,
192
- tempTextValue,
193
- setTempTextValue,
194
- searchText,
195
- setSearchText,
196
- debouncedSearchText,
197
- filteredOptions,
198
- peopleSuggestions,
199
- isPeopleLoading,
200
- peopleSearchText,
201
- setPeopleSearchText,
202
- tempDateFrom,
203
- setTempDateFrom,
204
- tempDateTo,
205
- setTempDateTo,
142
+ tempSelected: multiSelectFilterState.tempSelected,
143
+ setTempSelected: multiSelectFilterState.setTempSelected,
144
+ tempTextValue: textFilterState.tempTextValue,
145
+ setTempTextValue: textFilterState.setTempTextValue,
146
+ searchText: multiSelectFilterState.searchText,
147
+ setSearchText: multiSelectFilterState.setSearchText,
148
+ debouncedSearchText: multiSelectFilterState.debouncedSearchText,
149
+ filteredOptions: multiSelectFilterState.filteredOptions,
150
+ peopleSuggestions: peopleFilterState.peopleSuggestions,
151
+ isPeopleLoading: peopleFilterState.isPeopleLoading,
152
+ peopleSearchText: peopleFilterState.peopleSearchText,
153
+ setPeopleSearchText: peopleFilterState.setPeopleSearchText,
154
+ tempDateFrom: dateFilterState.tempDateFrom,
155
+ setTempDateFrom: dateFilterState.setTempDateFrom,
156
+ tempDateTo: dateFilterState.tempDateTo,
157
+ setTempDateTo: dateFilterState.setTempDateTo,
206
158
  hasActiveFilter,
207
159
  popoverPosition,
208
160
  handlers: {
209
161
  handleFilterIconClick,
210
162
  handleApplyMultiSelect,
211
163
  handleTextApply,
212
- handleTextClear,
164
+ handleTextClear: textFilterState.handleTextClear,
213
165
  handleUserSelect,
214
166
  handleClearUser,
215
167
  handleDateApply,
216
- handleDateClear,
217
- handleCheckboxChange,
218
- handleSelectAll,
219
- handleClearSelection,
168
+ handleDateClear: dateFilterState.handleDateClear,
169
+ handleCheckboxChange: multiSelectFilterState.handleCheckboxChange,
170
+ handleSelectAll: multiSelectFilterState.handleSelectAll,
171
+ handleClearSelection: multiSelectFilterState.handleClearSelection,
220
172
  handlePopoverClick,
221
173
  handleInputFocus,
222
174
  handleInputMouseDown,
@@ -1,8 +1,25 @@
1
- import { useCallback, useRef } from 'react';
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import { useLatestRef } from './useLatestRef';
3
+ /**
4
+ * Manages column resize drag interactions with RAF-throttled state updates.
5
+ * @param params - Sizing overrides, setter, min/default widths, and resize callback.
6
+ * @returns Resize start handler and column width getter.
7
+ */
2
8
  export function useColumnResize({ columnSizingOverrides, setColumnSizingOverrides, minWidth = 80, defaultWidth = 120, onColumnResized, }) {
3
9
  const rafRef = useRef(0);
4
10
  const onColumnResizedRef = useRef(onColumnResized);
5
11
  onColumnResizedRef.current = onColumnResized;
12
+ const columnSizingOverridesRef = useLatestRef(columnSizingOverrides);
13
+ // Track active drag listeners so we can clean up on unmount
14
+ const cleanupRef = useRef(null);
15
+ useEffect(() => {
16
+ return () => {
17
+ if (cleanupRef.current) {
18
+ cleanupRef.current();
19
+ cleanupRef.current = null;
20
+ }
21
+ };
22
+ }, []);
6
23
  const handleResizeStart = useCallback((e, col) => {
7
24
  e.preventDefault();
8
25
  e.stopPropagation();
@@ -14,7 +31,7 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
14
31
  const thEl = e.currentTarget.parentElement;
15
32
  const startWidth = thEl
16
33
  ? thEl.getBoundingClientRect().width
17
- : columnSizingOverrides[columnId]?.widthPx
34
+ : columnSizingOverridesRef.current[columnId]?.widthPx
18
35
  ?? col.idealWidth
19
36
  ?? col.defaultWidth
20
37
  ?? defaultWidth;
@@ -40,9 +57,10 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
40
57
  });
41
58
  }
42
59
  };
43
- const onUp = () => {
60
+ const cleanup = () => {
44
61
  document.removeEventListener('mousemove', onMove);
45
62
  document.removeEventListener('mouseup', onUp);
63
+ cleanupRef.current = null;
46
64
  // Restore cursor and user-select
47
65
  document.body.style.cursor = prevCursor;
48
66
  document.body.style.userSelect = prevUserSelect;
@@ -51,6 +69,9 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
51
69
  cancelAnimationFrame(rafRef.current);
52
70
  rafRef.current = 0;
53
71
  }
72
+ };
73
+ const onUp = () => {
74
+ cleanup();
54
75
  flushWidth();
55
76
  if (onColumnResizedRef.current) {
56
77
  onColumnResizedRef.current(columnId, latestWidth);
@@ -58,7 +79,9 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
58
79
  };
59
80
  document.addEventListener('mousemove', onMove);
60
81
  document.addEventListener('mouseup', onUp);
61
- }, [columnSizingOverrides, defaultWidth, minWidth, setColumnSizingOverrides]);
82
+ cleanupRef.current = cleanup;
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ }, [defaultWidth, minWidth, setColumnSizingOverrides]); // columnSizingOverrides read via ref
62
85
  const getColumnWidth = useCallback((col) => {
63
86
  return columnSizingOverrides[col.columnId]?.widthPx
64
87
  ?? col.idealWidth
@@ -1,16 +1,20 @@
1
1
  import { useState, useCallback } from 'react';
2
+ /**
3
+ * Manages context menu position state for right-click menus.
4
+ * @returns Menu position, setter, right-click handler, and close handler.
5
+ */
2
6
  export function useContextMenu() {
3
- const [contextMenu, setContextMenu] = useState(null);
7
+ const [contextMenuPosition, setContextMenuPosition] = useState(null);
4
8
  const handleCellContextMenu = useCallback((e) => {
5
9
  e.preventDefault?.();
6
- setContextMenu({ x: e.clientX, y: e.clientY });
10
+ setContextMenuPosition({ x: e.clientX, y: e.clientY });
7
11
  }, []);
8
12
  const closeContextMenu = useCallback(() => {
9
- setContextMenu(null);
13
+ setContextMenuPosition(null);
10
14
  }, []);
11
15
  return {
12
- contextMenu,
13
- setContextMenu,
16
+ contextMenuPosition,
17
+ setContextMenuPosition,
14
18
  handleCellContextMenu,
15
19
  closeContextMenu,
16
20
  };