@alaarab/ogrid-react-radix 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.
@@ -39,7 +39,7 @@
39
39
  min-width: 220px;
40
40
  background: var(--ogrid-bg, #fff);
41
41
  border-radius: 6px;
42
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
42
+ box-shadow: var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12));
43
43
  border: 1px solid var(--ogrid-border, #e0e0e0);
44
44
  display: flex;
45
45
  flex-direction: column;
@@ -108,7 +108,7 @@
108
108
  background: var(--ogrid-bg, #fff);
109
109
  border: 1px solid var(--ogrid-border, #d1d1d1);
110
110
  border-radius: 8px;
111
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.14);
111
+ box-shadow: var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12));
112
112
  overflow: hidden;
113
113
  }
114
114
 
@@ -8,7 +8,7 @@ function UserAvatar({ user, size = 32 }) {
8
8
  width: size,
9
9
  height: size,
10
10
  borderRadius: '50%',
11
- background: '#e0e0e0',
11
+ background: 'var(--ogrid-border, #e0e0e0)',
12
12
  display: 'flex',
13
13
  alignItems: 'center',
14
14
  justifyContent: 'center',
@@ -8,7 +8,7 @@ import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
8
8
  import { InlineCellEditor } from './InlineCellEditor';
9
9
  import { StatusBar } from './StatusBar';
10
10
  import { GridContextMenu } from './GridContextMenu';
11
- import { useDataGridState, useColumnResize, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
11
+ import { useDataGridState, useColumnResize, useColumnReorder, useVirtualScroll, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
12
12
  import styles from './DataGridTable.module.css';
13
13
  // Module-scope stable constants (avoid per-render allocations)
14
14
  const GRID_ROOT_STYLE = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
@@ -35,7 +35,7 @@ function DataGridTableInner(props) {
35
35
  const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
36
36
  const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
37
37
  const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
38
- const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, } = props;
38
+ const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, pinnedColumns, } = props;
39
39
  // Memoize header rows (recursive tree traversal — avoid recomputing every render)
40
40
  const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
41
41
  const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
@@ -44,6 +44,23 @@ function DataGridTableInner(props) {
44
44
  columnSizingOverrides,
45
45
  setColumnSizingOverrides,
46
46
  });
47
+ const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
48
+ columns: visibleCols,
49
+ columnOrder,
50
+ onColumnOrderChange,
51
+ enabled: columnReorder === true,
52
+ pinnedColumns,
53
+ wrapperRef,
54
+ });
55
+ const virtualScrollEnabled = virtualScroll?.enabled === true;
56
+ const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
57
+ const { visibleRange } = useVirtualScroll({
58
+ totalRows: items.length,
59
+ rowHeight: virtualRowHeight,
60
+ enabled: virtualScrollEnabled,
61
+ overscan: virtualScroll?.overscan,
62
+ containerRef: wrapperRef,
63
+ });
47
64
  const editCallbacks = useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
48
65
  const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
49
66
  // Refs for volatile state — lets renderCellContent be stable (same function ref across
@@ -145,11 +162,20 @@ function DataGridTableInner(props) {
145
162
  const leafRowSpan = headerRows.length > 1 && rowIdx < headerRows.length - 1
146
163
  ? headerRows.length - rowIdx
147
164
  : undefined;
148
- return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, rowSpan: leafRowSpan, className: columnMeta.hdrClasses[col.columnId] || undefined, style: columnMeta.hdrStyles[col.columnId], children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
149
- })] }, rowIdx))) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
150
- const rowIdStr = getRowId(item);
151
- return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), visibleCols: visibleCols, columnMeta: columnMeta, 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));
152
- }) }))] }), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: styles.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: styles.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }))] }) }) }), menuPosition &&
165
+ return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, rowSpan: leafRowSpan, className: columnMeta.hdrClasses[col.columnId] || undefined, style: {
166
+ ...columnMeta.hdrStyles[col.columnId],
167
+ ...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : undefined),
168
+ }, onMouseDown: columnReorder ? (e) => handleHeaderMouseDown(col.columnId, e) : undefined, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
169
+ })] }, rowIdx))) }), !showEmptyInGrid && (_jsxs("tbody", { children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx("tr", { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
170
+ ? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
171
+ const rowIndex = visibleRange.startIndex + i;
172
+ const rowIdStr = getRowId(item);
173
+ return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), visibleCols: visibleCols, columnMeta: columnMeta, 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));
174
+ })
175
+ : items.map((item, rowIndex) => {
176
+ const rowIdStr = getRowId(item);
177
+ return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), visibleCols: visibleCols, columnMeta: columnMeta, 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));
178
+ })), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx("tr", { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx("div", { className: styles.dropIndicator, style: { left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0) } })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: styles.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: styles.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }))] }) }) }), menuPosition &&
153
179
  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("div", { className: styles.loadingOverlay, "aria-live": "polite", children: _jsxs("div", { className: styles.loadingOverlayContent, children: [_jsx("div", { className: styles.spinner }), _jsx("span", { className: styles.loadingOverlayText, children: loadingMessage })] }) }))] }));
154
180
  }
155
181
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -125,7 +125,7 @@
125
125
  right: -4px;
126
126
  bottom: 0;
127
127
  width: 4px;
128
- background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
128
+ background: linear-gradient(to right, var(--ogrid-pinned-shadow, rgba(0, 0, 0, 0.12)), transparent);
129
129
  pointer-events: none;
130
130
  }
131
131
 
@@ -147,7 +147,7 @@
147
147
  left: -4px;
148
148
  bottom: 0;
149
149
  width: 4px;
150
- background: linear-gradient(to left, rgba(0, 0, 0, 0.08), transparent);
150
+ background: linear-gradient(to left, var(--ogrid-pinned-shadow, rgba(0, 0, 0, 0.12)), transparent);
151
151
  pointer-events: none;
152
152
  }
153
153
 
@@ -172,6 +172,18 @@
172
172
  background: var(--ogrid-header-bg, #f5f5f5);
173
173
  }
174
174
 
175
+ /* Column reorder drop indicator */
176
+ .dropIndicator {
177
+ position: absolute;
178
+ top: 0;
179
+ bottom: 0;
180
+ width: 3px;
181
+ background: var(--ogrid-primary, #217346);
182
+ pointer-events: none;
183
+ z-index: 100;
184
+ transition: left 0.05s;
185
+ }
186
+
175
187
  /* Column resize handle — wide hit area with narrow visual indicator */
176
188
  .resizeHandle {
177
189
  position: absolute;
@@ -361,7 +373,7 @@
361
373
  background: var(--ogrid-bg, #fff);
362
374
  border: 1px solid var(--ogrid-border, #e0e0e0);
363
375
  border-radius: 6px;
364
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
376
+ box-shadow: var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12));
365
377
  }
366
378
 
367
379
  .contextMenuItem {
@@ -408,7 +420,7 @@
408
420
  display: flex;
409
421
  align-items: center;
410
422
  justify-content: center;
411
- background: rgba(255, 255, 255, 0.7);
423
+ background: var(--ogrid-loading-bg, rgba(255, 255, 255, 0.7));
412
424
  backdrop-filter: blur(1px);
413
425
  pointer-events: all;
414
426
  }
@@ -422,7 +434,7 @@
422
434
  background: var(--ogrid-bg, #fff);
423
435
  border: 1px solid var(--ogrid-border, #c4c4c4);
424
436
  border-radius: 6px;
425
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14);
437
+ box-shadow: var(--ogrid-shadow-sm, 0 2px 4px rgba(0, 0, 0, 0.08));
426
438
  }
427
439
 
428
440
  .loadingOverlayText {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-radix",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "OGrid default (Radix) – Data grid with sorting, filtering, pagination, column chooser, and CSV export. Packed with Radix UI; no Fluent or Material required.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -41,7 +41,7 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "dependencies": {
44
- "@alaarab/ogrid-react": "2.0.1",
44
+ "@alaarab/ogrid-react": "2.0.3",
45
45
  "@radix-ui/react-checkbox": "^1.1.2",
46
46
  "@radix-ui/react-popover": "^1.1.2"
47
47
  },
@@ -51,7 +51,14 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@storybook/react-vite": "10.2.8",
54
+ "@testing-library/jest-dom": "^6.6.3",
55
+ "@testing-library/react": "^16.1.0",
56
+ "@testing-library/user-event": "^14.6.1",
57
+ "@types/react": "^18.3.18",
58
+ "@types/react-dom": "^18.3.5",
54
59
  "eslint-plugin-storybook": "10.2.8",
60
+ "react": "^18.3.1",
61
+ "react-dom": "^18.3.1",
55
62
  "sass": "^1.83.4",
56
63
  "storybook": "10.2.8",
57
64
  "vite": "^7.0.0"