@alaarab/ogrid-react-material 2.0.2 → 2.0.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.
- package/dist/esm/ColumnHeaderMenu/ColumnHeaderMenu.js +39 -0
- package/dist/esm/ColumnHeaderMenu/index.js +1 -0
- package/dist/esm/DataGridTable/DataGridTable.js +129 -19
- package/dist/esm/DataGridTable/GridContextMenu.js +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/types/ColumnHeaderMenu/ColumnHeaderMenu.d.ts +17 -0
- package/dist/types/ColumnHeaderMenu/index.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import Menu from '@mui/material/Menu';
|
|
4
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
5
|
+
import IconButton from '@mui/material/IconButton';
|
|
6
|
+
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
7
|
+
import { COLUMN_HEADER_MENU_ITEMS } from '@alaarab/ogrid-core';
|
|
8
|
+
/**
|
|
9
|
+
* Column header dropdown menu for pin/unpin actions.
|
|
10
|
+
* Uses Material UI Menu component.
|
|
11
|
+
*/
|
|
12
|
+
export function ColumnHeaderMenu(props) {
|
|
13
|
+
const { columnId, isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, canPinLeft, canPinRight, canUnpin, } = props;
|
|
14
|
+
const [triggerEl, setTriggerEl] = React.useState(null);
|
|
15
|
+
const handleTriggerClick = (event) => {
|
|
16
|
+
event.stopPropagation();
|
|
17
|
+
setTriggerEl(event.currentTarget);
|
|
18
|
+
};
|
|
19
|
+
return (_jsxs(_Fragment, { children: [_jsx(IconButton, { size: "small", onClick: handleTriggerClick, "aria-label": `Column options for ${columnId}`, sx: {
|
|
20
|
+
opacity: 0,
|
|
21
|
+
transition: 'opacity 0.15s',
|
|
22
|
+
padding: '4px',
|
|
23
|
+
'.MuiTableCell-root:hover &': {
|
|
24
|
+
opacity: 1,
|
|
25
|
+
},
|
|
26
|
+
'&:focus': {
|
|
27
|
+
opacity: 1,
|
|
28
|
+
},
|
|
29
|
+
}, children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsxs(Menu, { anchorEl: triggerEl, open: Boolean(triggerEl), onClose: () => {
|
|
30
|
+
setTriggerEl(null);
|
|
31
|
+
onClose();
|
|
32
|
+
}, anchorOrigin: {
|
|
33
|
+
vertical: 'bottom',
|
|
34
|
+
horizontal: 'left',
|
|
35
|
+
}, transformOrigin: {
|
|
36
|
+
vertical: 'top',
|
|
37
|
+
horizontal: 'left',
|
|
38
|
+
}, children: [_jsx(MenuItem, { disabled: !canPinLeft, onClick: onPinLeft, children: COLUMN_HEADER_MENU_ITEMS[0].label }), _jsx(MenuItem, { disabled: !canPinRight, onClick: onPinRight, children: COLUMN_HEADER_MENU_ITEMS[1].label }), _jsx(MenuItem, { disabled: !canUnpin, onClick: onUnpin, children: COLUMN_HEADER_MENU_ITEMS[2].label })] })] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ColumnHeaderMenu } from './ColumnHeaderMenu';
|
|
@@ -4,10 +4,11 @@ import { useCallback, useRef, useMemo } from 'react';
|
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { Box, CircularProgress, Typography, Button, Popover, Checkbox, Table, TableHead, TableBody, TableRow, TableCell, TableContainer, } from '@mui/material';
|
|
6
6
|
import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
|
|
7
|
+
import { ColumnHeaderMenu } from '../ColumnHeaderMenu';
|
|
7
8
|
import { InlineCellEditor } from './InlineCellEditor';
|
|
8
9
|
import { StatusBar } from './StatusBar';
|
|
9
10
|
import { GridContextMenu } from './GridContextMenu';
|
|
10
|
-
import { useDataGridState, useColumnResize, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, buildHeaderRows, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
|
|
11
|
+
import { useDataGridState, useColumnResize, useColumnReorder, useVirtualScroll, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, buildHeaderRows, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
|
|
11
12
|
// ── Module-scope stable styles (avoid per-render Emotion resolutions) ──
|
|
12
13
|
const gridRootSx = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
|
|
13
14
|
// Row
|
|
@@ -20,6 +21,14 @@ const CHECKBOX_PLACEHOLDER_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKB
|
|
|
20
21
|
const STICKY_HEADER_SX = { position: 'sticky', top: 0, zIndex: 8, bgcolor: 'action.hover', '& th': { bgcolor: 'action.hover' } };
|
|
21
22
|
const HEADER_ROW_SX = { bgcolor: 'action.hover' };
|
|
22
23
|
const GROUP_HEADER_CELL_SX = { textAlign: 'center', fontWeight: 600, borderBottom: 2, borderColor: 'divider', py: 0.75 };
|
|
24
|
+
// Density padding helper
|
|
25
|
+
function getDensityPadding(density) {
|
|
26
|
+
switch (density) {
|
|
27
|
+
case 'compact': return { px: '8px', py: '4px' };
|
|
28
|
+
case 'comfortable': return { px: '16px', py: '12px' };
|
|
29
|
+
default: return { px: '10px', py: '6px' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
23
32
|
// Cell content base variants (selected by column type + editability)
|
|
24
33
|
const CELL_CONTENT_BASE_SX = {
|
|
25
34
|
width: '100%', height: '100%', display: 'flex', alignItems: 'center', minWidth: 0,
|
|
@@ -89,12 +98,12 @@ const FILL_HANDLE_SX = {
|
|
|
89
98
|
};
|
|
90
99
|
// Cell <td> positioning variants
|
|
91
100
|
const CELL_TD_BASE_SX = { position: 'relative', p: 0, height: '1px' };
|
|
92
|
-
const CELL_TD_PINNED_LEFT_SX = { ...CELL_TD_BASE_SX, position: 'sticky', left: 0, zIndex: 6, bgcolor: 'background.paper', willChange: 'transform' };
|
|
93
|
-
const CELL_TD_PINNED_RIGHT_SX = { ...CELL_TD_BASE_SX, position: 'sticky', right: 0, zIndex: 6, bgcolor: 'background.paper', willChange: 'transform' };
|
|
101
|
+
const CELL_TD_PINNED_LEFT_SX = { ...CELL_TD_BASE_SX, position: 'sticky', left: 0, zIndex: 6, bgcolor: 'background.paper', willChange: 'transform', borderLeft: '2px solid', borderLeftColor: 'primary.main' };
|
|
102
|
+
const CELL_TD_PINNED_RIGHT_SX = { ...CELL_TD_BASE_SX, position: 'sticky', right: 0, zIndex: 6, bgcolor: 'background.paper', willChange: 'transform', borderRight: '2px solid', borderRightColor: 'primary.main' };
|
|
94
103
|
// Header cell positioning variants
|
|
95
104
|
const HEADER_BASE_SX = { fontWeight: 600, position: 'relative' };
|
|
96
|
-
const HEADER_PINNED_LEFT_SX = { ...HEADER_BASE_SX, position: 'sticky', left: 0, top: 0, zIndex: 9, bgcolor: 'action.hover', willChange: 'transform' };
|
|
97
|
-
const HEADER_PINNED_RIGHT_SX = { ...HEADER_BASE_SX, position: 'sticky', right: 0, top: 0, zIndex: 9, bgcolor: 'action.hover', willChange: 'transform' };
|
|
105
|
+
const HEADER_PINNED_LEFT_SX = { ...HEADER_BASE_SX, position: 'sticky', left: 0, top: 0, zIndex: 9, bgcolor: 'action.hover', willChange: 'transform', borderLeft: '2px solid', borderLeftColor: 'primary.main' };
|
|
106
|
+
const HEADER_PINNED_RIGHT_SX = { ...HEADER_BASE_SX, position: 'sticky', right: 0, top: 0, zIndex: 9, bgcolor: 'action.hover', willChange: 'transform', borderRight: '2px solid', borderRightColor: 'primary.main' };
|
|
98
107
|
// Resize handle
|
|
99
108
|
const RESIZE_HANDLE_SX = {
|
|
100
109
|
position: 'absolute', top: 0, right: '-3px', bottom: 0, width: '8px',
|
|
@@ -117,7 +126,7 @@ const EMPTY_STATE_SX = { py: 4, px: 2, textAlign: 'center', borderTop: 1, border
|
|
|
117
126
|
const LOADING_OVERLAY_SX = {
|
|
118
127
|
position: 'absolute', inset: 0, zIndex: 2,
|
|
119
128
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
120
|
-
|
|
129
|
+
background: 'var(--ogrid-loading-bg, rgba(255,255,255,0.7))',
|
|
121
130
|
};
|
|
122
131
|
const LOADING_INNER_SX = {
|
|
123
132
|
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
|
|
@@ -128,8 +137,20 @@ const STOP_PROPAGATION = (e) => e.stopPropagation();
|
|
|
128
137
|
const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
|
|
129
138
|
const NOOP = () => { };
|
|
130
139
|
function GridRowInner(props) {
|
|
131
|
-
const { item, rowIndex, rowId, isSelected, columnLayouts, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, } = props;
|
|
132
|
-
return (_jsxs(TableRow, { selected: isSelected, "data-row-id": rowId, onClick: handleSingleRowClick, sx: ROW_HOVER_SX, children: [hasCheckboxCol && (_jsx(TableCell, { padding: "checkbox", sx: CHECKBOX_CELL_SX, children: _jsx(Box, { "data-row-index": rowIndex, "data-col-index": 0, onClick: STOP_PROPAGATION, sx: CHECKBOX_WRAPPER_SX, children: _jsx(Checkbox, { checked: isSelected, onChange: (_, checked) => handleRowCheckboxChange(rowId, checked, rowIndex, lastMouseShiftRef.current), size: "small", "aria-label": `Select row ${rowIndex + 1}` }) }) })),
|
|
140
|
+
const { item, rowIndex, rowId, isSelected, columnLayouts, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, hasRowNumbersCol, rowNumberOffset, } = props;
|
|
141
|
+
return (_jsxs(TableRow, { selected: isSelected, "data-row-id": rowId, onClick: handleSingleRowClick, sx: ROW_HOVER_SX, children: [hasCheckboxCol && (_jsx(TableCell, { padding: "checkbox", sx: CHECKBOX_CELL_SX, children: _jsx(Box, { "data-row-index": rowIndex, "data-col-index": 0, onClick: STOP_PROPAGATION, sx: CHECKBOX_WRAPPER_SX, children: _jsx(Checkbox, { checked: isSelected, onChange: (_, checked) => handleRowCheckboxChange(rowId, checked, rowIndex, lastMouseShiftRef.current), size: "small", "aria-label": `Select row ${rowIndex + 1}` }) }) })), hasRowNumbersCol && (_jsx(TableCell, { sx: {
|
|
142
|
+
width: ROW_NUMBER_COLUMN_WIDTH,
|
|
143
|
+
minWidth: ROW_NUMBER_COLUMN_WIDTH,
|
|
144
|
+
maxWidth: ROW_NUMBER_COLUMN_WIDTH,
|
|
145
|
+
textAlign: 'center',
|
|
146
|
+
fontWeight: 600,
|
|
147
|
+
fontVariantNumeric: 'tabular-nums',
|
|
148
|
+
color: 'text.secondary',
|
|
149
|
+
backgroundColor: 'action.hover',
|
|
150
|
+
position: 'sticky',
|
|
151
|
+
left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
|
|
152
|
+
zIndex: 3,
|
|
153
|
+
}, children: rowNumberOffset + rowIndex + 1 })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { sx: cl.tdSx, style: { minWidth: cl.minWidth, width: cl.width, maxWidth: cl.maxWidth }, children: renderCellContent(item, cl.col, rowIndex, colIdx) }, cl.col.columnId)))] }));
|
|
133
154
|
}
|
|
134
155
|
const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
|
|
135
156
|
function DataGridTableInner(props) {
|
|
@@ -137,23 +158,46 @@ function DataGridTableInner(props) {
|
|
|
137
158
|
const tableContainerRef = useRef(null);
|
|
138
159
|
const state = useDataGridState({ props, wrapperRef });
|
|
139
160
|
const lastMouseShiftRef = useRef(false);
|
|
140
|
-
const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels } = state;
|
|
141
|
-
const { visibleCols, hasCheckboxCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
|
|
161
|
+
const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels, pinning } = state;
|
|
162
|
+
const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
|
|
142
163
|
const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
143
164
|
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
144
165
|
const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
|
|
145
166
|
const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
|
|
146
167
|
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
147
168
|
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
|
|
148
|
-
const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
|
|
169
|
+
const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
|
|
170
|
+
// Calculate row number offset for pagination
|
|
171
|
+
const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
|
|
149
172
|
const fitToContent = layoutMode === 'content';
|
|
150
173
|
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
174
|
+
// Density-aware cell padding
|
|
175
|
+
const densityPadding = useMemo(() => getDensityPadding(density), [density]);
|
|
176
|
+
const cellSx = useMemo(() => ({ ...CELL_CONTENT_BASE_SX, ...densityPadding }), [densityPadding]);
|
|
177
|
+
const headerCellSx = useMemo(() => ({ px: densityPadding.px, py: densityPadding.py }), [densityPadding]);
|
|
151
178
|
// Memoize header rows (recursive tree traversal)
|
|
152
179
|
const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
|
|
153
180
|
const { handleResizeStart, getColumnWidth } = useColumnResize({
|
|
154
181
|
columnSizingOverrides,
|
|
155
182
|
setColumnSizingOverrides,
|
|
156
183
|
});
|
|
184
|
+
const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
|
|
185
|
+
columns: visibleCols,
|
|
186
|
+
columnOrder,
|
|
187
|
+
onColumnOrderChange,
|
|
188
|
+
enabled: columnReorder === true,
|
|
189
|
+
pinnedColumns,
|
|
190
|
+
wrapperRef,
|
|
191
|
+
});
|
|
192
|
+
const virtualScrollEnabled = virtualScroll?.enabled === true;
|
|
193
|
+
const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
|
|
194
|
+
const { visibleRange } = useVirtualScroll({
|
|
195
|
+
totalRows: items.length,
|
|
196
|
+
rowHeight: virtualRowHeight,
|
|
197
|
+
enabled: virtualScrollEnabled,
|
|
198
|
+
overscan: virtualScroll?.overscan,
|
|
199
|
+
containerRef: wrapperRef,
|
|
200
|
+
});
|
|
157
201
|
// Pre-compute per-column layout (tdSx, widths) so GridRow doesn't recalculate per-cell
|
|
158
202
|
const columnLayouts = useMemo(() => visibleCols.map((col, colIdx) => {
|
|
159
203
|
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
@@ -194,6 +238,7 @@ function DataGridTableInner(props) {
|
|
|
194
238
|
bgcolor: 'background.paper',
|
|
195
239
|
willChange: 'scroll-position',
|
|
196
240
|
'& [data-drag-range]': { bgcolor: 'rgba(33, 115, 70, 0.12) !important' },
|
|
241
|
+
'& [data-drag-anchor]': { bgcolor: 'background.paper !important' },
|
|
197
242
|
}), [fitToContent, suppressHorizontalScroll, allowOverflowX]);
|
|
198
243
|
const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
|
|
199
244
|
const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
|
|
@@ -215,13 +260,31 @@ function DataGridTableInner(props) {
|
|
|
215
260
|
// Select pre-computed sx variant (module-scope = no per-cell allocation)
|
|
216
261
|
const cellSx = getCellSx(col.type, descriptor.canEditAny, descriptor.isActive && !descriptor.isInRange, descriptor.isInRange, descriptor.isInCutRange);
|
|
217
262
|
const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
|
|
218
|
-
cellContent = (_jsxs(Box, { component: "div", ...interactionProps, sx: cellSx, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx(Box, { component: "div", onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle", sx: FILL_HANDLE_SX }))] }));
|
|
263
|
+
cellContent = (_jsxs(Box, { component: "div", ...interactionProps, sx: Array.isArray(cellSx) ? [...cellSx, densityPadding] : { ...cellSx, ...densityPadding }, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx(Box, { component: "div", onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle", sx: FILL_HANDLE_SX }))] }));
|
|
219
264
|
}
|
|
220
265
|
return (_jsx(CellErrorBoundary, { onError: onCellError, children: cellContent }, `${rowId}-${col.columnId}`));
|
|
221
266
|
},
|
|
222
267
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
|
|
223
268
|
[editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
|
|
224
|
-
return (_jsxs(Box, { sx: gridRootSx, children: [_jsxs(Box, { ref: wrapperRef, tabIndex: 0, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, onKeyDown: handleGridKeyDown, onContextMenu: PREVENT_DEFAULT, "data-overflow-x": allowOverflowX ? 'true' : 'false', sx: wrapperSx, children: [_jsx(Box, { sx: WRAPPER_SCROLL_SX, children: _jsx(TableContainer, { sx: { minWidth: allowOverflowX ? minTableWidth : undefined }, children: _jsxs(Box, { ref: tableContainerRef, sx: isLoading && items.length > 0 ? TABLE_WRAPPER_LOADING_SX : TABLE_WRAPPER_SX, children: [_jsxs(Table, { size: "small", sx: { overflow: 'hidden', minWidth: minTableWidth }, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, children: [_jsx(TableHead, { sx: STICKY_HEADER_SX, children: headerRows.map((row, rowIdx) => (_jsxs(TableRow, { sx: HEADER_ROW_SX, children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { padding: "checkbox", rowSpan: headerRows.length > 1 ? 1 : undefined, sx: CHECKBOX_CELL_SX, children: _jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, onChange: (_, c) => handleSelectAll(!!c), size: "small", "aria-label": "Select all rows" }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { rowSpan: headerRows.length - 1, sx: CHECKBOX_PLACEHOLDER_SX })),
|
|
269
|
+
return (_jsxs(Box, { sx: gridRootSx, children: [_jsxs(Box, { ref: wrapperRef, tabIndex: 0, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, onKeyDown: handleGridKeyDown, onContextMenu: PREVENT_DEFAULT, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-density": density, sx: wrapperSx, children: [_jsx(Box, { sx: WRAPPER_SCROLL_SX, children: _jsx(TableContainer, { sx: { minWidth: allowOverflowX ? minTableWidth : undefined }, children: _jsxs(Box, { ref: tableContainerRef, sx: isLoading && items.length > 0 ? TABLE_WRAPPER_LOADING_SX : TABLE_WRAPPER_SX, children: [_jsxs(Table, { size: "small", sx: { overflow: 'hidden', minWidth: minTableWidth }, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, children: [_jsx(TableHead, { sx: STICKY_HEADER_SX, children: headerRows.map((row, rowIdx) => (_jsxs(TableRow, { sx: HEADER_ROW_SX, children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { padding: "checkbox", rowSpan: headerRows.length > 1 ? 1 : undefined, sx: CHECKBOX_CELL_SX, children: _jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, onChange: (_, c) => handleSelectAll(!!c), size: "small", "aria-label": "Select all rows" }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { rowSpan: headerRows.length - 1, sx: CHECKBOX_PLACEHOLDER_SX })), rowIdx === headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { component: "th", scope: "col", rowSpan: headerRows.length > 1 ? 1 : undefined, sx: {
|
|
270
|
+
width: ROW_NUMBER_COLUMN_WIDTH,
|
|
271
|
+
minWidth: ROW_NUMBER_COLUMN_WIDTH,
|
|
272
|
+
maxWidth: ROW_NUMBER_COLUMN_WIDTH,
|
|
273
|
+
textAlign: 'center',
|
|
274
|
+
fontWeight: 600,
|
|
275
|
+
backgroundColor: 'action.hover',
|
|
276
|
+
position: 'sticky',
|
|
277
|
+
left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
|
|
278
|
+
zIndex: 4,
|
|
279
|
+
...headerCellSx,
|
|
280
|
+
}, children: "#" })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { rowSpan: headerRows.length - 1, sx: {
|
|
281
|
+
width: ROW_NUMBER_COLUMN_WIDTH,
|
|
282
|
+
minWidth: ROW_NUMBER_COLUMN_WIDTH,
|
|
283
|
+
position: 'sticky',
|
|
284
|
+
left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
|
|
285
|
+
zIndex: 4,
|
|
286
|
+
backgroundColor: 'background.paper',
|
|
287
|
+
} })), row.map((cell, cellIdx) => {
|
|
225
288
|
if (cell.isGroup) {
|
|
226
289
|
return (_jsx(TableCell, { colSpan: cell.colSpan, component: "th", scope: "colgroup", sx: GROUP_HEADER_CELL_SX, children: cell.label }, cellIdx));
|
|
227
290
|
}
|
|
@@ -233,11 +296,58 @@ function DataGridTableInner(props) {
|
|
|
233
296
|
const isPinnedRight = col.pinned === 'right';
|
|
234
297
|
const columnWidth = getColumnWidth(col);
|
|
235
298
|
const headerSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? HEADER_PINNED_LEFT_SX : isPinnedRight ? HEADER_PINNED_RIGHT_SX : HEADER_BASE_SX;
|
|
236
|
-
return (_jsxs(TableCell, { component: "th", scope: "col", rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined, sx:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
299
|
+
return (_jsxs(TableCell, { component: "th", scope: "col", "data-column-id": col.columnId, rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined, sx: { ...headerSx, ...headerCellSx }, style: {
|
|
300
|
+
minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
|
|
301
|
+
width: columnWidth,
|
|
302
|
+
maxWidth: columnWidth,
|
|
303
|
+
...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : undefined),
|
|
304
|
+
}, onMouseDown: columnReorder ? (e) => handleHeaderMouseDown(col.columnId, e) : undefined, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx(Box, { component: "button", onClick: (e) => {
|
|
305
|
+
e.stopPropagation();
|
|
306
|
+
pinning.headerMenu.open(col.columnId, e.currentTarget);
|
|
307
|
+
}, "aria-label": "Column options", title: "Column options", sx: {
|
|
308
|
+
background: 'transparent',
|
|
309
|
+
border: 'none',
|
|
310
|
+
cursor: 'pointer',
|
|
311
|
+
padding: '2px 4px',
|
|
312
|
+
fontSize: '16px',
|
|
313
|
+
lineHeight: 1,
|
|
314
|
+
color: 'text.secondary',
|
|
315
|
+
opacity: 0,
|
|
316
|
+
transition: 'opacity 0.15s, background-color 0.15s',
|
|
317
|
+
borderRadius: '3px',
|
|
318
|
+
display: 'flex',
|
|
319
|
+
alignItems: 'center',
|
|
320
|
+
justifyContent: 'center',
|
|
321
|
+
minWidth: '20px',
|
|
322
|
+
height: '20px',
|
|
323
|
+
'&:hover': {
|
|
324
|
+
bgcolor: 'action.hover',
|
|
325
|
+
opacity: 1,
|
|
326
|
+
},
|
|
327
|
+
'th:hover &': {
|
|
328
|
+
opacity: 1,
|
|
329
|
+
},
|
|
330
|
+
}, children: "\u22EE" })] }), _jsx(Box, { onMouseDown: (e) => handleResizeStart(e, col), sx: RESIZE_HANDLE_SX })] }, col.columnId));
|
|
331
|
+
})] }, rowIdx))) }), !showEmptyInGrid && (_jsxs(TableBody, { children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
|
|
332
|
+
? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
|
|
333
|
+
const rowIndex = visibleRange.startIndex + i;
|
|
334
|
+
const rowIdStr = getRowId(item);
|
|
335
|
+
return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), columnLayouts: columnLayouts, renderCellContent: renderCellContent, handleSingleRowClick: handleSingleRowClick, handleRowCheckboxChange: handleRowCheckboxChange, lastMouseShiftRef: lastMouseShiftRef, hasCheckboxCol: hasCheckboxCol, hasRowNumbersCol: hasRowNumbersCol, rowNumberOffset: rowNumberOffset, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
|
|
336
|
+
})
|
|
337
|
+
: items.map((item, rowIndex) => {
|
|
338
|
+
const rowIdStr = getRowId(item);
|
|
339
|
+
return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), columnLayouts: columnLayouts, renderCellContent: renderCellContent, handleSingleRowClick: handleSingleRowClick, handleRowCheckboxChange: handleRowCheckboxChange, lastMouseShiftRef: lastMouseShiftRef, hasCheckboxCol: hasCheckboxCol, hasRowNumbersCol: hasRowNumbersCol, rowNumberOffset: rowNumberOffset, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
|
|
340
|
+
})), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(Box, { sx: {
|
|
341
|
+
position: 'absolute',
|
|
342
|
+
top: 0,
|
|
343
|
+
bottom: 0,
|
|
344
|
+
width: 3,
|
|
345
|
+
bgcolor: 'var(--ogrid-primary, #217346)',
|
|
346
|
+
pointerEvents: 'none',
|
|
347
|
+
zIndex: 100,
|
|
348
|
+
transition: 'left 0.05s',
|
|
349
|
+
left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0),
|
|
350
|
+
} })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), showEmptyInGrid && emptyState && (_jsx(Box, { sx: EMPTY_STATE_SX, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "No results found" }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx(Button, { variant: "text", size: "small", onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }))] }) }) }), menuPosition &&
|
|
351
|
+
createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? NOOP, onRedo: onRedo ?? NOOP, onCopy: handleCopy, onCut: handleCut, onPaste: handlePasteVoid, onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body), _jsx(ColumnHeaderMenu, { columnId: pinning.headerMenu.openForColumn || '', isOpen: pinning.headerMenu.isOpen, anchorElement: pinning.headerMenu.anchorElement, onClose: pinning.headerMenu.close, onPinLeft: pinning.headerMenu.handlePinLeft, onPinRight: pinning.headerMenu.handlePinRight, onUnpin: pinning.headerMenu.handleUnpin, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin })] }), statusBarConfig && (_jsx(StatusBar, { totalCount: statusBarConfig.totalCount, filteredCount: statusBarConfig.filteredCount, selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size, selectedCellCount: selectionRange ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1) : undefined, aggregation: statusBarConfig.aggregation, suppressRowCount: statusBarConfig.suppressRowCount })), isLoading && (_jsx(Box, { sx: LOADING_OVERLAY_SX, children: _jsxs(Box, { sx: LOADING_INNER_SX, children: [_jsx(CircularProgress, { size: 24 }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: loadingMessage })] }) }))] }));
|
|
242
352
|
}
|
|
243
353
|
export const DataGridTable = React.memo(DataGridTableInner);
|
|
@@ -14,5 +14,5 @@ export function GridContextMenu(props) {
|
|
|
14
14
|
return true;
|
|
15
15
|
return false;
|
|
16
16
|
}, [hasSelection, canUndo, canRedo]);
|
|
17
|
-
return (_jsx(Menu, { open: true, onClose: onClose, anchorReference: "anchorPosition", anchorPosition: { top: y, left: x }, MenuListProps: { dense: true, 'aria-label': 'Grid context menu' }, children: GRID_CONTEXT_MENU_ITEMS.map((item) => (_jsxs(React.Fragment, { children: [item.dividerBefore && _jsx(Divider, {}), _jsxs(MenuItem, { onClick: handlers[item.id], disabled: isDisabled(item), children: [_jsx("span", { style: { flex: 1 }, children: item.label }), item.shortcut && (_jsx("span", { style: { marginLeft: 24, color: 'rgba(0,0,0,0.4)', fontSize: '0.8em' }, children: formatShortcut(item.shortcut) }))] })] }, item.id))) }));
|
|
17
|
+
return (_jsx(Menu, { open: true, onClose: onClose, anchorReference: "anchorPosition", anchorPosition: { top: y, left: x }, MenuListProps: { dense: true, 'aria-label': 'Grid context menu' }, children: GRID_CONTEXT_MENU_ITEMS.map((item) => (_jsxs(React.Fragment, { children: [item.dividerBefore && _jsx(Divider, {}), _jsxs(MenuItem, { onClick: handlers[item.id], disabled: isDisabled(item), children: [_jsx("span", { style: { flex: 1 }, children: item.label }), item.shortcut && (_jsx("span", { style: { marginLeft: 24, color: 'var(--ogrid-fg-muted, rgba(0,0,0,0.4))', fontSize: '0.8em' }, children: formatShortcut(item.shortcut) }))] })] }, item.id))) }));
|
|
18
18
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { DataGridTable } from './DataGridTable/DataGridTable';
|
|
|
4
4
|
export { ColumnChooser } from './ColumnChooser/ColumnChooser';
|
|
5
5
|
export { ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter';
|
|
6
6
|
export { PaginationControls } from './PaginationControls/PaginationControls';
|
|
7
|
+
export { ColumnHeaderMenu } from './ColumnHeaderMenu/ColumnHeaderMenu';
|
|
7
8
|
// Re-export everything from core
|
|
8
9
|
export * from '@alaarab/ogrid-react';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ColumnHeaderMenuProps {
|
|
2
|
+
columnId: string;
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
anchorElement: HTMLElement | null;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
onPinLeft: () => void;
|
|
7
|
+
onPinRight: () => void;
|
|
8
|
+
onUnpin: () => void;
|
|
9
|
+
canPinLeft: boolean;
|
|
10
|
+
canPinRight: boolean;
|
|
11
|
+
canUnpin: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Column header dropdown menu for pin/unpin actions.
|
|
15
|
+
* Uses Material UI Menu component.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { DataGridTable } from './DataGridTable/DataGridTable';
|
|
|
3
3
|
export { ColumnChooser, type IColumnChooserProps } from './ColumnChooser/ColumnChooser';
|
|
4
4
|
export { ColumnHeaderFilter, type IColumnHeaderFilterProps } from './ColumnHeaderFilter/ColumnHeaderFilter';
|
|
5
5
|
export { PaginationControls, type IPaginationControlsProps } from './PaginationControls/PaginationControls';
|
|
6
|
+
export { ColumnHeaderMenu, type ColumnHeaderMenuProps } from './ColumnHeaderMenu/ColumnHeaderMenu';
|
|
6
7
|
export * from '@alaarab/ogrid-react';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-react-material",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "OGrid Material UI implementation – MUI Table–based data grid with sorting, filtering, pagination, column chooser, spreadsheet selection, and CSV export.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"node": ">=18"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@alaarab/ogrid-react": "2.0.
|
|
41
|
+
"@alaarab/ogrid-react": "2.0.4"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@emotion/react": "^11.0.0",
|