@alaarab/ogrid-react-fluent 2.0.2 → 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,76 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useRef, useEffect } from 'react';
3
+ import { COLUMN_HEADER_MENU_ITEMS } from '@alaarab/ogrid-core';
4
+ import { makeStyles, tokens } from '@fluentui/react-components';
5
+ const useStyles = makeStyles({
6
+ menu: {
7
+ position: 'fixed',
8
+ minWidth: '140px',
9
+ backgroundColor: tokens.colorNeutralBackground1,
10
+ borderRadius: tokens.borderRadiusMedium,
11
+ padding: '4px',
12
+ boxShadow: tokens.shadow16,
13
+ zIndex: 100,
14
+ },
15
+ menuItem: {
16
+ display: 'flex',
17
+ alignItems: 'center',
18
+ height: '28px',
19
+ padding: '0 8px',
20
+ fontSize: tokens.fontSizeBase200,
21
+ color: tokens.colorNeutralForeground1,
22
+ borderRadius: tokens.borderRadiusSmall,
23
+ cursor: 'pointer',
24
+ userSelect: 'none',
25
+ outline: 'none',
26
+ backgroundColor: 'transparent',
27
+ border: 'none',
28
+ width: '100%',
29
+ textAlign: 'left',
30
+ ':hover:not([disabled])': {
31
+ backgroundColor: tokens.colorNeutralBackground1Hover,
32
+ },
33
+ ':active:not([disabled])': {
34
+ backgroundColor: tokens.colorNeutralBackground1Pressed,
35
+ },
36
+ ':disabled': {
37
+ color: tokens.colorNeutralForegroundDisabled,
38
+ cursor: 'not-allowed',
39
+ },
40
+ },
41
+ });
42
+ export function ColumnHeaderMenu(props) {
43
+ const { isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, canPinLeft, canPinRight, canUnpin } = props;
44
+ const menuRef = useRef(null);
45
+ const styles = useStyles();
46
+ useEffect(() => {
47
+ if (!isOpen)
48
+ return;
49
+ const handleClickOutside = (e) => {
50
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
51
+ onClose();
52
+ }
53
+ };
54
+ const handleEscape = (e) => {
55
+ if (e.key === 'Escape') {
56
+ onClose();
57
+ }
58
+ };
59
+ document.addEventListener('mousedown', handleClickOutside);
60
+ document.addEventListener('keydown', handleEscape);
61
+ return () => {
62
+ document.removeEventListener('mousedown', handleClickOutside);
63
+ document.removeEventListener('keydown', handleEscape);
64
+ };
65
+ }, [isOpen, onClose]);
66
+ if (!isOpen || !anchorElement)
67
+ return null;
68
+ const rect = anchorElement.getBoundingClientRect();
69
+ const menuStyle = {
70
+ top: rect.bottom + 4,
71
+ left: rect.left,
72
+ };
73
+ const handlers = [onPinLeft, onPinRight, onUnpin];
74
+ const disabled = [!canPinLeft, !canPinRight, !canUnpin];
75
+ return (_jsx("div", { ref: menuRef, className: styles.menu, style: menuStyle, children: COLUMN_HEADER_MENU_ITEMS.map((item, idx) => (_jsx("button", { className: styles.menuItem, onClick: handlers[idx], disabled: disabled[idx], children: item.label }, item.id))) }));
76
+ }
@@ -0,0 +1 @@
1
+ export { ColumnHeaderMenu } from './ColumnHeaderMenu';
@@ -1,13 +1,14 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
3
  import { useMemo, useRef, useEffect, useCallback } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { DataGrid, DataGridHeader, DataGridRow, DataGridHeaderCell, DataGridBody, DataGridCell, createTableColumn, Spinner, Checkbox, Popover, PopoverSurface, } from '@fluentui/react-components';
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, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
11
+ import { useDataGridState, useColumnReorder, useVirtualScroll, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
11
12
  import styles from './DataGridTable.module.css';
12
13
  // Module-scope stable constants (avoid per-render allocations)
13
14
  const gridRootStyle = {
@@ -37,24 +38,46 @@ function DataGridTableInner(props) {
37
38
  const wrapperRef = useRef(null);
38
39
  const tableContainerRef = useRef(null);
39
40
  const state = useDataGridState({ props, wrapperRef });
40
- const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels } = state;
41
- const { flatColumns, visibleCols, totalColCount, hasCheckboxCol, colOffset, rowIndexByRowId, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
41
+ const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels, pinning } = state;
42
+ const { flatColumns, visibleCols, totalColCount, hasCheckboxCol, hasRowNumbersCol, colOffset, rowIndexByRowId, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
42
43
  const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
43
44
  const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
44
45
  const { activeCell, setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
45
46
  const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
46
47
  const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
47
48
  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;
49
+ 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, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
50
+ // Calculate row number offset for pagination
51
+ const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
49
52
  // Memoize header rows (recursive tree traversal)
50
- const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
53
+ const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
51
54
  const hasGroupHeaders = headerRows.length > 1;
52
55
  const fitToContent = layoutMode === 'content';
56
+ const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
57
+ columns: visibleCols,
58
+ columnOrder,
59
+ onColumnOrderChange,
60
+ enabled: columnReorder === true,
61
+ pinnedColumns,
62
+ wrapperRef,
63
+ });
64
+ const virtualScrollEnabled = virtualScroll?.enabled === true;
65
+ const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
66
+ const { visibleRange } = useVirtualScroll({
67
+ totalRows: items.length,
68
+ rowHeight: virtualRowHeight,
69
+ enabled: virtualScrollEnabled,
70
+ overscan: virtualScroll?.overscan,
71
+ containerRef: wrapperRef,
72
+ });
53
73
  const columnSizingOptions = useMemo(() => {
54
74
  const acc = {};
55
75
  if (hasCheckboxCol) {
56
76
  acc['__selection__'] = { minWidth: CHECKBOX_COLUMN_WIDTH, defaultWidth: CHECKBOX_COLUMN_WIDTH, idealWidth: CHECKBOX_COLUMN_WIDTH };
57
77
  }
78
+ if (hasRowNumbersCol) {
79
+ acc['__row_number__'] = { minWidth: ROW_NUMBER_COLUMN_WIDTH, defaultWidth: ROW_NUMBER_COLUMN_WIDTH, idealWidth: ROW_NUMBER_COLUMN_WIDTH };
80
+ }
58
81
  visibleCols.forEach((c) => {
59
82
  const minW = c.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
60
83
  const defaultW = c.defaultWidth ?? 120;
@@ -68,7 +91,7 @@ function DataGridTableInner(props) {
68
91
  };
69
92
  });
70
93
  return acc;
71
- }, [visibleCols, columnSizingOverrides, hasCheckboxCol]);
94
+ }, [visibleCols, columnSizingOverrides, hasCheckboxCol, hasRowNumbersCol]);
72
95
  const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
73
96
  // Pre-compute column class maps (avoids per-cell .filter(Boolean).join(' '))
74
97
  const { cellClassMap, headerClassMap } = useMemo(() => {
@@ -121,11 +144,16 @@ function DataGridTableInner(props) {
121
144
  const handleSelectAllRef = useLatestRef(handleSelectAll);
122
145
  const handleRowCheckboxChangeRef = useLatestRef(handleRowCheckboxChange);
123
146
  const rowIndexByRowIdRef = useLatestRef(rowIndexByRowId);
147
+ const handleHeaderMouseDownRef = useLatestRef(handleHeaderMouseDown);
148
+ const isReorderDraggingRef = useLatestRef(isReorderDragging);
124
149
  const fluentColumns = useMemo(() => {
125
150
  const dataCols = visibleCols.map((col, colIdx) => createTableColumn({
126
151
  columnId: col.columnId,
127
152
  compare: col.compare ?? (() => 0),
128
- renderHeaderCell: () => (_jsx("div", { "data-column-id": col.columnId, children: _jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInputRef.current) }) })),
153
+ 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: _jsxs("div", { className: styles.headerCellContent, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInputRef.current) }), _jsx("button", { className: styles.headerMenuTrigger, onClick: (e) => {
154
+ e.stopPropagation();
155
+ pinning.headerMenu.open(col.columnId, e.currentTarget);
156
+ }, "aria-label": "Column options", title: "Column options", children: "\u22EE" })] }) })),
129
157
  renderCell: (item) => {
130
158
  const rowId = getRowId(item);
131
159
  const rowIndex = rowIndexByRowIdRef.current.get(rowId) ?? -1;
@@ -176,11 +204,38 @@ function DataGridTableInner(props) {
176
204
  }, "aria-label": `Select row ${rowIndex + 1}` }) }));
177
205
  },
178
206
  });
179
- return [checkboxCol, ...dataCols];
207
+ const cols = [checkboxCol];
208
+ if (hasRowNumbersCol) {
209
+ const rowNumberCol = createTableColumn({
210
+ columnId: '__row_number__',
211
+ compare: () => 0,
212
+ renderHeaderCell: () => (_jsx("div", { className: styles.rowNumberHeaderCell, children: "#" })),
213
+ renderCell: (item) => {
214
+ const rowId = getRowId(item);
215
+ const rowIndex = rowIndexByRowIdRef.current.get(rowId) ?? -1;
216
+ return (_jsx("div", { className: styles.rowNumberCell, children: rowNumberOffset + rowIndex + 1 }));
217
+ },
218
+ });
219
+ cols.push(rowNumberCol);
220
+ }
221
+ return [...cols, ...dataCols];
222
+ }
223
+ if (hasRowNumbersCol) {
224
+ const rowNumberCol = createTableColumn({
225
+ columnId: '__row_number__',
226
+ compare: () => 0,
227
+ renderHeaderCell: () => (_jsx("div", { className: styles.rowNumberHeaderCell, children: "#" })),
228
+ renderCell: (item) => {
229
+ const rowId = getRowId(item);
230
+ const rowIndex = rowIndexByRowIdRef.current.get(rowId) ?? -1;
231
+ return (_jsx("div", { className: styles.rowNumberCell, children: rowNumberOffset + rowIndex + 1 }));
232
+ },
233
+ });
234
+ return [rowNumberCol, ...dataCols];
180
235
  }
181
236
  return dataCols;
182
237
  // eslint-disable-next-line react-hooks/exhaustive-deps
183
- }, [visibleCols, hasCheckboxCol, getRowId, setPopoverAnchorEl]); // All volatile state/callbacks read via refs
238
+ }, [visibleCols, hasCheckboxCol, hasRowNumbersCol, getRowId, setPopoverAnchorEl, columnReorder, rowNumberOffset]); // All volatile state/callbacks read via refs
184
239
  // Stable row-click handler
185
240
  const handleSingleRowClick = useCallback((rowId) => {
186
241
  if (rowSelection !== 'single')
@@ -233,7 +288,7 @@ function DataGridTableInner(props) {
233
288
  [String(data.columnId)]: { widthPx: data.width },
234
289
  }));
235
290
  }, [setColumnSizingOverrides]);
236
- return (_jsxs("div", { style: gridRootStyle, children: [_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: `${styles.tableWrapper} ${rowSelection !== 'none' ? styles.selectableGrid : ''}`, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, "data-empty": showEmptyInGrid ? 'true' : undefined, "data-auto-fit": layoutMode === 'fill' && !allowOverflowX ? 'true' : undefined, "data-column-count": totalColCount, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-container-width": containerWidth, "data-min-table-width": Math.round(minTableWidth), "data-has-selection": rowSelection !== 'none' ? 'true' : undefined, onContextMenu: PREVENT_DEFAULT, style: {
291
+ return (_jsxs("div", { style: gridRootStyle, children: [_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: `${styles.tableWrapper} ${rowSelection !== 'none' ? styles.selectableGrid : ''} ${styles[`density-${density}`] || ''}`, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, "data-empty": showEmptyInGrid ? 'true' : undefined, "data-auto-fit": layoutMode === 'fill' && !allowOverflowX ? 'true' : undefined, "data-column-count": totalColCount, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-container-width": containerWidth, "data-min-table-width": Math.round(minTableWidth), "data-has-selection": rowSelection !== 'none' ? 'true' : undefined, onContextMenu: PREVENT_DEFAULT, style: {
237
292
  ['--data-table-column-count']: totalColCount,
238
293
  ['--data-table-width']: showEmptyInGrid
239
294
  ? '100%'
@@ -249,15 +304,16 @@ function DataGridTableInner(props) {
249
304
  : fitToContent
250
305
  ? 'max-content'
251
306
  : '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) => {
307
+ }, 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 } })), rowIdx === 0 && hasRowNumbersCol && (_jsx("th", { rowSpan: headerRows.length - 1, style: { width: ROW_NUMBER_COLUMN_WIDTH, minWidth: ROW_NUMBER_COLUMN_WIDTH } })), row.map((cell, cellIdx) => {
253
308
  if (cell.isGroup) {
254
309
  return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
255
310
  }
256
311
  return (_jsx("th", { rowSpan: headerRows.length - rowIdx, className: styles.leafHeaderCellSpan, scope: "col", children: cell.columnDef?.name }, cellIdx));
257
312
  })] }, `group-${rowIdx}`))), _jsx(DataGridRow, { children: ({ renderHeaderCell, columnId }) => (_jsx(DataGridHeaderCell, { className: headerClassMap[String(columnId)] || undefined, children: renderHeaderCell() })) })] }), _jsx(DataGridBody, { children: ({ item }) => {
258
313
  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 &&
261
- 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 })] }) }))] }));
314
+ const rowIndex = rowIndexByRowId.get(rowId) ?? -1;
315
+ 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));
316
+ } })] }), 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 &&
317
+ 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), createPortal(_jsx(ColumnHeaderMenu, { 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 }), 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
318
  }
263
319
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -204,6 +204,14 @@
204
204
  min-width: 48px !important;
205
205
  max-width: 48px !important;
206
206
  }
207
+ .tableWrapper {
208
+ /* Row numbers column must stay 50px even in auto-fit mode */
209
+ }
210
+ .tableWrapper[data-auto-fit=true][data-column-count] .rowNumberHeaderCellWrapper, .tableWrapper[data-auto-fit=true][data-column-count] .rowNumberCellWrapper {
211
+ width: 50px !important;
212
+ min-width: 50px !important;
213
+ max-width: 50px !important;
214
+ }
207
215
  .tableWrapper {
208
216
  /* Hide resize handle on last column only when table fits (autoFitColumns); when overflow/scroll, last column can resize (Fluent docs) */
209
217
  }
@@ -246,6 +254,20 @@
246
254
  padding: 0 !important;
247
255
  }
248
256
 
257
+ .rowNumberHeaderCellWrapper {
258
+ width: 50px !important;
259
+ min-width: 50px !important;
260
+ max-width: 50px !important;
261
+ padding: 0 !important;
262
+ }
263
+
264
+ .rowNumberCellWrapper {
265
+ width: 50px !important;
266
+ min-width: 50px !important;
267
+ max-width: 50px !important;
268
+ padding: 0 !important;
269
+ }
270
+
249
271
  .selectionHeaderCell {
250
272
  display: flex;
251
273
  align-items: center;
@@ -262,6 +284,19 @@
262
284
  height: 100%;
263
285
  }
264
286
 
287
+ .rowNumberHeaderCell,
288
+ .rowNumberCell {
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ width: 100%;
293
+ height: 100%;
294
+ font-weight: 600;
295
+ font-variant-numeric: tabular-nums;
296
+ color: var(--colorNeutralForeground3, #666);
297
+ background: var(--colorNeutralBackground3, #f5f5f5);
298
+ }
299
+
265
300
  .selectedRow :global .fui-DataGridCell {
266
301
  background-color: var(--colorNeutralBackground1Selected, #e6f0fb) !important;
267
302
  }
@@ -308,6 +343,10 @@
308
343
  background-color: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
309
344
  }
310
345
 
346
+ :global([data-drag-anchor]) {
347
+ background-color: var(--colorNeutralBackground1, #fff) !important;
348
+ }
349
+
311
350
  .cellCut {
312
351
  background-color: var(--colorNeutralBackground1Hover, rgba(0, 0, 0, 0.04)) !important;
313
352
  opacity: 0.7;
@@ -379,7 +418,7 @@
379
418
  right: -4px;
380
419
  bottom: 0;
381
420
  width: 4px;
382
- background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
421
+ background: linear-gradient(to right, var(--ogrid-pinned-shadow, rgba(0, 0, 0, 0.12)), transparent);
383
422
  pointer-events: none;
384
423
  }
385
424
 
@@ -393,7 +432,7 @@
393
432
  left: -4px;
394
433
  bottom: 0;
395
434
  width: 4px;
396
- background: linear-gradient(to left, rgba(0, 0, 0, 0.08), transparent);
435
+ background: linear-gradient(to left, var(--ogrid-pinned-shadow, rgba(0, 0, 0, 0.12)), transparent);
397
436
  pointer-events: none;
398
437
  }
399
438
 
@@ -493,7 +532,7 @@
493
532
  display: flex;
494
533
  align-items: center;
495
534
  justify-content: center;
496
- background: rgba(255, 255, 255, 0.7);
535
+ background: var(--ogrid-loading-bg, rgba(255, 255, 255, 0.7));
497
536
  backdrop-filter: blur(1px);
498
537
  pointer-events: all;
499
538
  }
@@ -581,6 +620,18 @@
581
620
  color: var(--colorBrandForeground1Hover, #115ea3);
582
621
  }
583
622
 
623
+ /* Column reorder drop indicator */
624
+ .dropIndicator {
625
+ position: absolute;
626
+ top: 0;
627
+ bottom: 0;
628
+ width: 3px;
629
+ background: var(--ogrid-primary, #217346);
630
+ pointer-events: none;
631
+ z-index: 100;
632
+ transition: left 0.05s;
633
+ }
634
+
584
635
  /* Empty state: hide body, keep header and empty message */
585
636
  .tableWrapper[data-empty=true] :global(.fui-DataGrid) tbody {
586
637
  display: none;
@@ -589,4 +640,65 @@
589
640
  /* Empty state: no extra bottom border (header row is last row, remove its border-bottom) */
590
641
  .tableWrapper[data-empty=true] :global(.fui-DataGridHeader .fui-DataGridRow) {
591
642
  border-bottom: none !important;
643
+ }
644
+
645
+ .headerCellContent {
646
+ display: flex;
647
+ align-items: center;
648
+ gap: 4px;
649
+ }
650
+
651
+ .headerMenuTrigger {
652
+ background: transparent;
653
+ border: none;
654
+ cursor: pointer;
655
+ padding: 2px 4px;
656
+ font-size: 16px;
657
+ line-height: 1;
658
+ color: var(--colorNeutralForeground3, #666);
659
+ opacity: 0;
660
+ transition: opacity 0.15s, background-color 0.15s;
661
+ border-radius: 3px;
662
+ display: flex;
663
+ align-items: center;
664
+ justify-content: center;
665
+ min-width: 20px;
666
+ height: 20px;
667
+ }
668
+ .headerMenuTrigger:hover {
669
+ background: var(--colorNeutralBackground1Hover, #f3f2f1);
670
+ opacity: 1;
671
+ }
672
+ .headerMenuTrigger:active {
673
+ background: var(--colorNeutralBackground1Pressed, #e1dfdd);
674
+ }
675
+
676
+ .tableWrapper :global(.fui-DataGridHeaderCell:hover) .headerMenuTrigger {
677
+ opacity: 1;
678
+ }
679
+
680
+ .tableWrapper :global(.fui-DataGridHeaderCell.pinnedColLeft),
681
+ .tableWrapper :global(.fui-DataGridCell.pinnedColLeft) {
682
+ border-left: 2px solid var(--colorBrandForeground1, #0078d4) !important;
683
+ }
684
+
685
+ .tableWrapper :global(.fui-DataGridHeaderCell.pinnedColRight),
686
+ .tableWrapper :global(.fui-DataGridCell.pinnedColRight) {
687
+ border-right: 2px solid var(--colorBrandForeground1, #0078d4) !important;
688
+ }
689
+
690
+ .density-compact :global(.fui-DataGridHeaderCell),
691
+ .density-compact :global(.fui-DataGridCell) {
692
+ padding: 4px 8px;
693
+ }
694
+ .density-compact .cellContent {
695
+ padding: 4px 8px;
696
+ }
697
+
698
+ .density-comfortable :global(.fui-DataGridHeaderCell),
699
+ .density-comfortable :global(.fui-DataGridCell) {
700
+ padding: 12px 16px;
701
+ }
702
+ .density-comfortable .cellContent {
703
+ padding: 12px 16px;
592
704
  }
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ export interface ColumnHeaderMenuProps {
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
+ export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): React.ReactElement | null;
@@ -0,0 +1,2 @@
1
+ export { ColumnHeaderMenu } from './ColumnHeaderMenu';
2
+ export type { ColumnHeaderMenuProps } from './ColumnHeaderMenu';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-fluent",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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.2"
43
+ "@alaarab/ogrid-react": "2.0.4"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@fluentui/react-components": "^9.0.0",