@alaarab/ogrid-react-material 2.0.3 → 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, useColumnReorder, useVirtualScroll, 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',
@@ -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,17 +158,23 @@ 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, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, pinnedColumns, } = 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({
@@ -211,6 +238,7 @@ function DataGridTableInner(props) {
211
238
  bgcolor: 'background.paper',
212
239
  willChange: 'scroll-position',
213
240
  '& [data-drag-range]': { bgcolor: 'rgba(33, 115, 70, 0.12) !important' },
241
+ '& [data-drag-anchor]': { bgcolor: 'background.paper !important' },
214
242
  }), [fitToContent, suppressHorizontalScroll, allowOverflowX]);
215
243
  const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
216
244
  const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
@@ -232,13 +260,31 @@ function DataGridTableInner(props) {
232
260
  // Select pre-computed sx variant (module-scope = no per-cell allocation)
233
261
  const cellSx = getCellSx(col.type, descriptor.canEditAny, descriptor.isActive && !descriptor.isInRange, descriptor.isInRange, descriptor.isInCutRange);
234
262
  const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
235
- 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 }))] }));
236
264
  }
237
265
  return (_jsx(CellErrorBoundary, { onError: onCellError, children: cellContent }, `${rowId}-${col.columnId}`));
238
266
  },
239
267
  // eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
240
268
  [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
241
- 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) => {
242
288
  if (cell.isGroup) {
243
289
  return (_jsx(TableCell, { colSpan: cell.colSpan, component: "th", scope: "colgroup", sx: GROUP_HEADER_CELL_SX, children: cell.label }, cellIdx));
244
290
  }
@@ -250,21 +296,47 @@ function DataGridTableInner(props) {
250
296
  const isPinnedRight = col.pinned === 'right';
251
297
  const columnWidth = getColumnWidth(col);
252
298
  const headerSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? HEADER_PINNED_LEFT_SX : isPinnedRight ? HEADER_PINNED_RIGHT_SX : HEADER_BASE_SX;
253
- return (_jsxs(TableCell, { component: "th", scope: "col", "data-column-id": col.columnId, rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined, sx: headerSx, style: {
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: {
254
300
  minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
255
301
  width: columnWidth,
256
302
  maxWidth: columnWidth,
257
303
  ...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : undefined),
258
- }, onMouseDown: columnReorder ? (e) => handleHeaderMouseDown(col.columnId, e) : undefined, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx(Box, { onMouseDown: (e) => handleResizeStart(e, col), sx: RESIZE_HANDLE_SX })] }, col.columnId));
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));
259
331
  })] }, rowIdx))) }), !showEmptyInGrid && (_jsxs(TableBody, { children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
260
332
  ? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
261
333
  const rowIndex = visibleRange.startIndex + i;
262
334
  const rowIdStr = getRowId(item);
263
- 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));
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));
264
336
  })
265
337
  : items.map((item, rowIndex) => {
266
338
  const rowIdStr = getRowId(item);
267
- 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));
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));
268
340
  })), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(Box, { sx: {
269
341
  position: 'absolute',
270
342
  top: 0,
@@ -276,6 +348,6 @@ function DataGridTableInner(props) {
276
348
  transition: 'left 0.05s',
277
349
  left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0),
278
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 &&
279
- 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 })] }) }))] }));
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 })] }) }))] }));
280
352
  }
281
353
  export const DataGridTable = React.memo(DataGridTableInner);
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.3",
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.3"
41
+ "@alaarab/ogrid-react": "2.0.4"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@emotion/react": "^11.0.0",