@alaarab/ogrid-react 2.0.19 → 2.0.21
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/BaseColumnHeaderMenu.js +78 -0
- package/dist/esm/components/BaseDropIndicator.js +4 -0
- package/dist/esm/components/BaseEmptyState.js +4 -0
- package/dist/esm/components/BaseLoadingOverlay.js +4 -0
- package/dist/esm/components/createOGrid.js +22 -0
- package/dist/esm/hooks/useActiveCell.js +48 -33
- package/dist/esm/hooks/useCellSelection.js +15 -11
- package/dist/esm/hooks/useDataGridState.js +5 -31
- package/dist/esm/hooks/useDataGridTableOrchestration.js +2 -1
- package/dist/esm/hooks/useOGrid.js +24 -4
- package/dist/esm/hooks/useRowSelection.js +11 -2
- package/dist/esm/index.js +5 -0
- package/dist/types/components/BaseColumnHeaderMenu.d.ts +34 -0
- package/dist/types/components/BaseDropIndicator.d.ts +7 -0
- package/dist/types/components/BaseEmptyState.d.ts +21 -0
- package/dist/types/components/BaseLoadingOverlay.d.ts +12 -0
- package/dist/types/components/createOGrid.d.ts +70 -0
- package/dist/types/hooks/useDataGridTableOrchestration.d.ts +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/types/dataGridTypes.d.ts +6 -0
- package/package.json +2 -2
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { getColumnHeaderMenuItems } from '../utils';
|
|
5
|
+
/**
|
|
6
|
+
* Base column header dropdown menu for pin/sort/autosize actions.
|
|
7
|
+
* Uses positioned div with portal rendering.
|
|
8
|
+
* Shared by Radix and Fluent UI packages (Material uses MUI Menu instead).
|
|
9
|
+
*/
|
|
10
|
+
export function BaseColumnHeaderMenu(props) {
|
|
11
|
+
const { isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, onSortAsc, onSortDesc, onClearSort, onAutosizeThis, onAutosizeAll, canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable, classNames, getPortalTarget, } = props;
|
|
12
|
+
const [position, setPosition] = React.useState(null);
|
|
13
|
+
const menuRef = React.useRef(null);
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
if (!isOpen || !anchorElement) {
|
|
16
|
+
setPosition(null);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const rect = anchorElement.getBoundingClientRect();
|
|
20
|
+
setPosition({
|
|
21
|
+
top: rect.bottom + 4,
|
|
22
|
+
left: rect.left,
|
|
23
|
+
});
|
|
24
|
+
const handleClickOutside = (e) => {
|
|
25
|
+
const target = e.target;
|
|
26
|
+
// Don't close if clicking inside the menu itself (portal) — let onClick fire first
|
|
27
|
+
if (menuRef.current && menuRef.current.contains(target))
|
|
28
|
+
return;
|
|
29
|
+
if (anchorElement && !anchorElement.contains(target)) {
|
|
30
|
+
onClose();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const handleEscape = (e) => {
|
|
34
|
+
if (e.key === 'Escape') {
|
|
35
|
+
onClose();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
39
|
+
document.addEventListener('keydown', handleEscape);
|
|
40
|
+
return () => {
|
|
41
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
42
|
+
document.removeEventListener('keydown', handleEscape);
|
|
43
|
+
};
|
|
44
|
+
}, [isOpen, anchorElement, onClose]);
|
|
45
|
+
const menuInput = React.useMemo(() => ({
|
|
46
|
+
canPinLeft,
|
|
47
|
+
canPinRight,
|
|
48
|
+
canUnpin,
|
|
49
|
+
currentSort,
|
|
50
|
+
isSortable,
|
|
51
|
+
isResizable,
|
|
52
|
+
}), [canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]);
|
|
53
|
+
const items = React.useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
|
|
54
|
+
const handlers = React.useMemo(() => ({
|
|
55
|
+
pinLeft: onPinLeft,
|
|
56
|
+
pinRight: onPinRight,
|
|
57
|
+
unpin: onUnpin,
|
|
58
|
+
sortAsc: onSortAsc,
|
|
59
|
+
sortDesc: onSortDesc,
|
|
60
|
+
clearSort: onClearSort,
|
|
61
|
+
autosizeThis: onAutosizeThis,
|
|
62
|
+
autosizeAll: onAutosizeAll,
|
|
63
|
+
}), [onPinLeft, onPinRight, onUnpin, onSortAsc, onSortDesc, onClearSort, onAutosizeThis, onAutosizeAll]);
|
|
64
|
+
if (!isOpen || !position)
|
|
65
|
+
return null;
|
|
66
|
+
const portalTarget = anchorElement && getPortalTarget
|
|
67
|
+
? getPortalTarget(anchorElement)
|
|
68
|
+
: document.body;
|
|
69
|
+
return createPortal(_jsx("div", { ref: menuRef, className: classNames?.content, style: {
|
|
70
|
+
position: 'fixed',
|
|
71
|
+
top: position.top,
|
|
72
|
+
left: position.left,
|
|
73
|
+
zIndex: 1000,
|
|
74
|
+
}, children: items.map((item, idx) => (_jsxs(React.Fragment, { children: [_jsx("button", { className: classNames?.item, disabled: item.disabled, onClick: () => {
|
|
75
|
+
handlers[item.id]();
|
|
76
|
+
onClose();
|
|
77
|
+
}, children: item.label }), item.divider && idx < items.length - 1 && (_jsx("div", { className: classNames?.separator }))] }, item.id))) }), portalTarget);
|
|
78
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function BaseEmptyState({ emptyState, classNames, icon }) {
|
|
3
|
+
return (_jsx("div", { className: classNames.emptyStateInGrid, children: _jsx("div", { className: classNames.emptyStateInGridInner, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [icon != null && (_jsx("span", { className: classNames.emptyStateInGridIcon, "aria-hidden": true, children: icon })), _jsx("div", { className: classNames.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: classNames.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: classNames.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function BaseLoadingOverlay({ message, classNames }) {
|
|
3
|
+
return (_jsx("div", { className: classNames.loadingOverlay, "aria-live": "polite", children: _jsxs("div", { className: classNames.loadingOverlayContent, children: [_jsx("div", { className: classNames.spinner }), _jsx("span", { className: classNames.loadingOverlayText, children: message })] }) }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { useOGrid } from '../hooks';
|
|
5
|
+
import { OGridLayout } from './OGridLayout';
|
|
6
|
+
/**
|
|
7
|
+
* Factory that creates a memoized, forwardRef OGrid component.
|
|
8
|
+
* Used by Radix and Fluent to avoid duplicating the same wiring code.
|
|
9
|
+
* Material uses its own OGrid because it adds MUI theme bridging (containerSx).
|
|
10
|
+
*/
|
|
11
|
+
export function createOGrid(components) {
|
|
12
|
+
const { DataGridTable, ColumnChooser, PaginationControls, containerComponent, containerProps, } = components;
|
|
13
|
+
const OGridInner = forwardRef(function OGridInner(props, ref) {
|
|
14
|
+
const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
|
|
15
|
+
return (_jsx(OGridLayout, { containerComponent: containerComponent, containerProps: containerProps, className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: (size) => {
|
|
16
|
+
pagination.setPageSize(size);
|
|
17
|
+
pagination.setPage(1);
|
|
18
|
+
}, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
|
|
19
|
+
});
|
|
20
|
+
OGridInner.displayName = 'OGrid';
|
|
21
|
+
return React.memo(OGridInner);
|
|
22
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useLayoutEffect, useCallback, useRef } from 'react';
|
|
1
|
+
import { useState, useLayoutEffect, useEffect, 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).
|
|
@@ -18,45 +18,60 @@ export function useActiveCell(wrapperRef, editingCell) {
|
|
|
18
18
|
return;
|
|
19
19
|
_setActiveCell(cell);
|
|
20
20
|
}, []);
|
|
21
|
-
//
|
|
22
|
-
|
|
21
|
+
// RAF ref for batching scroll-into-view during rapid keyboard navigation
|
|
22
|
+
const scrollRafRef = useRef(0);
|
|
23
|
+
// Synchronously focus the cell to prevent the browser from resetting
|
|
24
|
+
// focus to body between arrow presses.
|
|
23
25
|
useLayoutEffect(() => {
|
|
24
|
-
if (activeCell == null ||
|
|
25
|
-
wrapperRef?.current == null ||
|
|
26
|
-
editingCell != null)
|
|
26
|
+
if (activeCell == null || wrapperRef?.current == null || editingCell != null)
|
|
27
27
|
return;
|
|
28
28
|
const { rowIndex, columnIndex } = activeCell;
|
|
29
29
|
const selector = `[data-row-index="${rowIndex}"][data-col-index="${columnIndex}"]`;
|
|
30
30
|
const cell = wrapperRef.current.querySelector(selector);
|
|
31
|
-
if (cell) {
|
|
32
|
-
|
|
33
|
-
// use native scrollIntoView() which scrolls all ancestor containers
|
|
34
|
-
// including the page, causing an unwanted viewport jump.
|
|
35
|
-
const wrapper = wrapperRef.current;
|
|
36
|
-
const thead = wrapper.querySelector('thead');
|
|
37
|
-
const headerHeight = thead ? thead.getBoundingClientRect().height : 0;
|
|
38
|
-
const wrapperRect = wrapper.getBoundingClientRect();
|
|
39
|
-
const cellRect = cell.getBoundingClientRect();
|
|
40
|
-
// Vertical scroll (account for sticky thead)
|
|
41
|
-
const visibleTop = wrapperRect.top + headerHeight;
|
|
42
|
-
if (cellRect.top < visibleTop) {
|
|
43
|
-
wrapper.scrollTop -= visibleTop - cellRect.top;
|
|
44
|
-
}
|
|
45
|
-
else if (cellRect.bottom > wrapperRect.bottom) {
|
|
46
|
-
wrapper.scrollTop += cellRect.bottom - wrapperRect.bottom;
|
|
47
|
-
}
|
|
48
|
-
// Horizontal scroll
|
|
49
|
-
if (cellRect.left < wrapperRect.left) {
|
|
50
|
-
wrapper.scrollLeft -= wrapperRect.left - cellRect.left;
|
|
51
|
-
}
|
|
52
|
-
else if (cellRect.right > wrapperRect.right) {
|
|
53
|
-
wrapper.scrollLeft += cellRect.right - wrapperRect.right;
|
|
54
|
-
}
|
|
55
|
-
if (document.activeElement !== cell && typeof cell.focus === 'function') {
|
|
56
|
-
cell.focus({ preventScroll: true });
|
|
57
|
-
}
|
|
31
|
+
if (cell && document.activeElement !== cell && typeof cell.focus === 'function') {
|
|
32
|
+
cell.focus({ preventScroll: true });
|
|
58
33
|
}
|
|
59
34
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
35
|
}, [activeCell, editingCell]); // wrapperRef excluded — refs are stable across renders
|
|
36
|
+
// Batch scroll-into-view via RAF so rapid keyboard navigation only scrolls once
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (activeCell == null || wrapperRef?.current == null || editingCell != null)
|
|
39
|
+
return;
|
|
40
|
+
cancelAnimationFrame(scrollRafRef.current);
|
|
41
|
+
scrollRafRef.current = requestAnimationFrame(() => {
|
|
42
|
+
const wrapper = wrapperRef?.current;
|
|
43
|
+
if (!wrapper)
|
|
44
|
+
return;
|
|
45
|
+
const { rowIndex, columnIndex } = activeCell;
|
|
46
|
+
const selector = `[data-row-index="${rowIndex}"][data-col-index="${columnIndex}"]`;
|
|
47
|
+
const cell = wrapper.querySelector(selector);
|
|
48
|
+
if (cell) {
|
|
49
|
+
const thead = wrapper.querySelector('thead');
|
|
50
|
+
const headerHeight = thead ? thead.getBoundingClientRect().height : 0;
|
|
51
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
52
|
+
const cellRect = cell.getBoundingClientRect();
|
|
53
|
+
// Vertical scroll (account for sticky thead)
|
|
54
|
+
const visibleTop = wrapperRect.top + headerHeight;
|
|
55
|
+
if (cellRect.top < visibleTop) {
|
|
56
|
+
wrapper.scrollTop -= visibleTop - cellRect.top;
|
|
57
|
+
}
|
|
58
|
+
else if (cellRect.bottom > wrapperRect.bottom) {
|
|
59
|
+
wrapper.scrollTop += cellRect.bottom - wrapperRect.bottom;
|
|
60
|
+
}
|
|
61
|
+
// Horizontal scroll
|
|
62
|
+
if (cellRect.left < wrapperRect.left) {
|
|
63
|
+
wrapper.scrollLeft -= wrapperRect.left - cellRect.left;
|
|
64
|
+
}
|
|
65
|
+
else if (cellRect.right > wrapperRect.right) {
|
|
66
|
+
wrapper.scrollLeft += cellRect.right - wrapperRect.right;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, [activeCell, editingCell]); // wrapperRef excluded — refs are stable across renders
|
|
72
|
+
// Clean up pending RAF on unmount
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
return () => cancelAnimationFrame(scrollRafRef.current);
|
|
75
|
+
}, []);
|
|
61
76
|
return { activeCell, setActiveCell };
|
|
62
77
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
2
|
import { normalizeSelectionRange } from '../types';
|
|
3
3
|
import { rangesEqual, computeAutoScrollSpeed } from '../utils';
|
|
4
|
+
import { useLatestRef } from './useLatestRef';
|
|
4
5
|
/** DOM attribute names used for drag-range highlighting (bypasses React). */
|
|
5
6
|
const DRAG_ATTR = 'data-drag-range';
|
|
6
7
|
const DRAG_ANCHOR_ATTR = 'data-drag-anchor';
|
|
@@ -14,6 +15,8 @@ const AUTO_SCROLL_INTERVAL = 16; // ~60fps
|
|
|
14
15
|
*/
|
|
15
16
|
export function useCellSelection(params) {
|
|
16
17
|
const { colOffset, rowCount, visibleColCount, setActiveCell, wrapperRef } = params;
|
|
18
|
+
// Use ref for colOffset to prevent drag restart mid-drag when colOffset changes
|
|
19
|
+
const colOffsetRef = useLatestRef(colOffset);
|
|
17
20
|
const [selectionRange, _setSelectionRange] = useState(null);
|
|
18
21
|
const isDraggingRef = useRef(false);
|
|
19
22
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -39,11 +42,12 @@ export function useCellSelection(params) {
|
|
|
39
42
|
// Only handle primary (left) button — let middle-click scroll and right-click context menu work natively
|
|
40
43
|
if (e.button !== 0)
|
|
41
44
|
return;
|
|
42
|
-
|
|
45
|
+
const colOff = colOffsetRef.current;
|
|
46
|
+
if (globalColIndex < colOff)
|
|
43
47
|
return;
|
|
44
48
|
// Prevent native text selection during cell drag
|
|
45
49
|
e.preventDefault();
|
|
46
|
-
const dataColIndex = globalColIndex -
|
|
50
|
+
const dataColIndex = globalColIndex - colOff;
|
|
47
51
|
const currentRange = selectionRangeRef.current;
|
|
48
52
|
if (e.shiftKey && currentRange != null) {
|
|
49
53
|
setSelectionRange(normalizeSelectionRange({
|
|
@@ -75,8 +79,8 @@ export function useCellSelection(params) {
|
|
|
75
79
|
setTimeout(() => applyDragAttrsRef.current?.(initial), 0);
|
|
76
80
|
}
|
|
77
81
|
},
|
|
78
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectionRange is a
|
|
79
|
-
[
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectionRange is stable; colOffsetRef is a ref
|
|
83
|
+
[setActiveCell]);
|
|
80
84
|
const handleSelectAllCells = useCallback(() => {
|
|
81
85
|
if (rowCount === 0 || visibleColCount === 0)
|
|
82
86
|
return;
|
|
@@ -86,9 +90,9 @@ export function useCellSelection(params) {
|
|
|
86
90
|
endRow: rowCount - 1,
|
|
87
91
|
endCol: visibleColCount - 1,
|
|
88
92
|
});
|
|
89
|
-
setActiveCell({ rowIndex: 0, columnIndex:
|
|
90
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectionRange is a
|
|
91
|
-
}, [rowCount, visibleColCount,
|
|
93
|
+
setActiveCell({ rowIndex: 0, columnIndex: colOffsetRef.current });
|
|
94
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectionRange is stable; colOffsetRef is a ref
|
|
95
|
+
}, [rowCount, visibleColCount, setActiveCell]);
|
|
92
96
|
/** Last known mouse position during drag — used by mouseUp to flush pending RAF work. */
|
|
93
97
|
const lastMousePosRef = useRef(null);
|
|
94
98
|
// Ref to expose applyDragAttrs outside useEffect so it can be called from mouseDown
|
|
@@ -97,7 +101,6 @@ export function useCellSelection(params) {
|
|
|
97
101
|
// Performance: during drag, we update a ref + toggle DOM attributes via rAF.
|
|
98
102
|
// React state is only committed on mouseup (single re-render instead of 60-120/s).
|
|
99
103
|
useEffect(() => {
|
|
100
|
-
const colOff = colOffset; // capture for closure
|
|
101
104
|
/** Toggle DRAG_ATTR on cells to show the range highlight via CSS.
|
|
102
105
|
* Also sets edge box-shadows for a green border around the selection range,
|
|
103
106
|
* and marks the anchor cell with DRAG_ANCHOR_ATTR (white background). */
|
|
@@ -114,7 +117,7 @@ export function useCellSelection(params) {
|
|
|
114
117
|
for (let i = 0; i < cells.length; i++) {
|
|
115
118
|
const el = cells[i];
|
|
116
119
|
const r = parseInt(el.getAttribute('data-row-index'), 10);
|
|
117
|
-
const c = parseInt(el.getAttribute('data-col-index'), 10) -
|
|
120
|
+
const c = parseInt(el.getAttribute('data-col-index'), 10) - colOffsetRef.current;
|
|
118
121
|
const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
|
|
119
122
|
if (inRange) {
|
|
120
123
|
if (!el.hasAttribute(DRAG_ATTR))
|
|
@@ -175,6 +178,7 @@ export function useCellSelection(params) {
|
|
|
175
178
|
return null;
|
|
176
179
|
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
177
180
|
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
181
|
+
const colOff = colOffsetRef.current;
|
|
178
182
|
if (Number.isNaN(r) || Number.isNaN(c) || c < colOff)
|
|
179
183
|
return null;
|
|
180
184
|
const dataCol = c - colOff;
|
|
@@ -317,7 +321,7 @@ export function useCellSelection(params) {
|
|
|
317
321
|
setSelectionRange(finalRange);
|
|
318
322
|
setActiveCell({
|
|
319
323
|
rowIndex: finalRange.endRow,
|
|
320
|
-
columnIndex: finalRange.endCol +
|
|
324
|
+
columnIndex: finalRange.endCol + colOffsetRef.current,
|
|
321
325
|
});
|
|
322
326
|
}
|
|
323
327
|
}
|
|
@@ -341,7 +345,7 @@ export function useCellSelection(params) {
|
|
|
341
345
|
stopAutoScroll();
|
|
342
346
|
};
|
|
343
347
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
344
|
-
}, [
|
|
348
|
+
}, [setActiveCell]); // wrapperRef, colOffsetRef excluded — refs are stable across renders
|
|
345
349
|
return {
|
|
346
350
|
selectionRange,
|
|
347
351
|
setSelectionRange,
|
|
@@ -26,7 +26,7 @@ const NOOP_CTX = (_e) => { };
|
|
|
26
26
|
*/
|
|
27
27
|
export function useDataGridState(params) {
|
|
28
28
|
const { props, wrapperRef } = params;
|
|
29
|
-
const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, showRowNumbers, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, initialColumnWidths, onColumnResized, pinnedColumns, onColumnPinned, onCellError, } = props;
|
|
29
|
+
const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, showRowNumbers, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, initialColumnWidths, onColumnResized, onAutosizeColumn, pinnedColumns, onColumnPinned, onCellError, } = props;
|
|
30
30
|
const cellSelection = cellSelectionProp !== false;
|
|
31
31
|
// Wrap onCellValueChanged with undo/redo tracking — all edits are recorded automatically
|
|
32
32
|
const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp });
|
|
@@ -216,8 +216,8 @@ export function useDataGridState(params) {
|
|
|
216
216
|
// Autosize callback — updates internal column sizing state + notifies external listener
|
|
217
217
|
const handleAutosizeColumn = useCallback((columnId, width) => {
|
|
218
218
|
setColumnSizingOverrides((prev) => ({ ...prev, [columnId]: { widthPx: width } }));
|
|
219
|
-
onColumnResized?.(columnId, width);
|
|
220
|
-
}, [setColumnSizingOverrides, onColumnResized]);
|
|
219
|
+
(onAutosizeColumn ?? onColumnResized)?.(columnId, width);
|
|
220
|
+
}, [setColumnSizingOverrides, onAutosizeColumn, onColumnResized]);
|
|
221
221
|
const headerMenuResult = useColumnHeaderMenuState({
|
|
222
222
|
pinnedColumns: pinningResult.pinnedColumns,
|
|
223
223
|
onPinColumn: pinningResult.pinColumn,
|
|
@@ -386,37 +386,11 @@ export function useDataGridState(params) {
|
|
|
386
386
|
isPinned: pinningResult.isPinned,
|
|
387
387
|
leftOffsets,
|
|
388
388
|
rightOffsets,
|
|
389
|
-
headerMenu:
|
|
390
|
-
isOpen: headerMenuResult.isOpen,
|
|
391
|
-
openForColumn: headerMenuResult.openForColumn,
|
|
392
|
-
anchorElement: headerMenuResult.anchorElement,
|
|
393
|
-
open: headerMenuResult.open,
|
|
394
|
-
close: headerMenuResult.close,
|
|
395
|
-
handlePinLeft: headerMenuResult.handlePinLeft,
|
|
396
|
-
handlePinRight: headerMenuResult.handlePinRight,
|
|
397
|
-
handleUnpin: headerMenuResult.handleUnpin,
|
|
398
|
-
handleSortAsc: headerMenuResult.handleSortAsc,
|
|
399
|
-
handleSortDesc: headerMenuResult.handleSortDesc,
|
|
400
|
-
handleClearSort: headerMenuResult.handleClearSort,
|
|
401
|
-
handleAutosizeThis: headerMenuResult.handleAutosizeThis,
|
|
402
|
-
handleAutosizeAll: headerMenuResult.handleAutosizeAll,
|
|
403
|
-
canPinLeft: headerMenuResult.canPinLeft,
|
|
404
|
-
canPinRight: headerMenuResult.canPinRight,
|
|
405
|
-
canUnpin: headerMenuResult.canUnpin,
|
|
406
|
-
currentSort: headerMenuResult.currentSort,
|
|
407
|
-
isSortable: headerMenuResult.isSortable,
|
|
408
|
-
isResizable: headerMenuResult.isResizable,
|
|
409
|
-
},
|
|
389
|
+
headerMenu: headerMenuResult,
|
|
410
390
|
}), [
|
|
411
391
|
pinningResult.pinnedColumns, pinningResult.pinColumn, pinningResult.unpinColumn,
|
|
412
392
|
pinningResult.isPinned, leftOffsets, rightOffsets,
|
|
413
|
-
headerMenuResult
|
|
414
|
-
headerMenuResult.open, headerMenuResult.close, headerMenuResult.handlePinLeft,
|
|
415
|
-
headerMenuResult.handlePinRight, headerMenuResult.handleUnpin,
|
|
416
|
-
headerMenuResult.handleSortAsc, headerMenuResult.handleSortDesc, headerMenuResult.handleClearSort,
|
|
417
|
-
headerMenuResult.handleAutosizeThis, headerMenuResult.handleAutosizeAll,
|
|
418
|
-
headerMenuResult.canPinLeft, headerMenuResult.canPinRight, headerMenuResult.canUnpin,
|
|
419
|
-
headerMenuResult.currentSort, headerMenuResult.isSortable, headerMenuResult.isResizable,
|
|
393
|
+
headerMenuResult,
|
|
420
394
|
]);
|
|
421
395
|
return {
|
|
422
396
|
layout: layoutState,
|
|
@@ -35,7 +35,7 @@ export function useDataGridTableOrchestration(params) {
|
|
|
35
35
|
const { headerMenu } = pinning;
|
|
36
36
|
const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
|
|
37
37
|
// ── Props destructuring ─────────────────────────────────────────────────
|
|
38
|
-
const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
|
|
38
|
+
const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, rowHeight, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
|
|
39
39
|
// ── Derived values ──────────────────────────────────────────────────────
|
|
40
40
|
const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
|
|
41
41
|
const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
|
|
@@ -126,6 +126,7 @@ export function useDataGridTableOrchestration(params) {
|
|
|
126
126
|
columnOrder,
|
|
127
127
|
columnReorder,
|
|
128
128
|
density,
|
|
129
|
+
rowHeight,
|
|
129
130
|
pinnedColumns,
|
|
130
131
|
currentPage,
|
|
131
132
|
propPageSize,
|
|
@@ -11,7 +11,7 @@ const EMPTY_LOADING_OPTIONS = {};
|
|
|
11
11
|
* @returns Grouped props for DataGridTable, pagination controls, column chooser, layout, and filters.
|
|
12
12
|
*/
|
|
13
13
|
export function useOGrid(props, ref) {
|
|
14
|
-
const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, showRowNumbers, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, columnReorder, virtualScroll, density = 'normal', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
|
|
14
|
+
const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, showRowNumbers, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, columnReorder, virtualScroll, rowHeight, density = 'normal', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
|
|
15
15
|
// Resolve column chooser placement
|
|
16
16
|
const columnChooserPlacement = columnChooserProp === false ? 'none'
|
|
17
17
|
: columnChooserProp === 'sidebar' ? 'sidebar'
|
|
@@ -105,10 +105,29 @@ export function useOGrid(props, ref) {
|
|
|
105
105
|
return deriveFilterOptionsFromData(displayData, columns);
|
|
106
106
|
}, [hasServerFilterOptions, displayData, columns, serverFilterOptions]);
|
|
107
107
|
// --- Client-side filtering & sorting ---
|
|
108
|
+
// Stabilize filters ref via shallow comparison so processClientSideData useMemo
|
|
109
|
+
// doesn't re-run when the filter object reference changes but values are identical.
|
|
110
|
+
const stableFiltersRef = useRef(filters);
|
|
111
|
+
const stableFilters = useMemo(() => {
|
|
112
|
+
const prev = stableFiltersRef.current;
|
|
113
|
+
const prevKeys = Object.keys(prev);
|
|
114
|
+
const nextKeys = Object.keys(filters);
|
|
115
|
+
if (prevKeys.length !== nextKeys.length) {
|
|
116
|
+
stableFiltersRef.current = filters;
|
|
117
|
+
return filters;
|
|
118
|
+
}
|
|
119
|
+
for (let i = 0; i < nextKeys.length; i++) {
|
|
120
|
+
if (prev[nextKeys[i]] !== filters[nextKeys[i]]) {
|
|
121
|
+
stableFiltersRef.current = filters;
|
|
122
|
+
return filters;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return prev;
|
|
126
|
+
}, [filters]);
|
|
108
127
|
const clientItemsAndTotal = useMemo(() => {
|
|
109
128
|
if (!isClientSide)
|
|
110
129
|
return null;
|
|
111
|
-
const rows = processClientSideData(displayData, columns,
|
|
130
|
+
const rows = processClientSideData(displayData, columns, stableFilters, sort.field, sort.direction);
|
|
112
131
|
const total = rows.length;
|
|
113
132
|
const start = (page - 1) * pageSize;
|
|
114
133
|
const paged = rows.slice(start, start + pageSize);
|
|
@@ -117,7 +136,7 @@ export function useOGrid(props, ref) {
|
|
|
117
136
|
isClientSide,
|
|
118
137
|
displayData,
|
|
119
138
|
columns,
|
|
120
|
-
|
|
139
|
+
stableFilters,
|
|
121
140
|
sort.field,
|
|
122
141
|
sort.direction,
|
|
123
142
|
page,
|
|
@@ -420,6 +439,7 @@ export function useOGrid(props, ref) {
|
|
|
420
439
|
suppressHorizontalScroll,
|
|
421
440
|
columnReorder,
|
|
422
441
|
virtualScroll,
|
|
442
|
+
rowHeight,
|
|
423
443
|
density,
|
|
424
444
|
'aria-label': ariaLabel,
|
|
425
445
|
'aria-labelledby': ariaLabelledBy,
|
|
@@ -437,7 +457,7 @@ export function useOGrid(props, ref) {
|
|
|
437
457
|
rowSelection, effectiveSelectedRows, handleSelectionChange, showRowNumbers, page, pageSize, statusBarConfig,
|
|
438
458
|
isLoadingResolved, filters, handleFilterChange, clientFilterOptions, dataSource,
|
|
439
459
|
loadingFilterOptions, layoutMode, suppressHorizontalScroll, columnReorder, virtualScroll,
|
|
440
|
-
density, ariaLabel, ariaLabelledBy,
|
|
460
|
+
rowHeight, density, ariaLabel, ariaLabelledBy,
|
|
441
461
|
hasActiveFilters, clearAllFilters, emptyState,
|
|
442
462
|
]);
|
|
443
463
|
const pagination = useMemo(() => ({
|
|
@@ -67,8 +67,17 @@ export function useRowSelection(params) {
|
|
|
67
67
|
updateSelection(new Set());
|
|
68
68
|
}
|
|
69
69
|
}, [items, getRowId, updateSelection]);
|
|
70
|
-
const allSelected = useMemo(() =>
|
|
71
|
-
|
|
70
|
+
const allSelected = useMemo(() => {
|
|
71
|
+
if (selectedRowIds.size === 0 || items.length === 0)
|
|
72
|
+
return false;
|
|
73
|
+
return items.every((item) => selectedRowIds.has(getRowId(item)));
|
|
74
|
+
}, [items, selectedRowIds, getRowId]);
|
|
75
|
+
const someSelected = useMemo(() => {
|
|
76
|
+
if (allSelected)
|
|
77
|
+
return false;
|
|
78
|
+
// No iteration needed — any selected row means "some" are selected
|
|
79
|
+
return selectedRowIds.size > 0;
|
|
80
|
+
}, [allSelected, selectedRowIds.size]);
|
|
72
81
|
return {
|
|
73
82
|
selectedRowIds,
|
|
74
83
|
updateSelection,
|
package/dist/esm/index.js
CHANGED
|
@@ -12,8 +12,13 @@ export { BaseInlineCellEditor, editorWrapperStyle, editorInputStyle, richSelectW
|
|
|
12
12
|
export { GridContextMenu } from './components/GridContextMenu';
|
|
13
13
|
export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
|
|
14
14
|
export { SideBar } from './components/SideBar';
|
|
15
|
+
export { BaseColumnHeaderMenu } from './components/BaseColumnHeaderMenu';
|
|
16
|
+
export { createOGrid } from './components/createOGrid';
|
|
15
17
|
export { CellErrorBoundary } from './components/CellErrorBoundary';
|
|
16
18
|
export { EmptyState } from './components/EmptyState';
|
|
19
|
+
export { BaseEmptyState } from './components/BaseEmptyState';
|
|
20
|
+
export { BaseLoadingOverlay } from './components/BaseLoadingOverlay';
|
|
21
|
+
export { BaseDropIndicator } from './components/BaseDropIndicator';
|
|
17
22
|
export { DateFilterContent, getColumnHeaderFilterStateParams, getDateFilterContentProps, } from './components/ColumnHeaderFilterContent';
|
|
18
23
|
// Utilities
|
|
19
24
|
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, isRowInRange, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, areGridRowPropsEqual, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, UndoRedoStack, } from './utils';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface ColumnHeaderMenuClassNames {
|
|
3
|
+
content?: string;
|
|
4
|
+
item?: string;
|
|
5
|
+
separator?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BaseColumnHeaderMenuProps {
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
anchorElement: HTMLElement | null;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onPinLeft: () => void;
|
|
12
|
+
onPinRight: () => void;
|
|
13
|
+
onUnpin: () => void;
|
|
14
|
+
onSortAsc: () => void;
|
|
15
|
+
onSortDesc: () => void;
|
|
16
|
+
onClearSort: () => void;
|
|
17
|
+
onAutosizeThis: () => void;
|
|
18
|
+
onAutosizeAll: () => void;
|
|
19
|
+
canPinLeft: boolean;
|
|
20
|
+
canPinRight: boolean;
|
|
21
|
+
canUnpin: boolean;
|
|
22
|
+
currentSort: 'asc' | 'desc' | null;
|
|
23
|
+
isSortable: boolean;
|
|
24
|
+
isResizable: boolean;
|
|
25
|
+
classNames?: ColumnHeaderMenuClassNames;
|
|
26
|
+
/** Resolve the portal target element. Defaults to document.body. */
|
|
27
|
+
getPortalTarget?: (anchorElement: HTMLElement) => HTMLElement;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Base column header dropdown menu for pin/sort/autosize actions.
|
|
31
|
+
* Uses positioned div with portal rendering.
|
|
32
|
+
* Shared by Radix and Fluent UI packages (Material uses MUI Menu instead).
|
|
33
|
+
*/
|
|
34
|
+
export declare function BaseColumnHeaderMenu(props: BaseColumnHeaderMenuProps): React.ReactPortal | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface BaseDropIndicatorProps {
|
|
3
|
+
dropIndicatorX: number;
|
|
4
|
+
wrapperLeft: number;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function BaseDropIndicator({ dropIndicatorX, wrapperLeft, className }: BaseDropIndicatorProps): React.ReactElement;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface BaseEmptyStateClassNames {
|
|
3
|
+
emptyStateInGrid?: string;
|
|
4
|
+
emptyStateInGridInner?: string;
|
|
5
|
+
emptyStateInGridIcon?: string;
|
|
6
|
+
emptyStateInGridTitle?: string;
|
|
7
|
+
emptyStateInGridMessage?: string;
|
|
8
|
+
emptyStateInGridLink?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface BaseEmptyStateProps {
|
|
11
|
+
emptyState: {
|
|
12
|
+
render?: () => React.ReactNode;
|
|
13
|
+
message?: React.ReactNode;
|
|
14
|
+
hasActiveFilters?: boolean;
|
|
15
|
+
onClearAll?: () => void;
|
|
16
|
+
};
|
|
17
|
+
classNames: BaseEmptyStateClassNames;
|
|
18
|
+
/** Optional icon rendered above the title (e.g. emoji or SVG) */
|
|
19
|
+
icon?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export declare function BaseEmptyState({ emptyState, classNames, icon }: BaseEmptyStateProps): React.ReactElement;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface BaseLoadingOverlayClassNames {
|
|
3
|
+
loadingOverlay?: string;
|
|
4
|
+
loadingOverlayContent?: string;
|
|
5
|
+
spinner?: string;
|
|
6
|
+
loadingOverlayText?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BaseLoadingOverlayProps {
|
|
9
|
+
message: string;
|
|
10
|
+
classNames: BaseLoadingOverlayClassNames;
|
|
11
|
+
}
|
|
12
|
+
export declare function BaseLoadingOverlay({ message, classNames }: BaseLoadingOverlayProps): React.ReactElement;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IOGridProps, IOGridApi, IOGridDataGridProps } from '../types';
|
|
3
|
+
import type { IColumnDef } from '../types';
|
|
4
|
+
import type { IColumnChooserProps } from './ColumnChooserProps';
|
|
5
|
+
import type { IPaginationControlsProps } from './PaginationControlsProps';
|
|
6
|
+
export interface InlineCellEditorProps<T> {
|
|
7
|
+
value: unknown;
|
|
8
|
+
item: T;
|
|
9
|
+
column: IColumnDef<T>;
|
|
10
|
+
rowIndex: number;
|
|
11
|
+
editorType: 'text' | 'select' | 'checkbox' | 'richSelect' | 'date';
|
|
12
|
+
onCommit: (value: unknown) => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
export interface GridRowProps {
|
|
16
|
+
item: unknown;
|
|
17
|
+
rowIndex: number;
|
|
18
|
+
rowId: string | number;
|
|
19
|
+
isSelected: boolean;
|
|
20
|
+
visibleCols: IColumnDef<unknown>[];
|
|
21
|
+
columnMeta: {
|
|
22
|
+
cellStyles: Record<string, React.CSSProperties>;
|
|
23
|
+
cellClasses: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
renderCellContent: (item: unknown, col: IColumnDef<unknown>, rowIndex: number, colIdx: number) => React.ReactNode;
|
|
26
|
+
handleSingleRowClick: (e: React.MouseEvent<HTMLTableRowElement>) => void;
|
|
27
|
+
handleRowCheckboxChange: (rowId: string | number, checked: boolean, rowIndex: number, shiftKey: boolean) => void;
|
|
28
|
+
lastMouseShiftRef: React.MutableRefObject<boolean>;
|
|
29
|
+
hasCheckboxCol: boolean;
|
|
30
|
+
hasRowNumbersCol: boolean;
|
|
31
|
+
rowNumberOffset: number;
|
|
32
|
+
selectionRange: {
|
|
33
|
+
startRow: number;
|
|
34
|
+
endRow: number;
|
|
35
|
+
startCol: number;
|
|
36
|
+
endCol: number;
|
|
37
|
+
} | null;
|
|
38
|
+
activeCell: {
|
|
39
|
+
rowIndex: number;
|
|
40
|
+
columnIndex: number;
|
|
41
|
+
} | null;
|
|
42
|
+
cutRange: {
|
|
43
|
+
startRow: number;
|
|
44
|
+
endRow: number;
|
|
45
|
+
startCol: number;
|
|
46
|
+
endCol: number;
|
|
47
|
+
} | null;
|
|
48
|
+
copyRange: {
|
|
49
|
+
startRow: number;
|
|
50
|
+
endRow: number;
|
|
51
|
+
startCol: number;
|
|
52
|
+
endCol: number;
|
|
53
|
+
} | null;
|
|
54
|
+
isDragging: boolean;
|
|
55
|
+
editingRowId: string | number | null;
|
|
56
|
+
}
|
|
57
|
+
export interface CreateOGridComponents {
|
|
58
|
+
DataGridTable: React.ComponentType<IOGridDataGridProps<unknown>>;
|
|
59
|
+
ColumnChooser: React.ComponentType<IColumnChooserProps>;
|
|
60
|
+
PaginationControls: React.ComponentType<IPaginationControlsProps>;
|
|
61
|
+
/** Optional wrapper component + props (e.g. MUI Box with sx). */
|
|
62
|
+
containerComponent?: React.ElementType;
|
|
63
|
+
containerProps?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Factory that creates a memoized, forwardRef OGrid component.
|
|
67
|
+
* Used by Radix and Fluent to avoid duplicating the same wiring code.
|
|
68
|
+
* Material uses its own OGrid because it adds MUI theme bridging (containerSx).
|
|
69
|
+
*/
|
|
70
|
+
export declare function createOGrid(components: CreateOGridComponents): React.ForwardRefExoticComponent<IOGridProps<unknown> & React.RefAttributes<IOGridApi<unknown>>>;
|
|
@@ -46,6 +46,7 @@ export interface UseDataGridTableOrchestrationResult<T> {
|
|
|
46
46
|
columnOrder: IOGridDataGridProps<T>['columnOrder'];
|
|
47
47
|
columnReorder: IOGridDataGridProps<T>['columnReorder'];
|
|
48
48
|
density: 'compact' | 'normal' | 'comfortable';
|
|
49
|
+
rowHeight: number | undefined;
|
|
49
50
|
pinnedColumns: IOGridDataGridProps<T>['pinnedColumns'];
|
|
50
51
|
currentPage: number;
|
|
51
52
|
propPageSize: number;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,10 +16,20 @@ export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
|
|
|
16
16
|
export type { MarchingAntsOverlayProps } from './components/MarchingAntsOverlay';
|
|
17
17
|
export { SideBar } from './components/SideBar';
|
|
18
18
|
export type { SideBarProps, SideBarFilterColumn } from './components/SideBar';
|
|
19
|
+
export { BaseColumnHeaderMenu } from './components/BaseColumnHeaderMenu';
|
|
20
|
+
export type { BaseColumnHeaderMenuProps, ColumnHeaderMenuClassNames } from './components/BaseColumnHeaderMenu';
|
|
21
|
+
export { createOGrid } from './components/createOGrid';
|
|
22
|
+
export type { CreateOGridComponents, GridRowProps, InlineCellEditorProps } from './components/createOGrid';
|
|
19
23
|
export { CellErrorBoundary } from './components/CellErrorBoundary';
|
|
20
24
|
export type { CellErrorBoundaryProps } from './components/CellErrorBoundary';
|
|
21
25
|
export { EmptyState } from './components/EmptyState';
|
|
22
26
|
export type { EmptyStateProps } from './components/EmptyState';
|
|
27
|
+
export { BaseEmptyState } from './components/BaseEmptyState';
|
|
28
|
+
export type { BaseEmptyStateProps, BaseEmptyStateClassNames } from './components/BaseEmptyState';
|
|
29
|
+
export { BaseLoadingOverlay } from './components/BaseLoadingOverlay';
|
|
30
|
+
export type { BaseLoadingOverlayProps, BaseLoadingOverlayClassNames } from './components/BaseLoadingOverlay';
|
|
31
|
+
export { BaseDropIndicator } from './components/BaseDropIndicator';
|
|
32
|
+
export type { BaseDropIndicatorProps } from './components/BaseDropIndicator';
|
|
23
33
|
export { DateFilterContent, getColumnHeaderFilterStateParams, getDateFilterContentProps, } from './components/ColumnHeaderFilterContent';
|
|
24
34
|
export type { IColumnHeaderFilterProps, DateFilterContentProps, DateFilterClassNames, } from './components/ColumnHeaderFilterContent';
|
|
25
35
|
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, isRowInRange, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, areGridRowPropsEqual, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, UndoRedoStack, } from './utils';
|
|
@@ -72,6 +72,8 @@ interface IOGridBaseProps<T> {
|
|
|
72
72
|
columnReorder?: boolean;
|
|
73
73
|
/** Virtual scrolling configuration. When provided, only visible rows are rendered for large datasets. */
|
|
74
74
|
virtualScroll?: IVirtualScrollConfig;
|
|
75
|
+
/** Fixed row height in pixels. Overrides default row height (36px). */
|
|
76
|
+
rowHeight?: number;
|
|
75
77
|
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
76
78
|
density?: 'compact' | 'normal' | 'comfortable';
|
|
77
79
|
/** Fires once when the grid first renders with data (useful for restoring column state). */
|
|
@@ -110,6 +112,8 @@ export interface IOGridDataGridProps<T> {
|
|
|
110
112
|
onColumnOrderChange?: (order: string[]) => void;
|
|
111
113
|
/** Called when a column is resized by the user. */
|
|
112
114
|
onColumnResized?: (columnId: string, width: number) => void;
|
|
115
|
+
/** Called when user requests autosize for a single column (with measured width). */
|
|
116
|
+
onAutosizeColumn?: (columnId: string, width: number) => void;
|
|
113
117
|
/** Called when a column is pinned or unpinned. */
|
|
114
118
|
onColumnPinned?: (columnId: string, pinned: 'left' | 'right' | null) => void;
|
|
115
119
|
/** Runtime pin overrides (from restored state or programmatic changes). */
|
|
@@ -157,6 +161,8 @@ export interface IOGridDataGridProps<T> {
|
|
|
157
161
|
columnReorder?: boolean;
|
|
158
162
|
/** Virtual scrolling configuration. When provided, only visible rows are rendered for large datasets. */
|
|
159
163
|
virtualScroll?: IVirtualScrollConfig;
|
|
164
|
+
/** Fixed row height in pixels. Overrides default row height (36px). */
|
|
165
|
+
rowHeight?: number;
|
|
160
166
|
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
161
167
|
density?: 'compact' | 'normal' | 'comfortable';
|
|
162
168
|
/** Called when a cell renderer or custom editor throws an error. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-react",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.21",
|
|
4
4
|
"description": "OGrid React – React hooks, headless components, and utilities for OGrid data grids.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@alaarab/ogrid-core": "2.0.
|
|
39
|
+
"@alaarab/ogrid-core": "2.0.21"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@tanstack/react-virtual": "^3.0.0",
|