@alaarab/ogrid-react-material 2.0.9 → 2.0.11

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.
@@ -26,7 +26,7 @@ export const ColumnChooser = (props) => {
26
26
  ev.stopPropagation();
27
27
  setColumnVisible(columnKey)(ev.target.checked);
28
28
  };
29
- return (_jsxs(Box, { className: className, sx: { display: 'inline-flex' }, children: [_jsxs(Button, { ref: buttonRef, variant: "outlined", size: "small", startIcon: _jsx(ViewColumnIcon, {}), endIcon: isOpen ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: handleToggle, "aria-expanded": isOpen, "aria-haspopup": "listbox", sx: {
29
+ return (_jsxs(Box, { className: className, sx: { display: 'inline-flex' }, children: [_jsxs(Button, { ref: buttonRef, variant: "outlined", size: "small", color: "inherit", startIcon: _jsx(ViewColumnIcon, {}), endIcon: isOpen ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: handleToggle, "aria-expanded": isOpen, "aria-haspopup": "listbox", sx: {
30
30
  textTransform: 'none',
31
31
  fontWeight: 600,
32
32
  borderColor: isOpen ? 'primary.main' : 'divider',
@@ -1,39 +1,53 @@
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';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useMemo, useEffect, useState } from 'react';
3
+ import { Menu, MenuItem, Divider } from '@mui/material';
4
+ import { getColumnHeaderMenuItems } from '@alaarab/ogrid-core';
8
5
  /**
9
- * Column header dropdown menu for pin/unpin actions.
10
- * Uses Material UI Menu component.
6
+ * Column header dropdown menu for pin/sort/autosize actions.
7
+ * Uses Material UI Menu component with anchor position.
11
8
  */
12
9
  export function ColumnHeaderMenu(props) {
13
- const { columnId, isOpen: _isOpen, anchorElement: _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);
10
+ const { isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, onSortAsc, onSortDesc, onClearSort, onAutosizeThis, onAutosizeAll, canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable, } = props;
11
+ const [anchorPosition, setAnchorPosition] = useState(undefined);
12
+ useEffect(() => {
13
+ if (isOpen && anchorElement) {
14
+ const rect = anchorElement.getBoundingClientRect();
15
+ setAnchorPosition({
16
+ top: rect.bottom + 4,
17
+ left: rect.left,
18
+ });
19
+ }
20
+ else {
21
+ setAnchorPosition(undefined);
22
+ }
23
+ }, [isOpen, anchorElement]);
24
+ const menuInput = useMemo(() => ({
25
+ canPinLeft,
26
+ canPinRight,
27
+ canUnpin,
28
+ currentSort,
29
+ isSortable,
30
+ isResizable,
31
+ }), [canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]);
32
+ const items = useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
33
+ const handlers = {
34
+ pinLeft: onPinLeft,
35
+ pinRight: onPinRight,
36
+ unpin: onUnpin,
37
+ sortAsc: onSortAsc,
38
+ sortDesc: onSortDesc,
39
+ clearSort: onClearSort,
40
+ autosizeThis: onAutosizeThis,
41
+ autosizeAll: onAutosizeAll,
18
42
  };
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 })] })] }));
43
+ return (_jsx(Menu, { open: isOpen && !!anchorPosition, onClose: onClose, anchorReference: "anchorPosition", anchorPosition: anchorPosition, slotProps: {
44
+ paper: {
45
+ sx: {
46
+ minWidth: 140,
47
+ },
48
+ },
49
+ }, children: items.map((item, idx) => (_jsxs(React.Fragment, { children: [_jsx(MenuItem, { disabled: item.disabled, onClick: () => {
50
+ handlers[item.id]();
51
+ onClose();
52
+ }, children: item.label }), item.divider && idx < items.length - 1 && _jsx(Divider, {})] }, item.id))) }));
39
53
  }
@@ -18,7 +18,13 @@ const CHECKBOX_CELL_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLU
18
18
  const CHECKBOX_WRAPPER_SX = { display: 'flex', alignItems: 'center', justifyContent: 'center' };
19
19
  const CHECKBOX_PLACEHOLDER_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLUMN_WIDTH, p: 0 };
20
20
  // Header
21
- const STICKY_HEADER_SX = { position: 'sticky', top: 0, zIndex: 8, bgcolor: 'action.hover', '& th': { bgcolor: 'action.hover' } };
21
+ const STICKY_HEADER_SX = {
22
+ /* Removed position: 'sticky', top: 0 - breaks horizontal sticky on pinned columns.
23
+ Instead, apply sticky to individual header cells (HEADER_BASE_SX). */
24
+ zIndex: 8,
25
+ bgcolor: 'action.hover',
26
+ '& th': { bgcolor: 'action.hover' }
27
+ };
22
28
  const HEADER_ROW_SX = { bgcolor: 'action.hover' };
23
29
  const GROUP_HEADER_CELL_SX = { textAlign: 'center', fontWeight: 600, borderBottom: 2, borderColor: 'divider', py: 0.75 };
24
30
  // Density padding helper
@@ -34,6 +40,7 @@ const CELL_CONTENT_BASE_SX = {
34
40
  width: '100%', height: '100%', display: 'flex', alignItems: 'center', minWidth: 0,
35
41
  px: '10px', py: '6px', boxSizing: 'border-box', overflow: 'hidden',
36
42
  textOverflow: 'ellipsis', whiteSpace: 'nowrap', userSelect: 'none', outline: 'none',
43
+ '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: '-2px', zIndex: 3 },
37
44
  };
38
45
  const CELL_CONTENT_NUMERIC_SX = { ...CELL_CONTENT_BASE_SX, justifyContent: 'flex-end', textAlign: 'right' };
39
46
  const CELL_CONTENT_BOOLEAN_SX = { ...CELL_CONTENT_BASE_SX, justifyContent: 'center', textAlign: 'center' };
@@ -101,9 +108,15 @@ const CELL_TD_BASE_SX = { position: 'relative', p: 0, height: '1px' };
101
108
  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
109
  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' };
103
110
  // Header cell positioning variants
104
- const HEADER_BASE_SX = { fontWeight: 600, position: 'relative' };
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' };
111
+ const HEADER_BASE_SX = {
112
+ fontWeight: 600,
113
+ position: 'sticky', /* Changed from relative - enables vertical sticky for all headers */
114
+ top: 0, /* Sticky vertically */
115
+ zIndex: 8, /* Stack above body cells */
116
+ bgcolor: 'action.hover' /* Required for sticky overlap */
117
+ };
118
+ const HEADER_PINNED_LEFT_SX = { ...HEADER_BASE_SX, position: 'sticky', left: 0, top: 0, zIndex: 10 /* Increased from 9 to stack above base header cells (z-index: 8) */, bgcolor: 'action.hover', willChange: 'transform', borderLeft: '2px solid', borderLeftColor: 'primary.main' };
119
+ const HEADER_PINNED_RIGHT_SX = { ...HEADER_BASE_SX, position: 'sticky', right: 0, top: 0, zIndex: 10 /* Increased from 9 to stack above base header cells (z-index: 8) */, bgcolor: 'action.hover', willChange: 'transform', borderRight: '2px solid', borderRightColor: 'primary.main' };
107
120
  // Resize handle
108
121
  const RESIZE_HANDLE_SX = {
109
122
  position: 'absolute', top: 0, right: '-3px', bottom: 0, width: '8px',
@@ -150,7 +163,7 @@ function GridRowInner(props) {
150
163
  position: 'sticky',
151
164
  left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
152
165
  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)))] }));
166
+ }, children: rowNumberOffset + rowIndex + 1 })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { sx: [cl.tdSx, { minWidth: cl.minWidth, width: cl.width, maxWidth: cl.maxWidth }], children: renderCellContent(item, cl.col, rowIndex, colIdx) }, cl.col.columnId)))] }));
154
167
  }
155
168
  const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
156
169
  function DataGridTableInner(props) {
@@ -268,27 +281,40 @@ function DataGridTableInner(props) {
268
281
  },
269
282
  // eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
270
283
  [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
271
- 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: {
272
- width: ROW_NUMBER_COLUMN_WIDTH,
273
- minWidth: ROW_NUMBER_COLUMN_WIDTH,
274
- maxWidth: ROW_NUMBER_COLUMN_WIDTH,
275
- textAlign: 'center',
276
- fontWeight: 600,
277
- backgroundColor: 'action.hover',
278
- position: 'sticky',
279
- left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
280
- zIndex: 4,
281
- ...headerCellSx,
282
- }, children: "#" })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { rowSpan: headerRows.length - 1, sx: {
283
- width: ROW_NUMBER_COLUMN_WIDTH,
284
- minWidth: ROW_NUMBER_COLUMN_WIDTH,
285
- position: 'sticky',
286
- left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
287
- zIndex: 4,
288
- backgroundColor: 'background.paper',
284
+ 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, { ...{
285
+ component: "th",
286
+ scope: "col",
287
+ rowSpan: headerRows.length > 1 ? 1 : undefined,
288
+ sx: {
289
+ width: ROW_NUMBER_COLUMN_WIDTH,
290
+ minWidth: ROW_NUMBER_COLUMN_WIDTH,
291
+ maxWidth: ROW_NUMBER_COLUMN_WIDTH,
292
+ textAlign: 'center',
293
+ fontWeight: 600,
294
+ backgroundColor: 'action.hover',
295
+ position: 'sticky',
296
+ left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
297
+ zIndex: 4,
298
+ ...headerCellSx,
299
+ }
300
+ }, children: "#" })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { ...{
301
+ rowSpan: headerRows.length - 1,
302
+ sx: {
303
+ width: ROW_NUMBER_COLUMN_WIDTH,
304
+ minWidth: ROW_NUMBER_COLUMN_WIDTH,
305
+ position: 'sticky',
306
+ left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
307
+ zIndex: 4,
308
+ backgroundColor: 'background.paper',
309
+ }
289
310
  } })), row.map((cell, cellIdx) => {
290
311
  if (cell.isGroup) {
291
- return (_jsx(TableCell, { colSpan: cell.colSpan, component: "th", scope: "colgroup", sx: GROUP_HEADER_CELL_SX, children: cell.label }, cellIdx));
312
+ return (_jsx(TableCell, { ...{
313
+ colSpan: cell.colSpan,
314
+ component: "th",
315
+ scope: "colgroup",
316
+ sx: GROUP_HEADER_CELL_SX
317
+ }, children: cell.label }, cellIdx));
292
318
  }
293
319
  // Leaf cell
294
320
  const col = cell.columnDef;
@@ -298,12 +324,33 @@ function DataGridTableInner(props) {
298
324
  const isPinnedRight = col.pinned === 'right';
299
325
  const columnWidth = getColumnWidth(col);
300
326
  const headerSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? HEADER_PINNED_LEFT_SX : isPinnedRight ? HEADER_PINNED_RIGHT_SX : HEADER_BASE_SX;
301
- return (_jsxs(TableCell, { component: "th", scope: "col", "data-column-id": col.columnId, rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined, sx: { ...headerSx, ...headerCellSx }, style: {
302
- minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
303
- width: columnWidth,
304
- maxWidth: columnWidth,
305
- ...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : undefined),
306
- }, 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) => {
327
+ // Determine aria-sort value for sorted columns
328
+ const isSorted = props.sortBy === col.columnId;
329
+ const ariaSort = isSorted
330
+ ? (props.sortDirection === 'asc' ? 'ascending' : 'descending')
331
+ : undefined;
332
+ return (_jsxs(TableCell, { ...{
333
+ component: "th",
334
+ scope: "col",
335
+ 'data-column-id': col.columnId,
336
+ rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined,
337
+ 'aria-sort': ariaSort,
338
+ sx: {
339
+ ...headerSx,
340
+ ...headerCellSx,
341
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
342
+ width: columnWidth,
343
+ maxWidth: columnWidth,
344
+ ...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : {}),
345
+ '&:focus-visible': {
346
+ outline: '2px solid',
347
+ outlineColor: 'primary.main',
348
+ outlineOffset: '-2px',
349
+ zIndex: 11,
350
+ },
351
+ },
352
+ onMouseDown: columnReorder ? (e) => handleHeaderMouseDown(col.columnId, e) : undefined
353
+ }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx(Box, { component: "button", onClick: (e) => {
307
354
  e.stopPropagation();
308
355
  pinning.headerMenu.open(col.columnId, e.currentTarget);
309
356
  }, "aria-label": "Column options", title: "Column options", sx: {
@@ -314,8 +361,8 @@ function DataGridTableInner(props) {
314
361
  fontSize: '16px',
315
362
  lineHeight: 1,
316
363
  color: 'text.secondary',
317
- opacity: 0,
318
- transition: 'opacity 0.15s, background-color 0.15s',
364
+ opacity: 1,
365
+ transition: 'background-color 0.15s',
319
366
  borderRadius: '3px',
320
367
  display: 'flex',
321
368
  alignItems: 'center',
@@ -324,10 +371,6 @@ function DataGridTableInner(props) {
324
371
  height: '20px',
325
372
  '&:hover': {
326
373
  bgcolor: 'action.hover',
327
- opacity: 1,
328
- },
329
- 'th:hover &': {
330
- opacity: 1,
331
374
  },
332
375
  }, children: "\u22EE" })] }), _jsx(Box, { onMouseDown: (e) => handleResizeStart(e, col), sx: RESIZE_HANDLE_SX })] }, col.columnId));
333
376
  })] }, rowIdx))) }), !showEmptyInGrid && (_jsxs(TableBody, { children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
@@ -349,7 +392,7 @@ function DataGridTableInner(props) {
349
392
  zIndex: 100,
350
393
  transition: 'left 0.05s',
351
394
  left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0),
352
- } })), _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 &&
353
- 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 })] }) }))] }));
395
+ } })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset, items: items, visibleColumns: props.visibleColumns, columnSizingOverrides: columnSizingOverrides, columnOrder: props.columnOrder }), 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 &&
396
+ 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, onSortAsc: pinning.headerMenu.handleSortAsc, onSortDesc: pinning.headerMenu.handleSortDesc, onClearSort: pinning.headerMenu.handleClearSort, onAutosizeThis: pinning.headerMenu.handleAutosizeThis, onAutosizeAll: pinning.headerMenu.handleAutosizeAll, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin, currentSort: pinning.headerMenu.currentSort, isSortable: pinning.headerMenu.isSortable, isResizable: pinning.headerMenu.isResizable })] }), 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 })] }) }))] }));
354
397
  }
355
398
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -1,16 +1,22 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
- import { useMemo, useCallback } from 'react';
4
3
  import { IconButton, Button, Select, MenuItem, Box, Typography, } from '@mui/material';
5
4
  import { FirstPage as FirstPageIcon, LastPage as LastPageIcon, ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon, } from '@mui/icons-material';
6
- import { getPaginationViewModel } from '@alaarab/ogrid-react';
5
+ import { usePaginationControls } from '@alaarab/ogrid-react';
7
6
  export const PaginationControls = React.memo((props) => {
8
7
  const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions, entityLabelPlural, className, } = props;
9
- const labelPlural = entityLabelPlural ?? 'items';
10
- const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined), [currentPage, pageSize, totalCount, pageSizeOptions]);
11
- const handlePageSizeChange = useCallback((event) => {
12
- onPageSizeChange(Number(event.target.value));
13
- }, [onPageSizeChange]);
8
+ const { labelPlural, vm, handlePageSizeChange } = usePaginationControls({
9
+ currentPage,
10
+ pageSize,
11
+ totalCount,
12
+ onPageChange,
13
+ onPageSizeChange,
14
+ pageSizeOptions,
15
+ entityLabelPlural,
16
+ });
17
+ const handlePageSizeChangeEvent = (event) => {
18
+ handlePageSizeChange(Number(event.target.value));
19
+ };
14
20
  if (!vm) {
15
21
  return null;
16
22
  }
@@ -25,5 +31,5 @@ export const PaginationControls = React.memo((props) => {
25
31
  width: '100%',
26
32
  minWidth: 0,
27
33
  boxSizing: 'border-box',
28
- }, children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(IconButton, { size: "small", onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", children: _jsx(FirstPageIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", children: _jsx(ChevronLeftIcon, { fontSize: "small" }) }), showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(1), "aria-label": "Page 1", sx: { minWidth: 32, px: 0.5 }, children: "1" }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx(Button, { variant: currentPage === pageNum ? 'contained' : 'outlined', size: "small", onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, sx: { minWidth: 32, px: 0.5 }, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, sx: { minWidth: 32, px: 0.5 }, children: totalPages })] })), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", children: _jsx(ChevronRightIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", children: _jsx(LastPageIcon, { fontSize: "small" }) })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", children: "Rows" }), _jsx(Select, { value: pageSize, onChange: handlePageSizeChange, size: "small", "aria-label": "Rows per page", sx: { minWidth: 70 }, children: vm.pageSizeOptions.map((n) => (_jsx(MenuItem, { value: n, children: n }, n))) })] })] }));
34
+ }, children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(IconButton, { size: "small", onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", children: _jsx(FirstPageIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", children: _jsx(ChevronLeftIcon, { fontSize: "small" }) }), showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(1), "aria-label": "Page 1", sx: { minWidth: 32, px: 0.5 }, children: "1" }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx(Button, { variant: currentPage === pageNum ? 'contained' : 'outlined', size: "small", onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, sx: { minWidth: 32, px: 0.5 }, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, sx: { minWidth: 32, px: 0.5 }, children: totalPages })] })), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", children: _jsx(ChevronRightIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", children: _jsx(LastPageIcon, { fontSize: "small" }) })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", children: "Rows" }), _jsx(Select, { value: pageSize, onChange: handlePageSizeChangeEvent, size: "small", "aria-label": "Rows per page", sx: { minWidth: 70 }, children: vm.pageSizeOptions.map((n) => (_jsx(MenuItem, { value: n, children: n }, n))) })] })] }));
29
35
  });
@@ -6,12 +6,20 @@ export interface ColumnHeaderMenuProps {
6
6
  onPinLeft: () => void;
7
7
  onPinRight: () => void;
8
8
  onUnpin: () => void;
9
+ onSortAsc: () => void;
10
+ onSortDesc: () => void;
11
+ onClearSort: () => void;
12
+ onAutosizeThis: () => void;
13
+ onAutosizeAll: () => void;
9
14
  canPinLeft: boolean;
10
15
  canPinRight: boolean;
11
16
  canUnpin: boolean;
17
+ currentSort: 'asc' | 'desc' | null;
18
+ isSortable: boolean;
19
+ isResizable: boolean;
12
20
  }
13
21
  /**
14
- * Column header dropdown menu for pin/unpin actions.
15
- * Uses Material UI Menu component.
22
+ * Column header dropdown menu for pin/sort/autosize actions.
23
+ * Uses Material UI Menu component with anchor position.
16
24
  */
17
25
  export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-material",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
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",
@@ -39,7 +39,7 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
- "@alaarab/ogrid-react": "2.0.9"
42
+ "@alaarab/ogrid-react": "2.0.11"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@emotion/react": "^11.0.0",
@@ -58,8 +58,8 @@
58
58
  "@testing-library/jest-dom": "^6.9.1",
59
59
  "@testing-library/react": "^16.3.2",
60
60
  "@testing-library/user-event": "^14.6.1",
61
- "@types/react": "^18.3.18",
62
- "@types/react-dom": "^18.3.5",
61
+ "@types/react": "^19.0.0",
62
+ "@types/react-dom": "^19.0.0",
63
63
  "eslint-plugin-storybook": "10.2.8",
64
64
  "react": "^18.3.1",
65
65
  "react-dom": "^18.3.1",