@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.
- package/README.md +42 -31
- package/dist/esm/components/BaseInlineCellEditor.js +112 -0
- package/dist/esm/components/CellErrorBoundary.js +43 -0
- package/dist/esm/components/EmptyState.js +19 -0
- package/dist/esm/components/GridContextMenu.js +4 -3
- package/dist/esm/components/MarchingAntsOverlay.js +6 -5
- package/dist/esm/components/OGridLayout.js +7 -6
- package/dist/esm/components/SideBar.js +66 -44
- package/dist/esm/constants.js +11 -0
- package/dist/esm/hooks/index.js +6 -0
- package/dist/esm/hooks/useActiveCell.js +25 -9
- package/dist/esm/hooks/useCellEditing.js +4 -0
- package/dist/esm/hooks/useCellSelection.js +7 -1
- package/dist/esm/hooks/useClipboard.js +36 -36
- package/dist/esm/hooks/useColumnHeaderFilterState.js +71 -119
- package/dist/esm/hooks/useColumnResize.js +27 -4
- package/dist/esm/hooks/useContextMenu.js +9 -5
- package/dist/esm/hooks/useDataGridState.js +110 -162
- package/dist/esm/hooks/useDateFilterState.js +34 -0
- package/dist/esm/hooks/useFillHandle.js +7 -2
- package/dist/esm/hooks/useFilterOptions.js +5 -5
- package/dist/esm/hooks/useKeyboardNavigation.js +18 -34
- package/dist/esm/hooks/useLatestRef.js +11 -0
- package/dist/esm/hooks/useMultiSelectFilterState.js +59 -0
- package/dist/esm/hooks/useOGrid.js +71 -18
- package/dist/esm/hooks/usePeopleFilterState.js +68 -0
- package/dist/esm/hooks/useRichSelectState.js +5 -0
- package/dist/esm/hooks/useRowSelection.js +14 -4
- package/dist/esm/hooks/useSideBarState.js +5 -0
- package/dist/esm/hooks/useTableLayout.js +77 -0
- package/dist/esm/hooks/useTextFilterState.js +25 -0
- package/dist/esm/hooks/useUndoRedo.js +6 -5
- package/dist/esm/index.js +7 -2
- package/dist/esm/utils/clientSideData.js +25 -12
- package/dist/esm/utils/columnUtils.js +6 -0
- package/dist/esm/utils/gridRowComparator.js +68 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/ogridHelpers.js +2 -1
- package/dist/esm/utils/paginationHelpers.js +7 -1
- package/dist/types/components/BaseInlineCellEditor.d.ts +33 -0
- package/dist/types/components/CellErrorBoundary.d.ts +25 -0
- package/dist/types/components/EmptyState.d.ts +26 -0
- package/dist/types/constants.d.ts +11 -0
- package/dist/types/hooks/index.d.ts +12 -1
- package/dist/types/hooks/useCellEditing.d.ts +4 -0
- package/dist/types/hooks/useCellSelection.d.ts +5 -0
- package/dist/types/hooks/useClipboard.d.ts +5 -0
- package/dist/types/hooks/useColumnHeaderFilterState.d.ts +1 -0
- package/dist/types/hooks/useColumnResize.d.ts +5 -0
- package/dist/types/hooks/useContextMenu.d.ts +6 -2
- package/dist/types/hooks/useDataGridState.d.ts +1 -0
- package/dist/types/hooks/useDateFilterState.d.ts +19 -0
- package/dist/types/hooks/useFillHandle.d.ts +5 -0
- package/dist/types/hooks/useKeyboardNavigation.d.ts +38 -25
- package/dist/types/hooks/useLatestRef.d.ts +6 -0
- package/dist/types/hooks/useMultiSelectFilterState.d.ts +24 -0
- package/dist/types/hooks/useOGrid.d.ts +30 -9
- package/dist/types/hooks/usePeopleFilterState.d.ts +25 -0
- package/dist/types/hooks/useRichSelectState.d.ts +5 -0
- package/dist/types/hooks/useRowSelection.d.ts +5 -0
- package/dist/types/hooks/useSideBarState.d.ts +5 -0
- package/dist/types/hooks/useTableLayout.d.ts +27 -0
- package/dist/types/hooks/useTextFilterState.d.ts +16 -0
- package/dist/types/hooks/useUndoRedo.d.ts +3 -1
- package/dist/types/index.d.ts +11 -4
- package/dist/types/types/columnTypes.d.ts +2 -3
- package/dist/types/types/dataGridTypes.d.ts +32 -4
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/gridRowComparator.d.ts +49 -0
- package/dist/types/utils/index.d.ts +2 -0
- 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 {
|
|
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 =
|
|
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
|
-
}, [
|
|
58
|
+
}, [getEffectiveRange, itemsRef, visibleColsRef]);
|
|
44
59
|
const handleCut = useCallback(() => {
|
|
45
|
-
if (
|
|
60
|
+
if (editableRef.current === false)
|
|
46
61
|
return;
|
|
47
|
-
const range =
|
|
48
|
-
|
|
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
|
-
}, [
|
|
72
|
+
}, [getEffectiveRange, handleCopy, editableRef, onCellValueChangedRef]);
|
|
66
73
|
const handlePaste = useCallback(async () => {
|
|
67
|
-
if (
|
|
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 =
|
|
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
|
-
}, [
|
|
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 {
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
multiSelectFilterState.handleApplyMultiSelect();
|
|
140
97
|
setFilterOpen(false);
|
|
141
|
-
}, [
|
|
98
|
+
}, [multiSelectFilterState]);
|
|
142
99
|
const handleTextApply = useCallback(() => {
|
|
143
|
-
|
|
100
|
+
textFilterState.handleTextApply();
|
|
144
101
|
setFilterOpen(false);
|
|
145
|
-
}, [
|
|
146
|
-
const handleTextClear = useCallback(() => setTempTextValue(''), []);
|
|
102
|
+
}, [textFilterState]);
|
|
147
103
|
const handleUserSelect = useCallback((user) => {
|
|
148
|
-
|
|
104
|
+
peopleFilterState.handleUserSelect(user);
|
|
149
105
|
setFilterOpen(false);
|
|
150
|
-
}, [
|
|
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
|
-
|
|
108
|
+
peopleFilterState.handleClearUser();
|
|
109
|
+
setFilterOpen(false);
|
|
110
|
+
}, [peopleFilterState]);
|
|
111
|
+
const handleDateApply = useCallback(() => {
|
|
112
|
+
dateFilterState.handleDateApply();
|
|
163
113
|
setFilterOpen(false);
|
|
164
|
-
}, [
|
|
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
|
-
:
|
|
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
|
|
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
|
-
|
|
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 [
|
|
7
|
+
const [contextMenuPosition, setContextMenuPosition] = useState(null);
|
|
4
8
|
const handleCellContextMenu = useCallback((e) => {
|
|
5
9
|
e.preventDefault?.();
|
|
6
|
-
|
|
10
|
+
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
|
7
11
|
}, []);
|
|
8
12
|
const closeContextMenu = useCallback(() => {
|
|
9
|
-
|
|
13
|
+
setContextMenuPosition(null);
|
|
10
14
|
}, []);
|
|
11
15
|
return {
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
contextMenuPosition,
|
|
17
|
+
setContextMenuPosition,
|
|
14
18
|
handleCellContextMenu,
|
|
15
19
|
closeContextMenu,
|
|
16
20
|
};
|