@alaarab/ogrid-core 1.8.0 → 1.8.2
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.
|
@@ -33,9 +33,11 @@ const toolbarSectionStyle = {
|
|
|
33
33
|
alignItems: 'center',
|
|
34
34
|
gap: 8,
|
|
35
35
|
};
|
|
36
|
-
/** Secondary toolbar row
|
|
36
|
+
/** Secondary toolbar row (e.g. active filter chips). Matches toolbar strip styling. */
|
|
37
37
|
const toolbarBelowStyle = {
|
|
38
38
|
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
39
|
+
padding: '6px 12px',
|
|
40
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
39
41
|
};
|
|
40
42
|
const footerStripStyle = {
|
|
41
43
|
borderTop: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import { useState, useLayoutEffect } from 'react';
|
|
1
|
+
import { useState, useLayoutEffect, useCallback, useRef } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Tracks the active cell for keyboard navigation.
|
|
4
4
|
* When wrapperRef and editingCell are provided, scrolls the active cell into view when it changes (and not editing).
|
|
5
5
|
*/
|
|
6
6
|
export function useActiveCell(wrapperRef, editingCell) {
|
|
7
|
-
const [activeCell,
|
|
7
|
+
const [activeCell, _setActiveCell] = useState(null);
|
|
8
|
+
const activeCellRef = useRef(activeCell);
|
|
9
|
+
activeCellRef.current = activeCell;
|
|
10
|
+
// Deduplicating setter — skips state update (and all downstream effects) when
|
|
11
|
+
// the cell coordinates haven't actually changed. This prevents re-renders when
|
|
12
|
+
// rapidly clicking the same cell.
|
|
13
|
+
const setActiveCell = useCallback((cell) => {
|
|
14
|
+
const prev = activeCellRef.current;
|
|
15
|
+
if (prev === cell)
|
|
16
|
+
return;
|
|
17
|
+
if (prev && cell && prev.rowIndex === cell.rowIndex && prev.columnIndex === cell.columnIndex)
|
|
18
|
+
return;
|
|
19
|
+
_setActiveCell(cell);
|
|
20
|
+
}, []);
|
|
8
21
|
// useLayoutEffect ensures focus moves synchronously before the browser can
|
|
9
22
|
// reset focus to body (fixes left/right arrow navigation losing focus)
|
|
10
23
|
useLayoutEffect(() => {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
2
|
import { normalizeSelectionRange } from '../types';
|
|
3
|
+
/** Compares two selection ranges by value. */
|
|
4
|
+
function rangesEqual(a, b) {
|
|
5
|
+
if (a === b)
|
|
6
|
+
return true;
|
|
7
|
+
if (!a || !b)
|
|
8
|
+
return false;
|
|
9
|
+
return a.startRow === b.startRow && a.endRow === b.endRow &&
|
|
10
|
+
a.startCol === b.startCol && a.endCol === b.endCol;
|
|
11
|
+
}
|
|
3
12
|
/** DOM attribute name used for drag-range highlighting (bypasses React). */
|
|
4
13
|
const DRAG_ATTR = 'data-drag-range';
|
|
5
14
|
/** Auto-scroll config */
|
|
@@ -14,9 +23,11 @@ function autoScrollSpeed(distance) {
|
|
|
14
23
|
}
|
|
15
24
|
export function useCellSelection(params) {
|
|
16
25
|
const { colOffset, rowCount, visibleColCount, setActiveCell, wrapperRef } = params;
|
|
17
|
-
const [selectionRange,
|
|
26
|
+
const [selectionRange, _setSelectionRange] = useState(null);
|
|
18
27
|
const isDraggingRef = useRef(false);
|
|
19
28
|
const [isDragging, setIsDragging] = useState(false);
|
|
29
|
+
/** True once a mousemove has been seen during the current drag gesture. */
|
|
30
|
+
const dragMovedRef = useRef(false);
|
|
20
31
|
const dragStartRef = useRef(null);
|
|
21
32
|
const rafRef = useRef(0);
|
|
22
33
|
/** Live drag range kept in a ref — only committed to React state on mouseup. */
|
|
@@ -27,6 +38,12 @@ export function useCellSelection(params) {
|
|
|
27
38
|
// without adding selectionRange to its useCallback deps (keeps it stable).
|
|
28
39
|
const selectionRangeRef = useRef(selectionRange);
|
|
29
40
|
selectionRangeRef.current = selectionRange;
|
|
41
|
+
// Deduplicating setter — skips re-render when the range hasn't actually changed.
|
|
42
|
+
const setSelectionRange = useCallback((next) => {
|
|
43
|
+
if (rangesEqual(selectionRangeRef.current, next))
|
|
44
|
+
return;
|
|
45
|
+
_setSelectionRange(next);
|
|
46
|
+
}, []);
|
|
30
47
|
const handleCellMouseDown = useCallback((e, rowIndex, globalColIndex) => {
|
|
31
48
|
// Only handle primary (left) button — let middle-click scroll and right-click context menu work natively
|
|
32
49
|
if (e.button !== 0)
|
|
@@ -48,6 +65,7 @@ export function useCellSelection(params) {
|
|
|
48
65
|
}
|
|
49
66
|
else {
|
|
50
67
|
dragStartRef.current = { row: rowIndex, col: dataColIndex };
|
|
68
|
+
dragMovedRef.current = false;
|
|
51
69
|
const initial = {
|
|
52
70
|
startRow: rowIndex,
|
|
53
71
|
startCol: dataColIndex,
|
|
@@ -57,8 +75,10 @@ export function useCellSelection(params) {
|
|
|
57
75
|
setSelectionRange(initial);
|
|
58
76
|
liveDragRangeRef.current = initial;
|
|
59
77
|
setActiveCell({ rowIndex, columnIndex: globalColIndex });
|
|
78
|
+
// Mark drag as "started" but don't set isDragging state yet —
|
|
79
|
+
// setIsDragging(true) is deferred to the first mousemove to avoid
|
|
80
|
+
// a true→false toggle on simple clicks (which causes 2 extra renders).
|
|
60
81
|
isDraggingRef.current = true;
|
|
61
|
-
setIsDragging(true);
|
|
62
82
|
}
|
|
63
83
|
}, [colOffset, setActiveCell]);
|
|
64
84
|
const handleSelectAllCells = useCallback(() => {
|
|
@@ -204,6 +224,12 @@ export function useCellSelection(params) {
|
|
|
204
224
|
const onMove = (e) => {
|
|
205
225
|
if (!isDraggingRef.current || !dragStartRef.current)
|
|
206
226
|
return;
|
|
227
|
+
// Promote to a real drag on first mousemove (deferred from mouseDown
|
|
228
|
+
// to avoid a true→false toggle on simple clicks).
|
|
229
|
+
if (!dragMovedRef.current) {
|
|
230
|
+
dragMovedRef.current = true;
|
|
231
|
+
setIsDragging(true);
|
|
232
|
+
}
|
|
207
233
|
// Always store latest position so mouseUp can flush if RAF hasn't executed
|
|
208
234
|
lastMousePosRef.current = { cx: e.clientX, cy: e.clientY };
|
|
209
235
|
// Update auto-scroll based on mouse proximity to edges
|
|
@@ -242,29 +268,35 @@ export function useCellSelection(params) {
|
|
|
242
268
|
rafRef.current = 0;
|
|
243
269
|
}
|
|
244
270
|
isDraggingRef.current = false;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
271
|
+
const wasDrag = dragMovedRef.current;
|
|
272
|
+
if (wasDrag) {
|
|
273
|
+
// Flush: if the last RAF hasn't executed yet, resolve the range now from the
|
|
274
|
+
// last known mouse position so the final committed range is always accurate.
|
|
275
|
+
const pos = lastMousePosRef.current;
|
|
276
|
+
if (pos) {
|
|
277
|
+
const flushed = resolveRange(pos.cx, pos.cy);
|
|
278
|
+
if (flushed)
|
|
279
|
+
liveDragRangeRef.current = flushed;
|
|
280
|
+
}
|
|
281
|
+
// Commit final range to React state (triggers a single re-render)
|
|
282
|
+
const finalRange = liveDragRangeRef.current;
|
|
283
|
+
if (finalRange) {
|
|
284
|
+
setSelectionRange(finalRange);
|
|
285
|
+
setActiveCell({
|
|
286
|
+
rowIndex: finalRange.endRow,
|
|
287
|
+
columnIndex: finalRange.endCol + colOff,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
261
290
|
}
|
|
291
|
+
// For simple clicks (no drag movement), mouseDown already set
|
|
292
|
+
// selectionRange + activeCell — skip redundant state updates.
|
|
262
293
|
// Clean up DOM attributes — React will apply CSS-module classes on the same paint
|
|
263
294
|
clearDragAttrs();
|
|
264
295
|
liveDragRangeRef.current = null;
|
|
265
296
|
lastMousePosRef.current = null;
|
|
266
297
|
dragStartRef.current = null;
|
|
267
|
-
|
|
298
|
+
if (wasDrag)
|
|
299
|
+
setIsDragging(false);
|
|
268
300
|
};
|
|
269
301
|
window.addEventListener('mousemove', onMove, true);
|
|
270
302
|
window.addEventListener('mouseup', onUp, true);
|
package/package.json
CHANGED