@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.
@@ -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
- bgcolor: 'rgba(255,255,255,0.7)',
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}` }) }) })), 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)))] }));
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 })), row.map((cell, cellIdx) => {
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: headerSx, style: { minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, width: columnWidth, maxWidth: columnWidth }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx(Box, { onMouseDown: (e) => handleResizeStart(e, col), sx: RESIZE_HANDLE_SX })] }, col.columnId));
237
- })] }, rowIdx))) }), !showEmptyInGrid && (_jsx(TableBody, { children: items.map((item, rowIndex) => {
238
- const rowIdStr = getRowId(item);
239
- 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, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
240
- }) }))] }), _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 &&
241
- 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)] }), 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 })] }) }))] }));
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;
@@ -0,0 +1,2 @@
1
+ export { ColumnHeaderMenu } from './ColumnHeaderMenu';
2
+ export type { ColumnHeaderMenuProps } from './ColumnHeaderMenu';
@@ -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.2",
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.2"
41
+ "@alaarab/ogrid-react": "2.0.4"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@emotion/react": "^11.0.0",