@alaarab/ogrid-react 2.0.9 → 2.0.12
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/dist/esm/components/BaseInlineCellEditor.js +2 -2
- package/dist/esm/components/CellErrorBoundary.js +1 -1
- package/dist/esm/components/ColumnChooserProps.js +6 -0
- package/dist/esm/components/ColumnHeaderFilterContent.js +35 -0
- package/dist/esm/components/ColumnHeaderFilterRenderers.js +67 -0
- package/dist/esm/components/MarchingAntsOverlay.js +25 -44
- package/dist/esm/components/PaginationControlsProps.js +6 -0
- package/dist/esm/constants/domHelpers.js +16 -0
- package/dist/esm/hooks/index.js +2 -0
- package/dist/esm/hooks/useColumnHeaderMenuState.js +52 -2
- package/dist/esm/hooks/useColumnResize.js +29 -0
- package/dist/esm/hooks/useDataGridState.js +77 -10
- package/dist/esm/hooks/useDataGridTableOrchestration.js +200 -0
- package/dist/esm/hooks/useDebounce.js +1 -1
- package/dist/esm/hooks/useOGrid.js +3 -6
- package/dist/esm/hooks/usePaginationControls.js +19 -0
- package/dist/esm/index.js +6 -1
- package/dist/esm/utils/index.js +1 -1
- package/dist/types/components/ColumnChooserProps.d.ts +12 -0
- package/dist/types/components/ColumnHeaderFilterContent.d.ts +62 -0
- package/dist/types/components/ColumnHeaderFilterRenderers.d.ts +71 -0
- package/dist/types/components/MarchingAntsOverlay.d.ts +11 -1
- package/dist/types/components/PaginationControlsProps.d.ts +15 -0
- package/dist/types/constants/domHelpers.d.ts +17 -0
- package/dist/types/hooks/index.d.ts +5 -1
- package/dist/types/hooks/useColumnHeaderMenuState.d.ts +23 -1
- package/dist/types/hooks/useDataGridState.d.ts +10 -6
- package/dist/types/hooks/useDataGridTableOrchestration.d.ts +131 -0
- package/dist/types/hooks/usePaginationControls.d.ts +20 -0
- package/dist/types/index.d.ts +9 -2
- package/dist/types/types/dataGridTypes.d.ts +1 -1
- package/dist/types/utils/index.d.ts +1 -1
- package/package.json +4 -4
|
@@ -35,9 +35,9 @@ export const richSelectDropdownStyle = {
|
|
|
35
35
|
maxHeight: 200,
|
|
36
36
|
overflowY: 'auto',
|
|
37
37
|
background: 'var(--ogrid-bg, #fff)',
|
|
38
|
-
border: '1px solid var(--ogrid-border,
|
|
38
|
+
border: '1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))',
|
|
39
39
|
zIndex: 10,
|
|
40
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
|
|
40
|
+
boxShadow: 'var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))',
|
|
41
41
|
};
|
|
42
42
|
export const richSelectOptionStyle = {
|
|
43
43
|
padding: '6px 8px',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
export const DateFilterContent = ({ tempDateFrom, setTempDateFrom, tempDateTo, setTempDateTo, onApply, onClear, classNames, }) => (_jsxs(_Fragment, { children: [_jsxs("div", { style: { padding: '8px 12px', display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }, children: ["From:", _jsx("input", { type: "date", value: tempDateFrom, onChange: (e) => setTempDateFrom(e.target.value), style: { flex: 1 } })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }, children: ["To:", _jsx("input", { type: "date", value: tempDateTo, onChange: (e) => setTempDateTo(e.target.value), style: { flex: 1 } })] })] }), _jsxs("div", { className: classNames?.popoverActions, children: [_jsx("button", { className: classNames?.clearButton, onClick: onClear, disabled: !tempDateFrom && !tempDateTo, children: "Clear" }), _jsx("button", { className: classNames?.applyButton, onClick: onApply, children: "Apply" })] })] }));
|
|
3
|
+
DateFilterContent.displayName = 'DateFilterContent';
|
|
4
|
+
// ---- Utility to extract useColumnHeaderFilterState params from props ----
|
|
5
|
+
export function getColumnHeaderFilterStateParams(props) {
|
|
6
|
+
return {
|
|
7
|
+
filterType: props.filterType,
|
|
8
|
+
isSorted: props.isSorted ?? false,
|
|
9
|
+
isSortedDescending: props.isSortedDescending ?? false,
|
|
10
|
+
onSort: props.onSort,
|
|
11
|
+
selectedValues: props.selectedValues,
|
|
12
|
+
onFilterChange: props.onFilterChange,
|
|
13
|
+
options: props.options,
|
|
14
|
+
isLoadingOptions: props.isLoadingOptions ?? false,
|
|
15
|
+
textValue: props.textValue ?? '',
|
|
16
|
+
onTextChange: props.onTextChange,
|
|
17
|
+
selectedUser: props.selectedUser,
|
|
18
|
+
onUserChange: props.onUserChange,
|
|
19
|
+
peopleSearch: props.peopleSearch,
|
|
20
|
+
dateValue: props.dateValue,
|
|
21
|
+
onDateChange: props.onDateChange,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// ---- Helper to build date filter props from state ----
|
|
25
|
+
export function getDateFilterContentProps(state, classNames) {
|
|
26
|
+
return {
|
|
27
|
+
tempDateFrom: state.tempDateFrom,
|
|
28
|
+
setTempDateFrom: state.setTempDateFrom,
|
|
29
|
+
tempDateTo: state.tempDateTo,
|
|
30
|
+
setTempDateTo: state.setTempDateTo,
|
|
31
|
+
onApply: state.handlers.handleDateApply,
|
|
32
|
+
onClear: state.handlers.handleDateClear,
|
|
33
|
+
classNames,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filter content dispatching for ColumnHeaderFilter across all React UI packages.
|
|
3
|
+
*
|
|
4
|
+
* Each UI package provides framework-specific sub-filter components (TextFilterPopover,
|
|
5
|
+
* MultiSelectFilterPopover, PeopleFilterPopover, date content). This utility dispatches
|
|
6
|
+
* to the correct renderer based on filterType, eliminating the duplicated if/switch chain
|
|
7
|
+
* that was previously in each UI package's ColumnHeaderFilter component.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Dispatches to the appropriate filter content renderer based on filterType.
|
|
11
|
+
* Eliminates the duplicated if/switch chain in each UI package's ColumnHeaderFilter.
|
|
12
|
+
*
|
|
13
|
+
* @param filterType - The column's filter type
|
|
14
|
+
* @param state - The result from useColumnHeaderFilterState
|
|
15
|
+
* @param options - The filter options array (for multiSelect)
|
|
16
|
+
* @param isLoadingOptions - Whether options are loading
|
|
17
|
+
* @param selectedUser - The currently selected user (for people filter)
|
|
18
|
+
* @param renderers - Framework-specific renderer functions
|
|
19
|
+
* @returns The rendered filter content, or null for unsupported filter types
|
|
20
|
+
*/
|
|
21
|
+
export function renderFilterContent(filterType, state, options, isLoadingOptions, selectedUser, renderers) {
|
|
22
|
+
if (filterType === 'multiSelect') {
|
|
23
|
+
return renderers.renderMultiSelect({
|
|
24
|
+
searchText: state.searchText,
|
|
25
|
+
onSearchChange: state.setSearchText,
|
|
26
|
+
options,
|
|
27
|
+
filteredOptions: state.filteredOptions,
|
|
28
|
+
selected: state.tempSelected,
|
|
29
|
+
onOptionToggle: state.handlers.handleCheckboxChange,
|
|
30
|
+
onSelectAll: state.handlers.handleSelectAll,
|
|
31
|
+
onClearSelection: state.handlers.handleClearSelection,
|
|
32
|
+
onApply: state.handlers.handleApplyMultiSelect,
|
|
33
|
+
isLoading: isLoadingOptions,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (filterType === 'text') {
|
|
37
|
+
return renderers.renderText({
|
|
38
|
+
value: state.tempTextValue,
|
|
39
|
+
onValueChange: state.setTempTextValue,
|
|
40
|
+
onApply: state.handlers.handleTextApply,
|
|
41
|
+
onClear: state.handlers.handleTextClear,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (filterType === 'people') {
|
|
45
|
+
return renderers.renderPeople({
|
|
46
|
+
selectedUser,
|
|
47
|
+
searchText: state.peopleSearchText,
|
|
48
|
+
onSearchChange: state.setPeopleSearchText,
|
|
49
|
+
suggestions: state.peopleSuggestions,
|
|
50
|
+
isLoading: state.isPeopleLoading,
|
|
51
|
+
onUserSelect: state.handlers.handleUserSelect,
|
|
52
|
+
onClearUser: state.handlers.handleClearUser,
|
|
53
|
+
inputRef: state.peopleInputRef,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (filterType === 'date') {
|
|
57
|
+
return renderers.renderDate({
|
|
58
|
+
tempDateFrom: state.tempDateFrom,
|
|
59
|
+
setTempDateFrom: state.setTempDateFrom,
|
|
60
|
+
tempDateTo: state.tempDateTo,
|
|
61
|
+
setTempDateTo: state.setTempDateTo,
|
|
62
|
+
onApply: state.handlers.handleDateApply,
|
|
63
|
+
onClear: state.handlers.handleDateClear,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
@@ -8,38 +8,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
8
8
|
* Uses SVG rects positioned via cell data-attribute measurements.
|
|
9
9
|
*/
|
|
10
10
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
11
|
+
import { measureRange, injectGlobalStyles } from '@alaarab/ogrid-core';
|
|
11
12
|
const MARCHING_ANTS_ANIMATION = { animation: 'ogrid-marching-ants 0.5s linear infinite' };
|
|
12
|
-
|
|
13
|
-
function ensureKeyframes() {
|
|
14
|
-
if (typeof document === 'undefined')
|
|
15
|
-
return;
|
|
16
|
-
if (document.getElementById('ogrid-marching-ants-keyframes'))
|
|
17
|
-
return;
|
|
18
|
-
const style = document.createElement('style');
|
|
19
|
-
style.id = 'ogrid-marching-ants-keyframes';
|
|
20
|
-
style.textContent =
|
|
21
|
-
'@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}';
|
|
22
|
-
document.head.appendChild(style);
|
|
23
|
-
}
|
|
24
|
-
/** Measure the bounding rect of a range within a container. */
|
|
25
|
-
function measureRange(container, range, colOffset) {
|
|
26
|
-
const startGlobalCol = range.startCol + colOffset;
|
|
27
|
-
const endGlobalCol = range.endCol + colOffset;
|
|
28
|
-
const topLeft = container.querySelector(`[data-row-index="${range.startRow}"][data-col-index="${startGlobalCol}"]`);
|
|
29
|
-
const bottomRight = container.querySelector(`[data-row-index="${range.endRow}"][data-col-index="${endGlobalCol}"]`);
|
|
30
|
-
if (!topLeft || !bottomRight)
|
|
31
|
-
return null;
|
|
32
|
-
const cRect = container.getBoundingClientRect();
|
|
33
|
-
const tlRect = topLeft.getBoundingClientRect();
|
|
34
|
-
const brRect = bottomRight.getBoundingClientRect();
|
|
35
|
-
return {
|
|
36
|
-
top: tlRect.top - cRect.top,
|
|
37
|
-
left: tlRect.left - cRect.left,
|
|
38
|
-
width: brRect.right - tlRect.left,
|
|
39
|
-
height: brRect.bottom - tlRect.top,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, cutRange, colOffset, }) {
|
|
13
|
+
export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, cutRange, colOffset, items, visibleColumns, columnSizingOverrides, columnOrder, }) {
|
|
43
14
|
const [selRect, setSelRect] = useState(null);
|
|
44
15
|
const [clipRect, setClipRect] = useState(null);
|
|
45
16
|
const rafRef = useRef(0);
|
|
@@ -53,10 +24,11 @@ export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, c
|
|
|
53
24
|
}
|
|
54
25
|
setSelRect(selectionRange ? measureRange(container, selectionRange, colOffset) : null);
|
|
55
26
|
setClipRect(clipRange ? measureRange(container, clipRange, colOffset) : null);
|
|
56
|
-
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
|
+
}, [selectionRange, clipRange, containerRef, colOffset, items, visibleColumns, columnSizingOverrides, columnOrder]);
|
|
57
29
|
// Inject keyframes on mount
|
|
58
30
|
useEffect(() => {
|
|
59
|
-
|
|
31
|
+
injectGlobalStyles('ogrid-marching-ants-keyframes', '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}');
|
|
60
32
|
}, []);
|
|
61
33
|
// Measure when any range changes; re-measure on resize
|
|
62
34
|
useEffect(() => {
|
|
@@ -88,23 +60,32 @@ export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, c
|
|
|
88
60
|
selectionRange.startCol === clipRange.startCol &&
|
|
89
61
|
selectionRange.endRow === clipRange.endRow &&
|
|
90
62
|
selectionRange.endCol === clipRange.endCol;
|
|
91
|
-
|
|
63
|
+
// Round to integer pixels so the stroke aligns to the pixel grid and corners connect cleanly
|
|
64
|
+
const roundRect = (r) => ({
|
|
65
|
+
top: Math.round(r.top),
|
|
66
|
+
left: Math.round(r.left),
|
|
67
|
+
width: Math.round(r.width),
|
|
68
|
+
height: Math.round(r.height),
|
|
69
|
+
});
|
|
70
|
+
const selR = selRect ? roundRect(selRect) : null;
|
|
71
|
+
const clipR = clipRect ? roundRect(clipRect) : null;
|
|
72
|
+
return (_jsxs(_Fragment, { children: [selR && !clipRangeMatchesSel && (_jsx("svg", { style: {
|
|
92
73
|
position: 'absolute',
|
|
93
|
-
top:
|
|
94
|
-
left:
|
|
95
|
-
width:
|
|
96
|
-
height:
|
|
74
|
+
top: selR.top,
|
|
75
|
+
left: selR.left,
|
|
76
|
+
width: selR.width,
|
|
77
|
+
height: selR.height,
|
|
97
78
|
pointerEvents: 'none',
|
|
98
79
|
zIndex: 4,
|
|
99
80
|
overflow: 'visible',
|
|
100
|
-
}, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0,
|
|
81
|
+
}, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, selR.width - 2), height: Math.max(0, selR.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2", style: { shapeRendering: 'crispEdges' } }) })), clipR && (_jsx("svg", { style: {
|
|
101
82
|
position: 'absolute',
|
|
102
|
-
top:
|
|
103
|
-
left:
|
|
104
|
-
width:
|
|
105
|
-
height:
|
|
83
|
+
top: clipR.top,
|
|
84
|
+
left: clipR.left,
|
|
85
|
+
width: clipR.width,
|
|
86
|
+
height: clipR.height,
|
|
106
87
|
pointerEvents: 'none',
|
|
107
88
|
zIndex: 5,
|
|
108
89
|
overflow: 'visible',
|
|
109
|
-
}, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0,
|
|
90
|
+
}, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, clipR.width - 2), height: Math.max(0, clipR.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2", strokeDasharray: "4 4", style: { ...MARCHING_ANTS_ANIMATION, shapeRendering: 'crispEdges' } }) }))] }));
|
|
110
91
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-scope stable constants shared across all React UI DataGridTable implementations.
|
|
3
|
+
* Avoid per-render allocations by keeping these at module scope.
|
|
4
|
+
*/
|
|
5
|
+
/** Root container style for the DataGridTable (flex column layout). */
|
|
6
|
+
export const GRID_ROOT_STYLE = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
|
|
7
|
+
/** Applied to cells that support editing — shows the cell cursor. */
|
|
8
|
+
export const CURSOR_CELL_STYLE = { cursor: 'cell' };
|
|
9
|
+
/** Minimum size for popover anchor elements. */
|
|
10
|
+
export const POPOVER_ANCHOR_STYLE = { minHeight: '100%', minWidth: 40 };
|
|
11
|
+
/** Prevents the default browser action for mouse events. */
|
|
12
|
+
export const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
|
|
13
|
+
/** No-operation function. */
|
|
14
|
+
export const NOOP = () => { };
|
|
15
|
+
/** Stops event propagation (e.g. click on checkbox inside a row). */
|
|
16
|
+
export const STOP_PROPAGATION = (e) => { e.stopPropagation(); };
|
package/dist/esm/hooks/index.js
CHANGED
|
@@ -27,3 +27,5 @@ export { useTableLayout } from './useTableLayout';
|
|
|
27
27
|
export { useColumnReorder } from './useColumnReorder';
|
|
28
28
|
export { useVirtualScroll } from './useVirtualScroll';
|
|
29
29
|
export { useLatestRef } from './useLatestRef';
|
|
30
|
+
export { usePaginationControls } from './usePaginationControls';
|
|
31
|
+
export { useDataGridTableOrchestration } from './useDataGridTableOrchestration';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
|
+
import { measureColumnContentWidth } from '../utils';
|
|
2
3
|
/**
|
|
3
|
-
* Manages state for the column header menu (pin
|
|
4
|
+
* Manages state for the column header menu (pin, sort, autosize actions).
|
|
4
5
|
* Tracks which column's menu is open, anchor element, and action handlers.
|
|
5
6
|
*/
|
|
6
7
|
export function useColumnHeaderMenuState(params) {
|
|
7
|
-
const { pinnedColumns, onPinColumn, onUnpinColumn } = params;
|
|
8
|
+
const { pinnedColumns, onPinColumn, onUnpinColumn, sortBy, sortDirection, onColumnSort, onColumnResized, onAutosizeColumn, columns, data: _data, getRowId: _getRowId, } = params;
|
|
8
9
|
const [isOpen, setIsOpen] = useState(false);
|
|
9
10
|
const [openForColumn, setOpenForColumn] = useState(null);
|
|
10
11
|
const [anchorElement, setAnchorElement] = useState(null);
|
|
@@ -22,6 +23,10 @@ export function useColumnHeaderMenuState(params) {
|
|
|
22
23
|
const canPinLeft = currentPinState !== 'left';
|
|
23
24
|
const canPinRight = currentPinState !== 'right';
|
|
24
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;
|
|
25
30
|
const handlePinLeft = useCallback(() => {
|
|
26
31
|
if (openForColumn && canPinLeft) {
|
|
27
32
|
onPinColumn(openForColumn, 'left');
|
|
@@ -40,6 +45,43 @@ export function useColumnHeaderMenuState(params) {
|
|
|
40
45
|
close();
|
|
41
46
|
}
|
|
42
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]);
|
|
43
85
|
return {
|
|
44
86
|
isOpen,
|
|
45
87
|
openForColumn,
|
|
@@ -49,8 +91,16 @@ export function useColumnHeaderMenuState(params) {
|
|
|
49
91
|
handlePinLeft,
|
|
50
92
|
handlePinRight,
|
|
51
93
|
handleUnpin,
|
|
94
|
+
handleSortAsc,
|
|
95
|
+
handleSortDesc,
|
|
96
|
+
handleClearSort,
|
|
97
|
+
handleAutosizeThis,
|
|
98
|
+
handleAutosizeAll,
|
|
52
99
|
canPinLeft,
|
|
53
100
|
canPinRight,
|
|
54
101
|
canUnpin,
|
|
102
|
+
currentSort,
|
|
103
|
+
isSortable,
|
|
104
|
+
isResizable,
|
|
55
105
|
};
|
|
56
106
|
}
|
|
@@ -36,6 +36,26 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
|
|
|
36
36
|
?? col.defaultWidth
|
|
37
37
|
?? defaultWidth;
|
|
38
38
|
let latestWidth = startWidth;
|
|
39
|
+
// Lock all column widths to their current DOM widths on first resize.
|
|
40
|
+
// With table-layout:auto, resizing one column causes the browser to compress others.
|
|
41
|
+
// Snapshotting all widths prevents this — only the dragged column changes.
|
|
42
|
+
const thead = thEl?.closest('thead');
|
|
43
|
+
if (thead) {
|
|
44
|
+
const allThs = thead.querySelectorAll('th[data-column-id]');
|
|
45
|
+
if (allThs.length > 0) {
|
|
46
|
+
setColumnSizingOverrides((prev) => {
|
|
47
|
+
const next = { ...prev };
|
|
48
|
+
allThs.forEach((th) => {
|
|
49
|
+
const colId = th.dataset.columnId;
|
|
50
|
+
if (colId && !next[colId]) {
|
|
51
|
+
next[colId] = { widthPx: th.getBoundingClientRect().width };
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
next[columnId] = { widthPx: startWidth };
|
|
55
|
+
return next;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
39
59
|
// Lock cursor and prevent text selection during drag
|
|
40
60
|
const prevCursor = document.body.style.cursor;
|
|
41
61
|
const prevUserSelect = document.body.style.userSelect;
|
|
@@ -73,6 +93,15 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
|
|
|
73
93
|
const onUp = () => {
|
|
74
94
|
cleanup();
|
|
75
95
|
flushWidth();
|
|
96
|
+
// Remove any rogue :focus-visible outlines that appeared during the drag.
|
|
97
|
+
// Re-focus the grid wrapper so keyboard navigation still works.
|
|
98
|
+
const wrapper = thEl?.closest('[tabindex]');
|
|
99
|
+
if (wrapper) {
|
|
100
|
+
wrapper.focus({ preventScroll: true });
|
|
101
|
+
}
|
|
102
|
+
else if (document.activeElement instanceof HTMLElement) {
|
|
103
|
+
document.activeElement.blur();
|
|
104
|
+
}
|
|
76
105
|
if (onColumnResizedRef.current) {
|
|
77
106
|
onColumnResizedRef.current(columnId, latestWidth);
|
|
78
107
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useMemo, useCallback, useState } from 'react';
|
|
1
|
+
import { useMemo, useCallback, useState, useLayoutEffect } from 'react';
|
|
2
2
|
import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations } from '../utils';
|
|
3
|
+
import { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
3
4
|
import { useRowSelection } from './useRowSelection';
|
|
4
5
|
import { useCellEditing } from './useCellEditing';
|
|
5
6
|
import { useActiveCell } from './useActiveCell';
|
|
@@ -150,11 +151,48 @@ export function useDataGridState(params) {
|
|
|
150
151
|
pinnedColumns,
|
|
151
152
|
onColumnPinned,
|
|
152
153
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
// Measure actual column widths from the DOM for accurate pinning offsets.
|
|
155
|
+
// With table-layout: auto, rendered widths can exceed declared minimums.
|
|
156
|
+
const [measuredColumnWidths, setMeasuredColumnWidths] = useState({});
|
|
157
|
+
useLayoutEffect(() => {
|
|
158
|
+
const wrapper = wrapperRef.current;
|
|
159
|
+
if (!wrapper)
|
|
160
|
+
return;
|
|
161
|
+
const headerCells = wrapper.querySelectorAll('th[data-column-id]');
|
|
162
|
+
if (headerCells.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
const measured = {};
|
|
165
|
+
headerCells.forEach((cell) => {
|
|
166
|
+
const colId = cell.getAttribute('data-column-id');
|
|
167
|
+
if (colId)
|
|
168
|
+
measured[colId] = cell.offsetWidth;
|
|
169
|
+
});
|
|
170
|
+
setMeasuredColumnWidths((prev) => {
|
|
171
|
+
// Only update if widths actually changed to avoid render loops
|
|
172
|
+
for (const key in measured) {
|
|
173
|
+
if (prev[key] !== measured[key])
|
|
174
|
+
return measured;
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(prev).length !== Object.keys(measured).length)
|
|
177
|
+
return measured;
|
|
178
|
+
return prev;
|
|
179
|
+
});
|
|
180
|
+
// Re-measure when columns, container size, or resize overrides change
|
|
181
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
+
}, [visibleCols, containerWidth, columnSizingOverrides]);
|
|
183
|
+
// Build column width map for pinning offset computation
|
|
184
|
+
const columnWidthMap = useMemo(() => {
|
|
185
|
+
const map = {};
|
|
186
|
+
for (const col of visibleCols) {
|
|
187
|
+
const override = columnSizingOverrides[col.columnId];
|
|
188
|
+
map[col.columnId] = override
|
|
189
|
+
? override.widthPx
|
|
190
|
+
: (measuredColumnWidths[col.columnId] ?? col.idealWidth ?? col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
|
|
191
|
+
}
|
|
192
|
+
return map;
|
|
193
|
+
}, [visibleCols, columnSizingOverrides, measuredColumnWidths]);
|
|
194
|
+
const leftOffsets = useMemo(() => pinningResult.computeLeftOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH, hasCheckboxCol, CHECKBOX_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap, hasCheckboxCol]);
|
|
195
|
+
const rightOffsets = useMemo(() => pinningResult.computeRightOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap]);
|
|
158
196
|
const aggregation = useMemo(() => computeAggregations(items, visibleCols, cellSelection ? selectionRange : null), [items, visibleCols, selectionRange, cellSelection]);
|
|
159
197
|
const statusBarConfig = useMemo(() => {
|
|
160
198
|
const base = getDataGridStatusBarConfig(statusBar, items.length, selectedRowIds.size);
|
|
@@ -172,9 +210,27 @@ export function useDataGridState(params) {
|
|
|
172
210
|
const onFilterChangeRef = useLatestRef(onFilterChange);
|
|
173
211
|
const peopleSearchRef = useLatestRef(peopleSearch);
|
|
174
212
|
// Stable callback wrappers that delegate to refs
|
|
175
|
-
const stableOnColumnSort = useCallback((columnKey) => onColumnSortRef.current?.(columnKey),
|
|
213
|
+
const stableOnColumnSort = useCallback((columnKey, direction) => onColumnSortRef.current?.(columnKey, direction),
|
|
176
214
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
177
215
|
[]);
|
|
216
|
+
// Autosize callback — updates internal column sizing state + notifies external listener
|
|
217
|
+
const handleAutosizeColumn = useCallback((columnId, width) => {
|
|
218
|
+
setColumnSizingOverrides((prev) => ({ ...prev, [columnId]: { widthPx: width } }));
|
|
219
|
+
onColumnResized?.(columnId, width);
|
|
220
|
+
}, [setColumnSizingOverrides, onColumnResized]);
|
|
221
|
+
const headerMenuResult = useColumnHeaderMenuState({
|
|
222
|
+
pinnedColumns: pinningResult.pinnedColumns,
|
|
223
|
+
onPinColumn: pinningResult.pinColumn,
|
|
224
|
+
onUnpinColumn: pinningResult.unpinColumn,
|
|
225
|
+
sortBy,
|
|
226
|
+
sortDirection,
|
|
227
|
+
onColumnSort: stableOnColumnSort,
|
|
228
|
+
onColumnResized,
|
|
229
|
+
onAutosizeColumn: handleAutosizeColumn,
|
|
230
|
+
columns: flatColumns,
|
|
231
|
+
data: items,
|
|
232
|
+
getRowId: getRowId,
|
|
233
|
+
});
|
|
178
234
|
const stableOnFilterChange = useCallback((...args) => onFilterChangeRef.current?.(...args),
|
|
179
235
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
180
236
|
[]);
|
|
@@ -326,8 +382,8 @@ export function useDataGridState(params) {
|
|
|
326
382
|
pinColumn: pinningResult.pinColumn,
|
|
327
383
|
unpinColumn: pinningResult.unpinColumn,
|
|
328
384
|
isPinned: pinningResult.isPinned,
|
|
329
|
-
|
|
330
|
-
|
|
385
|
+
leftOffsets,
|
|
386
|
+
rightOffsets,
|
|
331
387
|
headerMenu: {
|
|
332
388
|
isOpen: headerMenuResult.isOpen,
|
|
333
389
|
openForColumn: headerMenuResult.openForColumn,
|
|
@@ -337,17 +393,28 @@ export function useDataGridState(params) {
|
|
|
337
393
|
handlePinLeft: headerMenuResult.handlePinLeft,
|
|
338
394
|
handlePinRight: headerMenuResult.handlePinRight,
|
|
339
395
|
handleUnpin: headerMenuResult.handleUnpin,
|
|
396
|
+
handleSortAsc: headerMenuResult.handleSortAsc,
|
|
397
|
+
handleSortDesc: headerMenuResult.handleSortDesc,
|
|
398
|
+
handleClearSort: headerMenuResult.handleClearSort,
|
|
399
|
+
handleAutosizeThis: headerMenuResult.handleAutosizeThis,
|
|
400
|
+
handleAutosizeAll: headerMenuResult.handleAutosizeAll,
|
|
340
401
|
canPinLeft: headerMenuResult.canPinLeft,
|
|
341
402
|
canPinRight: headerMenuResult.canPinRight,
|
|
342
403
|
canUnpin: headerMenuResult.canUnpin,
|
|
404
|
+
currentSort: headerMenuResult.currentSort,
|
|
405
|
+
isSortable: headerMenuResult.isSortable,
|
|
406
|
+
isResizable: headerMenuResult.isResizable,
|
|
343
407
|
},
|
|
344
408
|
}), [
|
|
345
409
|
pinningResult.pinnedColumns, pinningResult.pinColumn, pinningResult.unpinColumn,
|
|
346
|
-
pinningResult.isPinned,
|
|
410
|
+
pinningResult.isPinned, leftOffsets, rightOffsets,
|
|
347
411
|
headerMenuResult.isOpen, headerMenuResult.openForColumn, headerMenuResult.anchorElement,
|
|
348
412
|
headerMenuResult.open, headerMenuResult.close, headerMenuResult.handlePinLeft,
|
|
349
413
|
headerMenuResult.handlePinRight, headerMenuResult.handleUnpin,
|
|
414
|
+
headerMenuResult.handleSortAsc, headerMenuResult.handleSortDesc, headerMenuResult.handleClearSort,
|
|
415
|
+
headerMenuResult.handleAutosizeThis, headerMenuResult.handleAutosizeAll,
|
|
350
416
|
headerMenuResult.canPinLeft, headerMenuResult.canPinRight, headerMenuResult.canUnpin,
|
|
417
|
+
headerMenuResult.currentSort, headerMenuResult.isSortable, headerMenuResult.isResizable,
|
|
351
418
|
]);
|
|
352
419
|
return {
|
|
353
420
|
layout: layoutState,
|