@alaarab/ogrid-react-fluent 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, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
10
+ import { useDataGridState, useColumnReorder, useVirtualScroll, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
11
11
  import styles from './DataGridTable.module.css';
12
12
  // Module-scope stable constants (avoid per-render allocations)
13
13
  const gridRootStyle = {
@@ -45,11 +45,28 @@ function DataGridTableInner(props) {
45
45
  const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
46
46
  const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
47
47
  const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
48
- const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
48
+ 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;
49
49
  // Memoize header rows (recursive tree traversal)
50
- const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
50
+ const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
51
51
  const hasGroupHeaders = headerRows.length > 1;
52
52
  const fitToContent = layoutMode === 'content';
53
+ const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
54
+ columns: visibleCols,
55
+ columnOrder,
56
+ onColumnOrderChange,
57
+ enabled: columnReorder === true,
58
+ pinnedColumns,
59
+ wrapperRef,
60
+ });
61
+ const virtualScrollEnabled = virtualScroll?.enabled === true;
62
+ const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
63
+ const { visibleRange } = useVirtualScroll({
64
+ totalRows: items.length,
65
+ rowHeight: virtualRowHeight,
66
+ enabled: virtualScrollEnabled,
67
+ overscan: virtualScroll?.overscan,
68
+ containerRef: wrapperRef,
69
+ });
53
70
  const columnSizingOptions = useMemo(() => {
54
71
  const acc = {};
55
72
  if (hasCheckboxCol) {
@@ -121,11 +138,13 @@ function DataGridTableInner(props) {
121
138
  const handleSelectAllRef = useLatestRef(handleSelectAll);
122
139
  const handleRowCheckboxChangeRef = useLatestRef(handleRowCheckboxChange);
123
140
  const rowIndexByRowIdRef = useLatestRef(rowIndexByRowId);
141
+ const handleHeaderMouseDownRef = useLatestRef(handleHeaderMouseDown);
142
+ const isReorderDraggingRef = useLatestRef(isReorderDragging);
124
143
  const fluentColumns = useMemo(() => {
125
144
  const dataCols = visibleCols.map((col, colIdx) => createTableColumn({
126
145
  columnId: col.columnId,
127
146
  compare: col.compare ?? (() => 0),
128
- renderHeaderCell: () => (_jsx("div", { "data-column-id": col.columnId, children: _jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInputRef.current) }) })),
147
+ renderHeaderCell: () => (_jsx("div", { "data-column-id": col.columnId, style: columnReorder ? { cursor: isReorderDraggingRef.current ? 'grabbing' : 'grab' } : undefined, onMouseDown: columnReorder ? (e) => handleHeaderMouseDownRef.current(col.columnId, e) : undefined, children: _jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInputRef.current) }) })),
129
148
  renderCell: (item) => {
130
149
  const rowId = getRowId(item);
131
150
  const rowIndex = rowIndexByRowIdRef.current.get(rowId) ?? -1;
@@ -180,7 +199,7 @@ function DataGridTableInner(props) {
180
199
  }
181
200
  return dataCols;
182
201
  // eslint-disable-next-line react-hooks/exhaustive-deps
183
- }, [visibleCols, hasCheckboxCol, getRowId, setPopoverAnchorEl]); // All volatile state/callbacks read via refs
202
+ }, [visibleCols, hasCheckboxCol, getRowId, setPopoverAnchorEl, columnReorder]); // All volatile state/callbacks read via refs
184
203
  // Stable row-click handler
185
204
  const handleSingleRowClick = useCallback((rowId) => {
186
205
  if (rowSelection !== 'single')
@@ -249,15 +268,16 @@ function DataGridTableInner(props) {
249
268
  : fitToContent
250
269
  ? 'max-content'
251
270
  : '100%',
252
- }, onKeyDown: handleGridKeyDown, children: [_jsx("div", { className: styles.tableScrollContent, children: _jsx("div", { className: isLoading && items.length > 0 ? styles.loadingDimmed : undefined, children: _jsxs("div", { className: styles.tableWidthAnchor, ref: tableContainerRef, children: [_jsxs(DataGrid, { items: items, columns: fluentColumns, resizableColumns: true, resizableColumnsOptions: { autoFitColumns: layoutMode === 'fill' && !allowOverflowX }, columnSizingOptions: columnSizingOptions, onColumnResize: handleColumnResize, getRowId: fluentGetRowId, focusMode: "composite", className: styles.dataGrid, children: [_jsxs(DataGridHeader, { className: styles.stickyHeader, children: [hasGroupHeaders && headerRows.slice(0, -1).map((row, rowIdx) => (_jsxs("tr", { className: styles.groupHeaderRow, children: [rowIdx === 0 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1, style: { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLUMN_WIDTH } })), row.map((cell, cellIdx) => {
271
+ }, onKeyDown: handleGridKeyDown, children: [_jsx("div", { className: styles.tableScrollContent, children: _jsx("div", { className: isLoading && items.length > 0 ? styles.loadingDimmed : undefined, children: _jsxs("div", { className: styles.tableWidthAnchor, ref: tableContainerRef, children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx("div", { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), _jsxs(DataGrid, { items: virtualScrollEnabled ? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1) : items, columns: fluentColumns, resizableColumns: true, resizableColumnsOptions: { autoFitColumns: layoutMode === 'fill' && !allowOverflowX }, columnSizingOptions: columnSizingOptions, onColumnResize: handleColumnResize, getRowId: fluentGetRowId, focusMode: "composite", className: styles.dataGrid, children: [_jsxs(DataGridHeader, { className: styles.stickyHeader, children: [hasGroupHeaders && headerRows.slice(0, -1).map((row, rowIdx) => (_jsxs("tr", { className: styles.groupHeaderRow, children: [rowIdx === 0 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1, style: { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLUMN_WIDTH } })), row.map((cell, cellIdx) => {
253
272
  if (cell.isGroup) {
254
273
  return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
255
274
  }
256
275
  return (_jsx("th", { rowSpan: headerRows.length - rowIdx, className: styles.leafHeaderCellSpan, scope: "col", children: cell.columnDef?.name }, cellIdx));
257
276
  })] }, `group-${rowIdx}`))), _jsx(DataGridRow, { children: ({ renderHeaderCell, columnId }) => (_jsx(DataGridHeaderCell, { className: headerClassMap[String(columnId)] || undefined, children: renderHeaderCell() })) })] }), _jsx(DataGridBody, { children: ({ item }) => {
258
277
  const rowId = getRowId(item);
259
- return (_jsx(GridRow, { item: item, rowId: rowId, rowIndex: rowIndexByRowId.get(rowId) ?? -1, isSelected: selectedRowIds.has(rowId), hasCheckboxCol: hasCheckboxCol, cellClassMap: cellClassMap, handleSingleRowClick: handleSingleRowClick, selectionRange: selectionRange, activeCell: activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowId));
260
- } })] }), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { className: styles.emptyStateInGridMessageSticky, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.emptyStateInGridIcon, "aria-hidden": true, children: "\uD83D\uDCCB" }), _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 &&
278
+ const rowIndex = rowIndexByRowId.get(rowId) ?? -1;
279
+ return (_jsx(GridRow, { item: item, rowId: rowId, rowIndex: rowIndex, isSelected: selectedRowIds.has(rowId), hasCheckboxCol: hasCheckboxCol, cellClassMap: cellClassMap, handleSingleRowClick: handleSingleRowClick, selectionRange: selectionRange, activeCell: activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowId));
280
+ } })] }), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx("div", { 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", { className: styles.emptyStateInGridMessageSticky, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.emptyStateInGridIcon, "aria-hidden": true, children: "\uD83D\uDCCB" }), _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 &&
261
281
  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(Spinner, { size: "small" }), _jsx("span", { className: styles.loadingOverlayText, children: loadingMessage })] }) }))] }));
262
282
  }
263
283
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -379,7 +379,7 @@
379
379
  right: -4px;
380
380
  bottom: 0;
381
381
  width: 4px;
382
- background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
382
+ background: linear-gradient(to right, var(--ogrid-pinned-shadow, rgba(0, 0, 0, 0.12)), transparent);
383
383
  pointer-events: none;
384
384
  }
385
385
 
@@ -393,7 +393,7 @@
393
393
  left: -4px;
394
394
  bottom: 0;
395
395
  width: 4px;
396
- background: linear-gradient(to left, rgba(0, 0, 0, 0.08), transparent);
396
+ background: linear-gradient(to left, var(--ogrid-pinned-shadow, rgba(0, 0, 0, 0.12)), transparent);
397
397
  pointer-events: none;
398
398
  }
399
399
 
@@ -493,7 +493,7 @@
493
493
  display: flex;
494
494
  align-items: center;
495
495
  justify-content: center;
496
- background: rgba(255, 255, 255, 0.7);
496
+ background: var(--ogrid-loading-bg, rgba(255, 255, 255, 0.7));
497
497
  backdrop-filter: blur(1px);
498
498
  pointer-events: all;
499
499
  }
@@ -581,6 +581,18 @@
581
581
  color: var(--colorBrandForeground1Hover, #115ea3);
582
582
  }
583
583
 
584
+ /* Column reorder drop indicator */
585
+ .dropIndicator {
586
+ position: absolute;
587
+ top: 0;
588
+ bottom: 0;
589
+ width: 3px;
590
+ background: var(--ogrid-primary, #217346);
591
+ pointer-events: none;
592
+ z-index: 100;
593
+ transition: left 0.05s;
594
+ }
595
+
584
596
  /* Empty state: hide body, keep header and empty message */
585
597
  .tableWrapper[data-empty=true] :global(.fui-DataGrid) tbody {
586
598
  display: none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-fluent",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "OGrid Fluent UI implementation – DataGrid-powered data table with sorting, filtering, pagination, column chooser, and CSV export.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -40,7 +40,7 @@
40
40
  "node": ">=18"
41
41
  },
42
42
  "dependencies": {
43
- "@alaarab/ogrid-react": "2.0.1"
43
+ "@alaarab/ogrid-react": "2.0.3"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@fluentui/react-components": "^9.0.0",
@@ -52,10 +52,17 @@
52
52
  "@fluentui/react-components": "^9.72.10",
53
53
  "@fluentui/react-icons": "^2.0.317",
54
54
  "@storybook/react-vite": "10.2.8",
55
+ "@testing-library/jest-dom": "^6.6.3",
56
+ "@testing-library/react": "^16.1.0",
57
+ "@testing-library/user-event": "^14.6.1",
58
+ "@types/react": "^18.3.18",
59
+ "@types/react-dom": "^18.3.5",
60
+ "eslint-plugin-storybook": "10.2.8",
61
+ "react": "^18.3.1",
62
+ "react-dom": "^18.3.1",
55
63
  "sass": "^1.83.4",
56
64
  "scheduler": "^0.27.0",
57
65
  "storybook": "10.2.8",
58
- "eslint-plugin-storybook": "10.2.8",
59
66
  "vite": "^7.0.0"
60
67
  },
61
68
  "publishConfig": {