@alaarab/ogrid-react-material 2.0.1 → 2.0.3

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.
@@ -7,7 +7,7 @@ import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
7
7
  import { InlineCellEditor } from './InlineCellEditor';
8
8
  import { StatusBar } from './StatusBar';
9
9
  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';
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
11
  // ── Module-scope stable styles (avoid per-render Emotion resolutions) ──
12
12
  const gridRootSx = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
13
13
  // Row
@@ -117,7 +117,7 @@ const EMPTY_STATE_SX = { py: 4, px: 2, textAlign: 'center', borderTop: 1, border
117
117
  const LOADING_OVERLAY_SX = {
118
118
  position: 'absolute', inset: 0, zIndex: 2,
119
119
  display: 'flex', alignItems: 'center', justifyContent: 'center',
120
- bgcolor: 'rgba(255,255,255,0.7)',
120
+ background: 'var(--ogrid-loading-bg, rgba(255,255,255,0.7))',
121
121
  };
122
122
  const LOADING_INNER_SX = {
123
123
  display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
@@ -145,7 +145,7 @@ function DataGridTableInner(props) {
145
145
  const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
146
146
  const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
147
147
  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;
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;
149
149
  const fitToContent = layoutMode === 'content';
150
150
  const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
151
151
  // Memoize header rows (recursive tree traversal)
@@ -154,6 +154,23 @@ function DataGridTableInner(props) {
154
154
  columnSizingOverrides,
155
155
  setColumnSizingOverrides,
156
156
  });
157
+ const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
158
+ columns: visibleCols,
159
+ columnOrder,
160
+ onColumnOrderChange,
161
+ enabled: columnReorder === true,
162
+ pinnedColumns,
163
+ wrapperRef,
164
+ });
165
+ const virtualScrollEnabled = virtualScroll?.enabled === true;
166
+ const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
167
+ const { visibleRange } = useVirtualScroll({
168
+ totalRows: items.length,
169
+ rowHeight: virtualRowHeight,
170
+ enabled: virtualScrollEnabled,
171
+ overscan: virtualScroll?.overscan,
172
+ containerRef: wrapperRef,
173
+ });
157
174
  // Pre-compute per-column layout (tdSx, widths) so GridRow doesn't recalculate per-cell
158
175
  const columnLayouts = useMemo(() => visibleCols.map((col, colIdx) => {
159
176
  const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
@@ -233,11 +250,32 @@ function DataGridTableInner(props) {
233
250
  const isPinnedRight = col.pinned === 'right';
234
251
  const columnWidth = getColumnWidth(col);
235
252
  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 &&
253
+ return (_jsxs(TableCell, { component: "th", scope: "col", "data-column-id": col.columnId, rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined, sx: headerSx, style: {
254
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
255
+ width: columnWidth,
256
+ maxWidth: columnWidth,
257
+ ...(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));
259
+ })] }, rowIdx))) }), !showEmptyInGrid && (_jsxs(TableBody, { children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
260
+ ? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
261
+ const rowIndex = visibleRange.startIndex + i;
262
+ 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));
264
+ })
265
+ : items.map((item, rowIndex) => {
266
+ 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));
268
+ })), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(Box, { sx: {
269
+ position: 'absolute',
270
+ top: 0,
271
+ bottom: 0,
272
+ width: 3,
273
+ bgcolor: 'var(--ogrid-primary, #217346)',
274
+ pointerEvents: 'none',
275
+ zIndex: 100,
276
+ transition: 'left 0.05s',
277
+ left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0),
278
+ } })), _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
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 })] }) }))] }));
242
280
  }
243
281
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-material",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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.1"
41
+ "@alaarab/ogrid-react": "2.0.3"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@emotion/react": "^11.0.0",
@@ -54,8 +54,15 @@
54
54
  "@mui/icons-material": "^7.0.0",
55
55
  "@mui/material": "^7.0.0",
56
56
  "@storybook/react-vite": "10.2.8",
57
- "storybook": "10.2.8",
57
+ "@testing-library/jest-dom": "^6.6.3",
58
+ "@testing-library/react": "^16.1.0",
59
+ "@testing-library/user-event": "^14.6.1",
60
+ "@types/react": "^18.3.18",
61
+ "@types/react-dom": "^18.3.5",
58
62
  "eslint-plugin-storybook": "10.2.8",
63
+ "react": "^18.3.1",
64
+ "react-dom": "^18.3.1",
65
+ "storybook": "10.2.8",
59
66
  "vite": "^7.0.0"
60
67
  },
61
68
  "publishConfig": {