@alaarab/ogrid-react 2.0.0-beta

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 (88) hide show
  1. package/README.md +55 -0
  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 +35 -0
  6. package/dist/esm/components/MarchingAntsOverlay.js +110 -0
  7. package/dist/esm/components/OGridLayout.js +91 -0
  8. package/dist/esm/components/SideBar.js +122 -0
  9. package/dist/esm/components/StatusBar.js +6 -0
  10. package/dist/esm/hooks/index.js +25 -0
  11. package/dist/esm/hooks/useActiveCell.js +62 -0
  12. package/dist/esm/hooks/useCellEditing.js +15 -0
  13. package/dist/esm/hooks/useCellSelection.js +327 -0
  14. package/dist/esm/hooks/useClipboard.js +161 -0
  15. package/dist/esm/hooks/useColumnChooserState.js +62 -0
  16. package/dist/esm/hooks/useColumnHeaderFilterState.js +180 -0
  17. package/dist/esm/hooks/useColumnResize.js +92 -0
  18. package/dist/esm/hooks/useContextMenu.js +21 -0
  19. package/dist/esm/hooks/useDataGridState.js +313 -0
  20. package/dist/esm/hooks/useDateFilterState.js +34 -0
  21. package/dist/esm/hooks/useDebounce.js +35 -0
  22. package/dist/esm/hooks/useFillHandle.js +195 -0
  23. package/dist/esm/hooks/useFilterOptions.js +40 -0
  24. package/dist/esm/hooks/useInlineCellEditorState.js +44 -0
  25. package/dist/esm/hooks/useKeyboardNavigation.js +419 -0
  26. package/dist/esm/hooks/useLatestRef.js +11 -0
  27. package/dist/esm/hooks/useMultiSelectFilterState.js +59 -0
  28. package/dist/esm/hooks/useOGrid.js +465 -0
  29. package/dist/esm/hooks/usePeopleFilterState.js +68 -0
  30. package/dist/esm/hooks/useRichSelectState.js +58 -0
  31. package/dist/esm/hooks/useRowSelection.js +80 -0
  32. package/dist/esm/hooks/useSideBarState.js +39 -0
  33. package/dist/esm/hooks/useTableLayout.js +77 -0
  34. package/dist/esm/hooks/useTextFilterState.js +25 -0
  35. package/dist/esm/hooks/useUndoRedo.js +83 -0
  36. package/dist/esm/index.js +16 -0
  37. package/dist/esm/storybook/index.js +1 -0
  38. package/dist/esm/storybook/mockData.js +73 -0
  39. package/dist/esm/types/columnTypes.js +1 -0
  40. package/dist/esm/types/dataGridTypes.js +1 -0
  41. package/dist/esm/types/index.js +1 -0
  42. package/dist/esm/utils/dataGridViewModel.js +220 -0
  43. package/dist/esm/utils/gridRowComparator.js +2 -0
  44. package/dist/esm/utils/index.js +5 -0
  45. package/dist/types/components/BaseInlineCellEditor.d.ts +33 -0
  46. package/dist/types/components/CellErrorBoundary.d.ts +25 -0
  47. package/dist/types/components/EmptyState.d.ts +26 -0
  48. package/dist/types/components/GridContextMenu.d.ts +18 -0
  49. package/dist/types/components/MarchingAntsOverlay.d.ts +15 -0
  50. package/dist/types/components/OGridLayout.d.ts +37 -0
  51. package/dist/types/components/SideBar.d.ts +30 -0
  52. package/dist/types/components/StatusBar.d.ts +24 -0
  53. package/dist/types/hooks/index.d.ts +48 -0
  54. package/dist/types/hooks/useActiveCell.d.ts +13 -0
  55. package/dist/types/hooks/useCellEditing.d.ts +16 -0
  56. package/dist/types/hooks/useCellSelection.d.ts +22 -0
  57. package/dist/types/hooks/useClipboard.d.ts +30 -0
  58. package/dist/types/hooks/useColumnChooserState.d.ts +27 -0
  59. package/dist/types/hooks/useColumnHeaderFilterState.d.ts +73 -0
  60. package/dist/types/hooks/useColumnResize.d.ts +23 -0
  61. package/dist/types/hooks/useContextMenu.d.ts +19 -0
  62. package/dist/types/hooks/useDataGridState.d.ts +137 -0
  63. package/dist/types/hooks/useDateFilterState.d.ts +19 -0
  64. package/dist/types/hooks/useDebounce.d.ts +9 -0
  65. package/dist/types/hooks/useFillHandle.d.ts +33 -0
  66. package/dist/types/hooks/useFilterOptions.d.ts +16 -0
  67. package/dist/types/hooks/useInlineCellEditorState.d.ts +24 -0
  68. package/dist/types/hooks/useKeyboardNavigation.d.ts +47 -0
  69. package/dist/types/hooks/useLatestRef.d.ts +6 -0
  70. package/dist/types/hooks/useMultiSelectFilterState.d.ts +24 -0
  71. package/dist/types/hooks/useOGrid.d.ts +52 -0
  72. package/dist/types/hooks/usePeopleFilterState.d.ts +25 -0
  73. package/dist/types/hooks/useRichSelectState.d.ts +22 -0
  74. package/dist/types/hooks/useRowSelection.d.ts +22 -0
  75. package/dist/types/hooks/useSideBarState.d.ts +20 -0
  76. package/dist/types/hooks/useTableLayout.d.ts +27 -0
  77. package/dist/types/hooks/useTextFilterState.d.ts +16 -0
  78. package/dist/types/hooks/useUndoRedo.d.ts +23 -0
  79. package/dist/types/index.d.ts +23 -0
  80. package/dist/types/storybook/index.d.ts +2 -0
  81. package/dist/types/storybook/mockData.d.ts +37 -0
  82. package/dist/types/types/columnTypes.d.ts +25 -0
  83. package/dist/types/types/dataGridTypes.d.ts +152 -0
  84. package/dist/types/types/index.d.ts +3 -0
  85. package/dist/types/utils/dataGridViewModel.d.ts +161 -0
  86. package/dist/types/utils/gridRowComparator.d.ts +2 -0
  87. package/dist/types/utils/index.d.ts +6 -0
  88. package/package.json +46 -0
@@ -0,0 +1,195 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { normalizeSelectionRange } from '../types';
3
+ import { getCellValue, parseValue } from '../utils';
4
+ /** DOM attribute name for fill-drag range highlighting (same as cell selection drag). */
5
+ const DRAG_ATTR = 'data-drag-range';
6
+ /**
7
+ * Manages Excel-style fill handle drag-to-fill for cell ranges.
8
+ * @param params - Items, columns, selection range, editability, and value change callback.
9
+ * @returns Fill drag state, setter, and mousedown handler for the fill handle.
10
+ */
11
+ export function useFillHandle(params) {
12
+ const { items, visibleCols, editable, onCellValueChanged, selectionRange, setSelectionRange, setActiveCell, colOffset, wrapperRef, beginBatch, endBatch, } = params;
13
+ const [fillDrag, setFillDrag] = useState(null);
14
+ const fillDragEndRef = useRef({ endRow: 0, endCol: 0 });
15
+ const rafRef = useRef(0);
16
+ const liveFillRangeRef = useRef(null);
17
+ useEffect(() => {
18
+ if (!fillDrag || editable === false || !onCellValueChanged || !wrapperRef.current)
19
+ return;
20
+ fillDragEndRef.current = { endRow: fillDrag.startRow, endCol: fillDrag.startCol };
21
+ liveFillRangeRef.current = null;
22
+ const colOff = colOffset;
23
+ const applyDragAttrs = (range) => {
24
+ const wrapper = wrapperRef.current;
25
+ if (!wrapper)
26
+ return;
27
+ const minR = Math.min(range.startRow, range.endRow);
28
+ const maxR = Math.max(range.startRow, range.endRow);
29
+ const minC = Math.min(range.startCol, range.endCol);
30
+ const maxC = Math.max(range.startCol, range.endCol);
31
+ const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
32
+ for (let i = 0; i < cells.length; i++) {
33
+ const el = cells[i];
34
+ const r = parseInt(el.getAttribute('data-row-index'), 10);
35
+ const c = parseInt(el.getAttribute('data-col-index'), 10) - colOff;
36
+ const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
37
+ if (inRange) {
38
+ if (!el.hasAttribute(DRAG_ATTR))
39
+ el.setAttribute(DRAG_ATTR, '');
40
+ }
41
+ else {
42
+ if (el.hasAttribute(DRAG_ATTR))
43
+ el.removeAttribute(DRAG_ATTR);
44
+ }
45
+ }
46
+ };
47
+ const clearDragAttrs = () => {
48
+ const wrapper = wrapperRef.current;
49
+ if (!wrapper)
50
+ return;
51
+ const marked = wrapper.querySelectorAll(`[${DRAG_ATTR}]`);
52
+ for (let i = 0; i < marked.length; i++)
53
+ marked[i].removeAttribute(DRAG_ATTR);
54
+ };
55
+ let lastFillMousePos = null;
56
+ const resolveRange = (cx, cy) => {
57
+ const target = document.elementFromPoint(cx, cy);
58
+ const cell = target?.closest?.('[data-row-index][data-col-index]');
59
+ if (!cell || !wrapperRef.current?.contains(cell))
60
+ return null;
61
+ const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
62
+ const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
63
+ if (Number.isNaN(r) || Number.isNaN(c) || c < colOff)
64
+ return null;
65
+ const dataCol = c - colOff;
66
+ return normalizeSelectionRange({
67
+ startRow: fillDrag.startRow,
68
+ startCol: fillDrag.startCol,
69
+ endRow: r,
70
+ endCol: dataCol,
71
+ });
72
+ };
73
+ const onMove = (e) => {
74
+ lastFillMousePos = { cx: e.clientX, cy: e.clientY };
75
+ if (rafRef.current)
76
+ cancelAnimationFrame(rafRef.current);
77
+ rafRef.current = requestAnimationFrame(() => {
78
+ rafRef.current = 0;
79
+ if (!lastFillMousePos)
80
+ return;
81
+ const newRange = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
82
+ if (!newRange)
83
+ return;
84
+ // Skip if unchanged
85
+ const prev = liveFillRangeRef.current;
86
+ if (prev &&
87
+ prev.startRow === newRange.startRow &&
88
+ prev.startCol === newRange.startCol &&
89
+ prev.endRow === newRange.endRow &&
90
+ prev.endCol === newRange.endCol) {
91
+ return;
92
+ }
93
+ liveFillRangeRef.current = newRange;
94
+ fillDragEndRef.current = { endRow: newRange.endRow, endCol: newRange.endCol };
95
+ applyDragAttrs(newRange);
96
+ });
97
+ };
98
+ const onUp = () => {
99
+ if (rafRef.current) {
100
+ cancelAnimationFrame(rafRef.current);
101
+ rafRef.current = 0;
102
+ }
103
+ // Flush: resolve final position if RAF hasn't executed yet
104
+ if (lastFillMousePos) {
105
+ const flushed = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
106
+ if (flushed) {
107
+ liveFillRangeRef.current = flushed;
108
+ fillDragEndRef.current = { endRow: flushed.endRow, endCol: flushed.endCol };
109
+ }
110
+ }
111
+ clearDragAttrs();
112
+ const end = fillDragEndRef.current;
113
+ const norm = normalizeSelectionRange({
114
+ startRow: fillDrag.startRow,
115
+ startCol: fillDrag.startCol,
116
+ endRow: end.endRow,
117
+ endCol: end.endCol,
118
+ });
119
+ // Commit range to React state
120
+ setSelectionRange(norm);
121
+ setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + colOff });
122
+ // Apply fill values
123
+ const startItem = items[norm.startRow];
124
+ const startColDef = visibleCols[norm.startCol];
125
+ if (startItem && startColDef) {
126
+ const startValue = getCellValue(startItem, startColDef);
127
+ beginBatch?.();
128
+ for (let row = norm.startRow; row <= norm.endRow; row++) {
129
+ for (let col = norm.startCol; col <= norm.endCol; col++) {
130
+ if (row === fillDrag.startRow && col === fillDrag.startCol)
131
+ continue;
132
+ if (row >= items.length || col >= visibleCols.length)
133
+ continue;
134
+ const item = items[row];
135
+ const colDef = visibleCols[col];
136
+ const colEditable = colDef.editable === true ||
137
+ (typeof colDef.editable === 'function' && colDef.editable(item));
138
+ if (!colEditable)
139
+ continue;
140
+ const oldValue = getCellValue(item, colDef);
141
+ const result = parseValue(startValue, oldValue, item, colDef);
142
+ if (!result.valid)
143
+ continue;
144
+ onCellValueChanged({
145
+ item,
146
+ columnId: colDef.columnId,
147
+ oldValue,
148
+ newValue: result.value,
149
+ rowIndex: row,
150
+ });
151
+ }
152
+ }
153
+ endBatch?.();
154
+ }
155
+ setFillDrag(null);
156
+ liveFillRangeRef.current = null;
157
+ };
158
+ window.addEventListener('mousemove', onMove, true);
159
+ window.addEventListener('mouseup', onUp, true);
160
+ return () => {
161
+ window.removeEventListener('mousemove', onMove, true);
162
+ window.removeEventListener('mouseup', onUp, true);
163
+ if (rafRef.current)
164
+ cancelAnimationFrame(rafRef.current);
165
+ };
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ }, [
168
+ fillDrag,
169
+ editable,
170
+ colOffset,
171
+ items,
172
+ visibleCols,
173
+ setSelectionRange,
174
+ setActiveCell,
175
+ onCellValueChanged,
176
+ // wrapperRef excluded — refs are stable across renders
177
+ beginBatch,
178
+ endBatch,
179
+ ]);
180
+ // Ref mirror — keeps handleFillHandleMouseDown stable across selection changes
181
+ const selectionRangeRef = useRef(selectionRange);
182
+ selectionRangeRef.current = selectionRange;
183
+ const handleFillHandleMouseDown = useCallback((e) => {
184
+ e.preventDefault();
185
+ e.stopPropagation();
186
+ const range = selectionRangeRef.current;
187
+ if (!range)
188
+ return;
189
+ setFillDrag({
190
+ startRow: range.startRow,
191
+ startCol: range.startCol,
192
+ });
193
+ }, []);
194
+ return { fillDrag, setFillDrag, handleFillHandleMouseDown };
195
+ }
@@ -0,0 +1,40 @@
1
+ import { useState, useEffect, useCallback, useMemo } from 'react';
2
+ /**
3
+ * Load filter options for the given fields from a data source.
4
+ *
5
+ * Accepts `IDataSource<T>` or a plain `{ fetchFilterOptions }` object.
6
+ */
7
+ export function useFilterOptions(dataSource, fields) {
8
+ const [filterOptions, setFilterOptions] = useState({});
9
+ const [loadingOptions, setLoadingOptions] = useState({});
10
+ const fieldsKey = useMemo(() => [...fields].sort().join(','), [fields]);
11
+ const load = useCallback(async () => {
12
+ const fetcher = 'fetchFilterOptions' in dataSource && typeof dataSource.fetchFilterOptions === 'function'
13
+ ? dataSource.fetchFilterOptions.bind(dataSource)
14
+ : undefined;
15
+ if (!fetcher) {
16
+ setFilterOptions({});
17
+ setLoadingOptions({});
18
+ return;
19
+ }
20
+ const loading = {};
21
+ fields.forEach((f) => { loading[f] = true; });
22
+ setLoadingOptions(loading);
23
+ const results = {};
24
+ await Promise.all(fields.map(async (field) => {
25
+ try {
26
+ results[field] = await fetcher(field);
27
+ }
28
+ catch {
29
+ results[field] = [];
30
+ }
31
+ }));
32
+ setFilterOptions(results);
33
+ setLoadingOptions({});
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
+ }, [dataSource, fieldsKey]);
36
+ useEffect(() => {
37
+ load().catch(() => { });
38
+ }, [load]);
39
+ return { filterOptions, loadingOptions };
40
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Headless inline cell editor state for Fluent, Material, and Radix InlineCellEditor.
3
+ * UI packages use this hook and render only the framework input (Input, TextField, select, Checkbox).
4
+ */
5
+ import { useState, useCallback } from 'react';
6
+ /**
7
+ * Returns localValue/setLocalValue (for text), handleKeyDown (Escape cancel, Enter commit for text),
8
+ * handleBlur (commit on blur for text), commit(value), cancel(). UI renders only the input.
9
+ */
10
+ export function useInlineCellEditorState(params) {
11
+ const { value, editorType, onCommit, onCancel } = params;
12
+ const [localValue, setLocalValue] = useState(value !== null && value !== undefined ? String(value) : '');
13
+ const commit = useCallback((v) => {
14
+ onCommit(v);
15
+ }, [onCommit]);
16
+ const cancel = useCallback(() => {
17
+ onCancel();
18
+ }, [onCancel]);
19
+ const handleKeyDown = useCallback((e) => {
20
+ if (e.key === 'Escape') {
21
+ e.preventDefault();
22
+ e.stopPropagation(); // Don't let the grid handler clear selection on Escape
23
+ cancel();
24
+ }
25
+ if (e.key === 'Enter' && editorType === 'text') {
26
+ e.preventDefault();
27
+ e.stopPropagation(); // Don't let the grid handler re-open an editor
28
+ commit(localValue);
29
+ }
30
+ }, [cancel, commit, localValue, editorType]);
31
+ const handleBlur = useCallback(() => {
32
+ if (editorType === 'text') {
33
+ commit(localValue);
34
+ }
35
+ }, [editorType, localValue, commit]);
36
+ return {
37
+ localValue,
38
+ setLocalValue,
39
+ handleKeyDown,
40
+ handleBlur,
41
+ commit,
42
+ cancel,
43
+ };
44
+ }