@alaarab/ogrid-react 2.1.2 → 2.1.4

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 (72) hide show
  1. package/dist/esm/index.js +7233 -26
  2. package/package.json +7 -4
  3. package/dist/esm/components/BaseColumnHeaderMenu.js +0 -78
  4. package/dist/esm/components/BaseDropIndicator.js +0 -4
  5. package/dist/esm/components/BaseEmptyState.js +0 -4
  6. package/dist/esm/components/BaseInlineCellEditor.js +0 -167
  7. package/dist/esm/components/BaseLoadingOverlay.js +0 -4
  8. package/dist/esm/components/CellErrorBoundary.js +0 -43
  9. package/dist/esm/components/ColumnChooserProps.js +0 -6
  10. package/dist/esm/components/ColumnHeaderFilterContent.js +0 -33
  11. package/dist/esm/components/ColumnHeaderFilterRenderers.js +0 -67
  12. package/dist/esm/components/EmptyState.js +0 -19
  13. package/dist/esm/components/GridContextMenu.js +0 -35
  14. package/dist/esm/components/MarchingAntsOverlay.js +0 -90
  15. package/dist/esm/components/OGridLayout.js +0 -136
  16. package/dist/esm/components/PaginationControlsProps.js +0 -6
  17. package/dist/esm/components/SideBar.js +0 -123
  18. package/dist/esm/components/StatusBar.js +0 -6
  19. package/dist/esm/components/createOGrid.js +0 -19
  20. package/dist/esm/constants/domHelpers.js +0 -16
  21. package/dist/esm/hooks/index.js +0 -43
  22. package/dist/esm/hooks/useActiveCell.js +0 -75
  23. package/dist/esm/hooks/useCellEditing.js +0 -15
  24. package/dist/esm/hooks/useCellSelection.js +0 -389
  25. package/dist/esm/hooks/useClipboard.js +0 -106
  26. package/dist/esm/hooks/useColumnChooserState.js +0 -74
  27. package/dist/esm/hooks/useColumnHeaderFilterState.js +0 -191
  28. package/dist/esm/hooks/useColumnHeaderMenuState.js +0 -106
  29. package/dist/esm/hooks/useColumnMeta.js +0 -61
  30. package/dist/esm/hooks/useColumnPinning.js +0 -67
  31. package/dist/esm/hooks/useColumnReorder.js +0 -143
  32. package/dist/esm/hooks/useColumnResize.js +0 -127
  33. package/dist/esm/hooks/useContextMenu.js +0 -21
  34. package/dist/esm/hooks/useDataGridContextMenu.js +0 -24
  35. package/dist/esm/hooks/useDataGridEditing.js +0 -56
  36. package/dist/esm/hooks/useDataGridInteraction.js +0 -109
  37. package/dist/esm/hooks/useDataGridLayout.js +0 -172
  38. package/dist/esm/hooks/useDataGridState.js +0 -169
  39. package/dist/esm/hooks/useDataGridTableOrchestration.js +0 -199
  40. package/dist/esm/hooks/useDateFilterState.js +0 -34
  41. package/dist/esm/hooks/useDebounce.js +0 -35
  42. package/dist/esm/hooks/useFillHandle.js +0 -200
  43. package/dist/esm/hooks/useFilterOptions.js +0 -55
  44. package/dist/esm/hooks/useInlineCellEditorState.js +0 -38
  45. package/dist/esm/hooks/useKeyboardNavigation.js +0 -261
  46. package/dist/esm/hooks/useLatestRef.js +0 -11
  47. package/dist/esm/hooks/useListVirtualizer.js +0 -29
  48. package/dist/esm/hooks/useMultiSelectFilterState.js +0 -59
  49. package/dist/esm/hooks/useOGrid.js +0 -355
  50. package/dist/esm/hooks/useOGridDataFetching.js +0 -74
  51. package/dist/esm/hooks/useOGridFilters.js +0 -59
  52. package/dist/esm/hooks/useOGridPagination.js +0 -24
  53. package/dist/esm/hooks/useOGridSorting.js +0 -24
  54. package/dist/esm/hooks/usePaginationControls.js +0 -16
  55. package/dist/esm/hooks/usePeopleFilterState.js +0 -73
  56. package/dist/esm/hooks/useRichSelectState.js +0 -60
  57. package/dist/esm/hooks/useRowSelection.js +0 -69
  58. package/dist/esm/hooks/useSelectState.js +0 -62
  59. package/dist/esm/hooks/useShallowEqualMemo.js +0 -14
  60. package/dist/esm/hooks/useSideBarState.js +0 -39
  61. package/dist/esm/hooks/useTableLayout.js +0 -69
  62. package/dist/esm/hooks/useTextFilterState.js +0 -25
  63. package/dist/esm/hooks/useUndoRedo.js +0 -84
  64. package/dist/esm/hooks/useVirtualScroll.js +0 -69
  65. package/dist/esm/storybook/index.js +0 -1
  66. package/dist/esm/storybook/mockData.js +0 -73
  67. package/dist/esm/types/columnTypes.js +0 -1
  68. package/dist/esm/types/dataGridTypes.js +0 -1
  69. package/dist/esm/types/index.js +0 -1
  70. package/dist/esm/utils/dataGridViewModel.js +0 -54
  71. package/dist/esm/utils/gridRowComparator.js +0 -2
  72. package/dist/esm/utils/index.js +0 -5
@@ -1,191 +0,0 @@
1
- /**
2
- * Headless column header filter state and handlers for Fluent, Material, and Radix.
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.
5
- */
6
- import { useState, useCallback, useRef, useEffect, useMemo, } from 'react';
7
- import { useTextFilterState } from './useTextFilterState';
8
- import { useMultiSelectFilterState } from './useMultiSelectFilterState';
9
- import { usePeopleFilterState } from './usePeopleFilterState';
10
- import { useDateFilterState } from './useDateFilterState';
11
- const EMPTY_OPTIONS = [];
12
- export function useColumnHeaderFilterState(params) {
13
- const { filterType, onSort, selectedValues, onFilterChange, options, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, dateValue, onDateChange, } = params;
14
- const safeSelectedValues = selectedValues ?? EMPTY_OPTIONS;
15
- // Shared state
16
- const headerRef = useRef(null);
17
- const popoverRef = useRef(null);
18
- const [isFilterOpen, setFilterOpen] = useState(false);
19
- const [popoverPosition, setPopoverPosition] = useState(null);
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
45
- useEffect(() => {
46
- if (!isFilterOpen) {
47
- setPopoverPosition(null);
48
- }
49
- }, [isFilterOpen]);
50
- // Click outside and Escape to close
51
- useEffect(() => {
52
- if (!isFilterOpen)
53
- return;
54
- const handleClickOutside = (e) => {
55
- const target = e.target;
56
- if (popoverRef.current &&
57
- !popoverRef.current.contains(target) &&
58
- headerRef.current &&
59
- !headerRef.current.contains(target)) {
60
- setFilterOpen(false);
61
- }
62
- };
63
- const handleKeyDown = (e) => {
64
- if (e.key === 'Escape' || e.key === 'Esc') {
65
- e.preventDefault();
66
- e.stopPropagation();
67
- setFilterOpen(false);
68
- }
69
- };
70
- const timeoutId = setTimeout(() => document.addEventListener('mousedown', handleClickOutside), 0);
71
- document.addEventListener('keydown', handleKeyDown, true);
72
- return () => {
73
- clearTimeout(timeoutId);
74
- document.removeEventListener('mousedown', handleClickOutside);
75
- document.removeEventListener('keydown', handleKeyDown, true);
76
- };
77
- }, [isFilterOpen]);
78
- // Shared handlers
79
- const handleFilterIconClick = useCallback((e) => {
80
- e.stopPropagation();
81
- e.preventDefault();
82
- setFilterOpen((prev) => {
83
- if (!prev && headerRef.current) {
84
- const rect = headerRef.current.getBoundingClientRect();
85
- setPopoverPosition({ top: rect.bottom + 4, left: rect.left });
86
- }
87
- return !prev;
88
- });
89
- }, []);
90
- const handleSortClick = useCallback((e) => {
91
- e.stopPropagation();
92
- onSort?.();
93
- }, [onSort]);
94
- // Destructure stable callbacks from sub-hooks before using as deps
95
- const { handleApplyMultiSelect: _applyMultiSelect } = multiSelectFilterState;
96
- const { handleTextApply: _textApply, handleTextClear: _textClear } = textFilterState;
97
- const { handleUserSelect: _userSelect, handleClearUser: _clearUser } = peopleFilterState;
98
- const { handleDateApply: _dateApply } = dateFilterState;
99
- // Wrap sub-hook handlers to close popover
100
- const handleApplyMultiSelect = useCallback(() => {
101
- _applyMultiSelect();
102
- setFilterOpen(false);
103
- }, [_applyMultiSelect]);
104
- const handleTextApply = useCallback(() => {
105
- _textApply();
106
- setFilterOpen(false);
107
- }, [_textApply]);
108
- // Clear immediately commits an empty value and closes the popover (no 2-step clear required)
109
- const handleTextClear = useCallback(() => {
110
- _textClear();
111
- onTextChange?.('');
112
- setFilterOpen(false);
113
- }, [_textClear, onTextChange]);
114
- const handleUserSelect = useCallback((user) => {
115
- _userSelect(user);
116
- setFilterOpen(false);
117
- }, [_userSelect]);
118
- const handleClearUser = useCallback(() => {
119
- _clearUser();
120
- setFilterOpen(false);
121
- }, [_clearUser]);
122
- const handleDateApply = useCallback(() => {
123
- _dateApply();
124
- setFilterOpen(false);
125
- }, [_dateApply]);
126
- // Event propagation stoppers
127
- const handlePopoverClick = useCallback((e) => e.stopPropagation(), []);
128
- const handleInputFocus = useCallback((e) => e.stopPropagation(), []);
129
- const handleInputMouseDown = useCallback((e) => e.stopPropagation(), []);
130
- const handleInputClick = useCallback((e) => e.stopPropagation(), []);
131
- const handleInputKeyDown = useCallback((e) => {
132
- if (e.key !== 'Escape' && e.key !== 'Esc')
133
- e.stopPropagation();
134
- }, []);
135
- // Compute hasActiveFilter from all sub-hooks
136
- const hasActiveFilter = useMemo(() => {
137
- if (filterType === 'multiSelect')
138
- return safeSelectedValues.length > 0;
139
- if (filterType === 'text')
140
- return !!textValue.trim();
141
- if (filterType === 'people')
142
- return !!selectedUser;
143
- if (filterType === 'date')
144
- return !!(dateValue?.from || dateValue?.to);
145
- return false;
146
- }, [filterType, safeSelectedValues, textValue, selectedUser, dateValue]);
147
- return {
148
- headerRef,
149
- popoverRef,
150
- peopleInputRef: peopleFilterState.peopleInputRef,
151
- isFilterOpen,
152
- setFilterOpen,
153
- tempSelected: multiSelectFilterState.tempSelected,
154
- setTempSelected: multiSelectFilterState.setTempSelected,
155
- tempTextValue: textFilterState.tempTextValue,
156
- setTempTextValue: textFilterState.setTempTextValue,
157
- searchText: multiSelectFilterState.searchText,
158
- setSearchText: multiSelectFilterState.setSearchText,
159
- debouncedSearchText: multiSelectFilterState.debouncedSearchText,
160
- filteredOptions: multiSelectFilterState.filteredOptions,
161
- peopleSuggestions: peopleFilterState.peopleSuggestions,
162
- isPeopleLoading: peopleFilterState.isPeopleLoading,
163
- peopleSearchText: peopleFilterState.peopleSearchText,
164
- setPeopleSearchText: peopleFilterState.setPeopleSearchText,
165
- tempDateFrom: dateFilterState.tempDateFrom,
166
- setTempDateFrom: dateFilterState.setTempDateFrom,
167
- tempDateTo: dateFilterState.tempDateTo,
168
- setTempDateTo: dateFilterState.setTempDateTo,
169
- hasActiveFilter,
170
- popoverPosition,
171
- handlers: {
172
- handleFilterIconClick,
173
- handleApplyMultiSelect,
174
- handleTextApply,
175
- handleTextClear,
176
- handleUserSelect,
177
- handleClearUser,
178
- handleDateApply,
179
- handleDateClear: dateFilterState.handleDateClear,
180
- handleCheckboxChange: multiSelectFilterState.handleCheckboxChange,
181
- handleSelectAll: multiSelectFilterState.handleSelectAll,
182
- handleClearSelection: multiSelectFilterState.handleClearSelection,
183
- handlePopoverClick,
184
- handleInputFocus,
185
- handleInputMouseDown,
186
- handleInputClick,
187
- handleInputKeyDown,
188
- handleSortClick,
189
- },
190
- };
191
- }
@@ -1,106 +0,0 @@
1
- import { useState, useCallback } from 'react';
2
- import { measureColumnContentWidth } from '../utils';
3
- /**
4
- * Manages state for the column header menu (pin, sort, autosize actions).
5
- * Tracks which column's menu is open, anchor element, and action handlers.
6
- */
7
- export function useColumnHeaderMenuState(params) {
8
- const { pinnedColumns, onPinColumn, onUnpinColumn, sortBy, sortDirection, onColumnSort, onColumnResized, onAutosizeColumn, columns, } = params;
9
- const [isOpen, setIsOpen] = useState(false);
10
- const [openForColumn, setOpenForColumn] = useState(null);
11
- const [anchorElement, setAnchorElement] = useState(null);
12
- const open = useCallback((columnId, anchorEl) => {
13
- setOpenForColumn(columnId);
14
- setAnchorElement(anchorEl);
15
- setIsOpen(true);
16
- }, []);
17
- const close = useCallback(() => {
18
- setIsOpen(false);
19
- setOpenForColumn(null);
20
- setAnchorElement(null);
21
- }, []);
22
- const currentPinState = openForColumn ? pinnedColumns[openForColumn] : undefined;
23
- const canPinLeft = currentPinState !== 'left';
24
- const canPinRight = currentPinState !== 'right';
25
- const canUnpin = !!currentPinState;
26
- const currentColumn = columns.find((c) => c.columnId === openForColumn);
27
- const currentSort = openForColumn === sortBy ? sortDirection : null;
28
- const isSortable = currentColumn?.sortable !== false;
29
- const isResizable = currentColumn?.resizable !== false;
30
- const handlePinLeft = useCallback(() => {
31
- if (openForColumn && canPinLeft) {
32
- onPinColumn(openForColumn, 'left');
33
- close();
34
- }
35
- }, [openForColumn, canPinLeft, onPinColumn, close]);
36
- const handlePinRight = useCallback(() => {
37
- if (openForColumn && canPinRight) {
38
- onPinColumn(openForColumn, 'right');
39
- close();
40
- }
41
- }, [openForColumn, canPinRight, onPinColumn, close]);
42
- const handleUnpin = useCallback(() => {
43
- if (openForColumn && canUnpin) {
44
- onUnpinColumn(openForColumn);
45
- close();
46
- }
47
- }, [openForColumn, canUnpin, onUnpinColumn, close]);
48
- const handleSortAsc = useCallback(() => {
49
- if (openForColumn && isSortable) {
50
- onColumnSort(openForColumn, 'asc');
51
- close();
52
- }
53
- }, [openForColumn, isSortable, onColumnSort, close]);
54
- const handleSortDesc = useCallback(() => {
55
- if (openForColumn && isSortable) {
56
- onColumnSort(openForColumn, 'desc');
57
- close();
58
- }
59
- }, [openForColumn, isSortable, onColumnSort, close]);
60
- const handleClearSort = useCallback(() => {
61
- if (openForColumn && isSortable) {
62
- onColumnSort(openForColumn, null);
63
- close();
64
- }
65
- }, [openForColumn, isSortable, onColumnSort, close]);
66
- const handleAutosizeThis = useCallback(() => {
67
- const resizer = onAutosizeColumn ?? onColumnResized;
68
- if (!openForColumn || !resizer || !isResizable)
69
- return;
70
- const col = columns.find((c) => c.columnId === openForColumn);
71
- resizer(openForColumn, measureColumnContentWidth(openForColumn, col?.minWidth));
72
- close();
73
- }, [openForColumn, onAutosizeColumn, onColumnResized, isResizable, columns, close]);
74
- const handleAutosizeAll = useCallback(() => {
75
- const resizer = onAutosizeColumn ?? onColumnResized;
76
- if (!resizer)
77
- return;
78
- columns.forEach((col) => {
79
- if (col.resizable === false)
80
- return;
81
- resizer(col.columnId, measureColumnContentWidth(col.columnId, col.minWidth));
82
- });
83
- close();
84
- }, [columns, onAutosizeColumn, onColumnResized, close]);
85
- return {
86
- isOpen,
87
- openForColumn,
88
- anchorElement,
89
- open,
90
- close,
91
- handlePinLeft,
92
- handlePinRight,
93
- handleUnpin,
94
- handleSortAsc,
95
- handleSortDesc,
96
- handleClearSort,
97
- handleAutosizeThis,
98
- handleAutosizeAll,
99
- canPinLeft,
100
- canPinRight,
101
- canUnpin,
102
- currentSort,
103
- isSortable,
104
- isResizable,
105
- };
106
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * Shared hook that pre-computes per-column styles and class names for DataGridTable.
3
- * Extracted from Radix/Fluent/Material DataGridTable to avoid duplication.
4
- *
5
- * @param params.addStickyPosition - When true, adds `position: 'sticky'` inline for pinned columns.
6
- * This is needed by Fluent UI whose `TableCell` injects atomic `position: relative` via CSS-in-JS,
7
- * overriding the shared `.pinnedColLeft { position: sticky }` class. Radix/Material don't need it.
8
- */
9
- import { useMemo } from 'react';
10
- import { DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
11
- /**
12
- * Computes per-column styles and class names once per render, avoiding per-cell object creation.
13
- */
14
- export function useColumnMeta(params) {
15
- const { visibleCols, getColumnWidth, columnSizingOverrides, measuredColumnWidths, pinnedColumns, leftOffsets, rightOffsets, pinnedColLeftClass, pinnedColRightClass, addStickyPosition = false, } = params;
16
- return useMemo(() => {
17
- const cellStyles = {};
18
- const cellClasses = {};
19
- const hdrStyles = {};
20
- const hdrClasses = {};
21
- for (let i = 0; i < visibleCols.length; i++) {
22
- const col = visibleCols[i];
23
- const columnWidth = getColumnWidth(col);
24
- const hasExplicitWidth = !!(columnSizingOverrides[col.columnId] || col.idealWidth != null || col.defaultWidth != null);
25
- const isPinnedLeft = pinnedColumns[col.columnId] === 'left';
26
- const isPinnedRight = pinnedColumns[col.columnId] === 'right';
27
- const isPinned = isPinnedLeft || isPinnedRight;
28
- const hasResizeOverride = !!columnSizingOverrides[col.columnId];
29
- const measuredW = measuredColumnWidths[col.columnId];
30
- const baseMinWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
31
- const effectiveMinWidth = hasResizeOverride ? columnWidth : Math.max(baseMinWidth, measuredW ?? 0);
32
- const stickyOverride = addStickyPosition && isPinned ? { position: 'sticky' } : undefined;
33
- cellStyles[col.columnId] = {
34
- minWidth: effectiveMinWidth,
35
- width: hasExplicitWidth ? columnWidth : undefined,
36
- maxWidth: hasExplicitWidth ? columnWidth : undefined,
37
- textAlign: col.type === 'numeric' ? 'right' : col.type === 'boolean' ? 'center' : undefined,
38
- ...stickyOverride,
39
- ...(isPinnedLeft && leftOffsets[col.columnId] != null ? { left: leftOffsets[col.columnId] } : undefined),
40
- ...(isPinnedRight && rightOffsets[col.columnId] != null ? { right: rightOffsets[col.columnId] } : undefined),
41
- };
42
- hdrStyles[col.columnId] = {
43
- minWidth: effectiveMinWidth,
44
- width: hasExplicitWidth ? columnWidth : undefined,
45
- maxWidth: hasExplicitWidth ? columnWidth : undefined,
46
- ...stickyOverride,
47
- ...(isPinnedLeft && leftOffsets[col.columnId] != null ? { left: leftOffsets[col.columnId] } : undefined),
48
- ...(isPinnedRight && rightOffsets[col.columnId] != null ? { right: rightOffsets[col.columnId] } : undefined),
49
- };
50
- const parts = [];
51
- if (isPinnedLeft)
52
- parts.push(pinnedColLeftClass);
53
- if (isPinnedRight)
54
- parts.push(pinnedColRightClass);
55
- const cn = parts.join(' ');
56
- cellClasses[col.columnId] = cn;
57
- hdrClasses[col.columnId] = cn;
58
- }
59
- return { cellStyles, cellClasses, hdrStyles, hdrClasses };
60
- }, [visibleCols, getColumnWidth, columnSizingOverrides, measuredColumnWidths, pinnedColumns, leftOffsets, rightOffsets, pinnedColLeftClass, pinnedColRightClass, addStickyPosition]);
61
- }
@@ -1,67 +0,0 @@
1
- import { useState, useCallback } from 'react';
2
- /**
3
- * Manages column pinning state (left/right sticky positioning).
4
- * Supports controlled and uncontrolled modes.
5
- * Initializes from column.pinned definitions and pinnedColumns prop.
6
- */
7
- export function useColumnPinning(params) {
8
- const { columns, pinnedColumns: controlledPinnedColumns, onColumnPinned } = params;
9
- // Initialize internal state from column.pinned definitions (mount only)
10
- const [internalPinnedColumns, setInternalPinnedColumns] = useState(() => {
11
- const initial = {};
12
- for (const col of columns) {
13
- if (col.pinned) {
14
- initial[col.columnId] = col.pinned;
15
- }
16
- }
17
- return initial;
18
- });
19
- // Use controlled state if provided, otherwise internal
20
- const pinnedColumns = controlledPinnedColumns ?? internalPinnedColumns;
21
- const pinColumn = useCallback((columnId, side) => {
22
- const next = { ...pinnedColumns, [columnId]: side };
23
- if (!controlledPinnedColumns)
24
- setInternalPinnedColumns(next);
25
- onColumnPinned?.(columnId, side);
26
- }, [pinnedColumns, controlledPinnedColumns, onColumnPinned]);
27
- const unpinColumn = useCallback((columnId) => {
28
- const { [columnId]: _, ...next } = pinnedColumns;
29
- if (!controlledPinnedColumns)
30
- setInternalPinnedColumns(next);
31
- onColumnPinned?.(columnId, null);
32
- }, [pinnedColumns, controlledPinnedColumns, onColumnPinned]);
33
- const isPinned = useCallback((columnId) => {
34
- return pinnedColumns[columnId];
35
- }, [pinnedColumns]);
36
- const computeLeftOffsets = useCallback((visibleCols, columnWidths, defaultWidth, hasCheckboxColumn, checkboxColumnWidth) => {
37
- const offsets = {};
38
- let left = hasCheckboxColumn ? checkboxColumnWidth : 0;
39
- for (const col of visibleCols) {
40
- if (pinnedColumns[col.columnId] === 'left') {
41
- offsets[col.columnId] = left;
42
- left += columnWidths[col.columnId] ?? defaultWidth;
43
- }
44
- }
45
- return offsets;
46
- }, [pinnedColumns]);
47
- const computeRightOffsets = useCallback((visibleCols, columnWidths, defaultWidth) => {
48
- const offsets = {};
49
- let right = 0;
50
- for (let i = visibleCols.length - 1; i >= 0; i--) {
51
- const col = visibleCols[i];
52
- if (pinnedColumns[col.columnId] === 'right') {
53
- offsets[col.columnId] = right;
54
- right += columnWidths[col.columnId] ?? defaultWidth;
55
- }
56
- }
57
- return offsets;
58
- }, [pinnedColumns]);
59
- return {
60
- pinnedColumns,
61
- pinColumn,
62
- unpinColumn,
63
- isPinned,
64
- computeLeftOffsets,
65
- computeRightOffsets,
66
- };
67
- }
@@ -1,143 +0,0 @@
1
- import { useState, useCallback, useRef, useEffect } from 'react';
2
- import { calculateDropTarget, reorderColumnArray, getPinStateForColumn, } from '@alaarab/ogrid-core';
3
- /** Width of the resize handle zone on the right edge of each header cell. */
4
- const RESIZE_HANDLE_ZONE = 8;
5
- /**
6
- * Convert Record<string, 'left' | 'right'> to the { left?, right? } shape core expects.
7
- */
8
- function toPinnedColumnsShape(pinned) {
9
- if (!pinned)
10
- return undefined;
11
- const left = [];
12
- const right = [];
13
- for (const [id, side] of Object.entries(pinned)) {
14
- if (side === 'left')
15
- left.push(id);
16
- else if (side === 'right')
17
- right.push(id);
18
- }
19
- if (left.length === 0 && right.length === 0)
20
- return undefined;
21
- return {
22
- ...(left.length > 0 ? { left } : {}),
23
- ...(right.length > 0 ? { right } : {}),
24
- };
25
- }
26
- /**
27
- * Manages column reorder drag interactions with RAF-throttled updates.
28
- * @param params - Columns, order, change callback, enabled flag, and wrapper ref.
29
- * @returns Drag state and mousedown handler for header cells.
30
- */
31
- export function useColumnReorder(params) {
32
- const { columns, columnOrder, onColumnOrderChange, enabled = true, pinnedColumns, wrapperRef, } = params;
33
- const [isDragging, setIsDragging] = useState(false);
34
- const [dropIndicatorX, setDropIndicatorX] = useState(null);
35
- const rafRef = useRef(0);
36
- // Refs for latest values so the window listeners capture current state
37
- const columnsRef = useRef(columns);
38
- columnsRef.current = columns;
39
- const columnOrderRef = useRef(columnOrder);
40
- columnOrderRef.current = columnOrder;
41
- const onColumnOrderChangeRef = useRef(onColumnOrderChange);
42
- onColumnOrderChangeRef.current = onColumnOrderChange;
43
- const pinnedColumnsRef = useRef(pinnedColumns);
44
- pinnedColumnsRef.current = pinnedColumns;
45
- // Track active drag state for cleanup on unmount
46
- const cleanupRef = useRef(null);
47
- useEffect(() => {
48
- return () => {
49
- if (cleanupRef.current) {
50
- cleanupRef.current();
51
- cleanupRef.current = null;
52
- }
53
- };
54
- }, []);
55
- const handleHeaderMouseDown = useCallback((columnId, event) => {
56
- if (!enabled)
57
- return;
58
- if (!onColumnOrderChangeRef.current)
59
- return;
60
- // Gate on left-click only
61
- if (event.button !== 0)
62
- return;
63
- // Skip if in resize handle zone (right 8px of the header cell)
64
- const target = event.currentTarget;
65
- const rect = target.getBoundingClientRect();
66
- if (event.clientX > rect.right - RESIZE_HANDLE_ZONE)
67
- return;
68
- // Skip column groups — only reorder leaf columns
69
- const cols = columnsRef.current;
70
- const colIndex = cols.findIndex((c) => c.columnId === columnId);
71
- if (colIndex === -1)
72
- return;
73
- event.preventDefault();
74
- const startX = event.clientX;
75
- let hasMoved = false;
76
- let latestDropTargetIndex = null;
77
- // Determine pin state of the dragged column
78
- const pinnedShape = toPinnedColumnsShape(pinnedColumnsRef.current);
79
- const draggedPinState = getPinStateForColumn(columnId, pinnedShape);
80
- // Lock text selection and set grabbing cursor during drag
81
- const prevUserSelect = document.body.style.userSelect;
82
- const prevCursor = document.body.style.cursor;
83
- document.body.style.userSelect = 'none';
84
- document.body.style.cursor = 'grabbing';
85
- const onMove = (moveEvent) => {
86
- // Require a small minimum drag distance before activating
87
- if (!hasMoved && Math.abs(moveEvent.clientX - startX) < 5)
88
- return;
89
- if (!hasMoved) {
90
- hasMoved = true;
91
- setIsDragging(true);
92
- }
93
- if (rafRef.current)
94
- cancelAnimationFrame(rafRef.current);
95
- rafRef.current = requestAnimationFrame(() => {
96
- rafRef.current = 0;
97
- const wrapper = wrapperRef.current;
98
- if (!wrapper)
99
- return;
100
- const currentOrder = columnOrderRef.current ?? columnsRef.current.map((c) => c.columnId);
101
- const result = calculateDropTarget({
102
- mouseX: moveEvent.clientX,
103
- columnOrder: currentOrder,
104
- draggedColumnId: columnId,
105
- draggedPinState,
106
- tableElement: wrapper,
107
- pinnedColumns: pinnedShape,
108
- });
109
- if (result) {
110
- latestDropTargetIndex = result.targetIndex;
111
- setDropIndicatorX(result.indicatorX);
112
- }
113
- });
114
- };
115
- const cleanup = () => {
116
- window.removeEventListener('mousemove', onMove, true);
117
- window.removeEventListener('mouseup', onUp, true);
118
- cleanupRef.current = null;
119
- // Restore user-select and cursor
120
- document.body.style.userSelect = prevUserSelect;
121
- document.body.style.cursor = prevCursor;
122
- // Cancel pending RAF
123
- if (rafRef.current) {
124
- cancelAnimationFrame(rafRef.current);
125
- rafRef.current = 0;
126
- }
127
- };
128
- const onUp = () => {
129
- cleanup();
130
- if (hasMoved && latestDropTargetIndex != null) {
131
- const currentOrder = columnOrderRef.current ?? columnsRef.current.map((c) => c.columnId);
132
- const newOrder = reorderColumnArray(currentOrder, columnId, latestDropTargetIndex);
133
- onColumnOrderChangeRef.current?.(newOrder);
134
- }
135
- setIsDragging(false);
136
- setDropIndicatorX(null);
137
- };
138
- window.addEventListener('mousemove', onMove, true);
139
- window.addEventListener('mouseup', onUp, true);
140
- cleanupRef.current = cleanup;
141
- }, [enabled, wrapperRef]);
142
- return { isDragging, dropIndicatorX, handleHeaderMouseDown };
143
- }