@alaarab/ogrid 1.5.0 → 1.7.1

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
- import { useCallback, useRef } from 'react';
3
+ import { useCallback, useRef, useMemo } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import * as Popover from '@radix-ui/react-popover';
6
6
  import * as Checkbox from '@radix-ui/react-checkbox';
@@ -10,22 +10,87 @@ import { StatusBar } from './StatusBar';
10
10
  import { GridContextMenu } from './GridContextMenu';
11
11
  import { useDataGridState, useColumnResize, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from '@alaarab/ogrid-core';
12
12
  import styles from './DataGridTable.module.css';
13
+ // Module-scope stable constants (avoid per-render allocations)
14
+ const GRID_ROOT_STYLE = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
15
+ const CURSOR_CELL_STYLE = { cursor: 'cell' };
16
+ const STOP_PROPAGATION = (e) => e.stopPropagation();
17
+ const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
13
18
  function DataGridTableInner(props) {
14
19
  const wrapperRef = useRef(null);
15
20
  const tableContainerRef = useRef(null);
16
21
  const state = useDataGridState({ props, wrapperRef });
17
22
  const lastMouseShiftRef = useRef(false);
18
- const { visibleCols, totalColCount, hasCheckboxCol, selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, setEditingCell, pendingEditorValue, setPendingEditorValue, setActiveCell, handleCellMouseDown, handleSelectAllCells, contextMenu, handleCellContextMenu, closeContextMenu, canUndo, canRedo, onUndo, onRedo, handleCopy, handleCut, handlePaste, handleGridKeyDown, handleFillHandleMouseDown, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, statusBarConfig, showEmptyInGrid, hasCellSelection, selectionRange, copyRange, cutRange, colOffset, headerFilterInput, cellDescriptorInput, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl, } = state;
23
+ const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels } = state;
24
+ const { visibleCols, totalColCount, hasCheckboxCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
25
+ const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
26
+ const { setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
27
+ const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo } = interaction;
28
+ const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
29
+ const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid } = viewModels;
19
30
  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;
20
- const headerRows = buildHeaderRows(columns, visibleColumns);
31
+ // Memoize header rows (recursive tree traversal — avoid recomputing every render)
32
+ const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
21
33
  const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
22
34
  const fitToContent = layoutMode === 'content';
23
35
  const { handleResizeStart, getColumnWidth } = useColumnResize({
24
36
  columnSizingOverrides,
25
37
  setColumnSizingOverrides,
26
38
  });
27
- const editCallbacks = React.useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
28
- const interactionHandlers = React.useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
39
+ const editCallbacks = useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
40
+ const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
41
+ // Pre-compute column styles and classNames (avoids per-cell object creation in the row loop)
42
+ const columnMeta = useMemo(() => {
43
+ const cellStyles = {};
44
+ const cellClasses = {};
45
+ const hdrStyles = {};
46
+ const hdrClasses = {};
47
+ for (let i = 0; i < visibleCols.length; i++) {
48
+ const col = visibleCols[i];
49
+ const columnWidth = getColumnWidth(col);
50
+ const hasExplicitWidth = !!(columnSizingOverrides[col.columnId] || col.idealWidth != null || col.defaultWidth != null);
51
+ const isFreezeCol = freezeCols != null && freezeCols >= 1 && i < freezeCols;
52
+ const isPinnedLeft = col.pinned === 'left';
53
+ const isPinnedRight = col.pinned === 'right';
54
+ cellStyles[col.columnId] = {
55
+ minWidth: col.minWidth ?? 80,
56
+ width: hasExplicitWidth ? columnWidth : undefined,
57
+ maxWidth: hasExplicitWidth ? columnWidth : undefined,
58
+ textAlign: col.type === 'numeric' ? 'right' : col.type === 'boolean' ? 'center' : undefined,
59
+ };
60
+ hdrStyles[col.columnId] = {
61
+ minWidth: col.minWidth ?? 80,
62
+ width: hasExplicitWidth ? columnWidth : undefined,
63
+ maxWidth: hasExplicitWidth ? columnWidth : undefined,
64
+ };
65
+ const parts = [];
66
+ if (isFreezeCol)
67
+ parts.push(styles.freezeCol);
68
+ if (isFreezeCol && i === 0)
69
+ parts.push(styles.freezeColFirst);
70
+ if (isPinnedLeft)
71
+ parts.push(styles.pinnedColLeft);
72
+ if (isPinnedRight)
73
+ parts.push(styles.pinnedColRight);
74
+ const cn = parts.join(' ');
75
+ cellClasses[col.columnId] = cn;
76
+ hdrClasses[col.columnId] = cn;
77
+ }
78
+ return { cellStyles, cellClasses, hdrStyles, hdrClasses };
79
+ }, [visibleCols, getColumnWidth, columnSizingOverrides, freezeCols]);
80
+ // Stable row-click handler (avoids creating a new arrow function per row)
81
+ const selectedRowIdsRef = useRef(selectedRowIds);
82
+ selectedRowIdsRef.current = selectedRowIds;
83
+ const handleSingleRowClick = useCallback((e) => {
84
+ if (rowSelection !== 'single')
85
+ return;
86
+ const rowId = e.currentTarget.dataset.rowId;
87
+ if (!rowId)
88
+ return;
89
+ const ids = selectedRowIdsRef.current;
90
+ updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
91
+ }, [rowSelection, updateSelection]);
92
+ // Stable header select-all handler
93
+ const handleSelectAllChecked = useCallback((c) => handleSelectAll(!!c), [handleSelectAll]);
29
94
  const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
30
95
  const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
31
96
  if (descriptor.mode === 'editing-inline') {
@@ -48,65 +113,28 @@ function DataGridTableInner(props) {
48
113
  descriptor.isInCopyRange ? styles.cellCopied : '',
49
114
  ].filter(Boolean).join(' ');
50
115
  const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
51
- return (_jsxs("div", { className: cellClassNames, ...interactionProps, style: descriptor.canEditAny ? { cursor: 'cell' } : undefined, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
116
+ return (_jsxs("div", { className: cellClassNames, ...interactionProps, style: descriptor.canEditAny ? CURSOR_CELL_STYLE : undefined, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
52
117
  }, [cellDescriptorInput, pendingEditorValue, popoverAnchorEl, editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit]);
53
- return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, 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-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: (e) => { e.preventDefault(); }, onKeyDown: handleGridKeyDown, style: {
54
- ['--data-table-column-count']: totalColCount,
55
- ['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
56
- ['--data-table-min-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'max-content' : fitToContent ? 'max-content' : '100%',
57
- ['--data-table-total-min-width']: `${minTableWidth}px`,
58
- }, children: [isLoading && items.length > 0 && (_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 })] }) })), _jsxs("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("table", { className: styles.dataTable, children: [_jsx("thead", { className: styles.stickyHeader, children: headerRows.map((row, rowIdx) => (_jsxs("tr", { children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { className: styles.selectionHeaderCell, scope: "col", rowSpan: 1, children: _jsx("div", { className: styles.selectionHeaderCellInner, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: allSelected ? true : someSelected ? 'indeterminate' : false, onCheckedChange: (c) => handleSelectAll(!!c), "aria-label": "Select all rows", children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: someSelected && !allSelected ? '–' : '✓' }) }) }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1 })), row.map((cell, cellIdx) => {
59
- if (cell.isGroup) {
60
- return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
61
- }
62
- // Leaf cell
63
- const col = cell.columnDef;
64
- const colIdx = visibleCols.indexOf(col);
65
- const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
66
- const isPinnedLeft = col.pinned === 'left';
67
- const isPinnedRight = col.pinned === 'right';
68
- const columnWidth = getColumnWidth(col);
69
- const hasExplicitWidth = !!(columnSizingOverrides[col.columnId] || col.idealWidth != null || col.defaultWidth != null);
70
- const leafRowSpan = headerRows.length > 1 && rowIdx < headerRows.length - 1
71
- ? headerRows.length - rowIdx
72
- : undefined;
73
- return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, rowSpan: leafRowSpan, className: [
74
- isFreezeCol ? styles.freezeCol : '',
75
- isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
76
- isPinnedLeft ? styles.pinnedColLeft : '',
77
- isPinnedRight ? styles.pinnedColRight : '',
78
- ].filter(Boolean).join(' '), style: {
79
- minWidth: col.minWidth ?? 80,
80
- width: hasExplicitWidth ? columnWidth : undefined,
81
- maxWidth: hasExplicitWidth ? columnWidth : undefined,
82
- }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
83
- })] }, rowIdx))) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
84
- const rowIdStr = getRowId(item);
85
- const isSelected = selectedRowIds.has(rowIdStr);
86
- return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', onClick: () => {
87
- if (rowSelection === 'single') {
88
- const id = getRowId(item);
89
- updateSelection(selectedRowIds.has(id) ? new Set() : new Set([id]));
90
- }
91
- }, children: [hasCheckboxCol && (_jsx("td", { className: styles.selectionCell, children: _jsx("div", { className: styles.selectionCellInner, "data-row-index": rowIndex, "data-col-index": 0, onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: selectedRowIds.has(rowIdStr), onCheckedChange: (c) => handleRowCheckboxChange(rowIdStr, !!c, rowIndex, lastMouseShiftRef.current), "aria-label": `Select row ${rowIndex + 1}`, children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: "\u2713" }) }) }) })), visibleCols.map((col, colIdx) => {
92
- const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
93
- const isPinnedLeft = col.pinned === 'left';
94
- const isPinnedRight = col.pinned === 'right';
95
- const columnWidth = getColumnWidth(col);
96
- const hasExplicitWidth = !!(columnSizingOverrides[col.columnId] || col.idealWidth != null || col.defaultWidth != null);
97
- return (_jsx("td", { className: [
98
- isFreezeCol ? styles.freezeCol : '',
99
- isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
100
- isPinnedLeft ? styles.pinnedColLeft : '',
101
- isPinnedRight ? styles.pinnedColRight : '',
102
- ].filter(Boolean).join(' '), style: {
103
- minWidth: col.minWidth ?? 80,
104
- width: hasExplicitWidth ? columnWidth : undefined,
105
- maxWidth: hasExplicitWidth ? columnWidth : undefined,
106
- textAlign: col.type === 'numeric' ? 'right' : col.type === 'boolean' ? 'center' : undefined,
107
- }, children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId));
108
- })] }, rowIdStr));
109
- }) }))] }), _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.') })] })) }) }))] }) }), 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 }))] }), contextMenu &&
110
- createPortal(_jsx(GridContextMenu, { x: contextMenu.x, y: contextMenu.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? (() => { }), onRedo: onRedo ?? (() => { }), onCopy: handleCopy, onCut: handleCut, onPaste: () => void handlePaste(), onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body)] }));
118
+ return (_jsxs("div", { style: GRID_ROOT_STYLE, children: [_jsxs("div", { ref: wrapperRef, tabIndex: 0, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, 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-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, onKeyDown: handleGridKeyDown, style: {
119
+ ['--data-table-column-count']: totalColCount,
120
+ ['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
121
+ ['--data-table-min-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'max-content' : fitToContent ? 'max-content' : '100%',
122
+ ['--data-table-total-min-width']: `${minTableWidth}px`,
123
+ }, children: [_jsxs("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("table", { className: styles.dataTable, children: [_jsx("thead", { className: styles.stickyHeader, children: headerRows.map((row, rowIdx) => (_jsxs("tr", { children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { className: styles.selectionHeaderCell, scope: "col", rowSpan: 1, children: _jsx("div", { className: styles.selectionHeaderCellInner, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: allSelected ? true : someSelected ? 'indeterminate' : false, onCheckedChange: handleSelectAllChecked, "aria-label": "Select all rows", children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: someSelected && !allSelected ? '–' : '✓' }) }) }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1 })), row.map((cell, cellIdx) => {
124
+ if (cell.isGroup) {
125
+ return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
126
+ }
127
+ // Leaf cell
128
+ const col = cell.columnDef;
129
+ const leafRowSpan = headerRows.length > 1 && rowIdx < headerRows.length - 1
130
+ ? headerRows.length - rowIdx
131
+ : undefined;
132
+ 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));
133
+ })] }, rowIdx))) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
134
+ const rowIdStr = getRowId(item);
135
+ const isSelected = selectedRowIds.has(rowIdStr);
136
+ return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', "data-row-id": rowIdStr, onClick: handleSingleRowClick, children: [hasCheckboxCol && (_jsx("td", { className: styles.selectionCell, children: _jsx("div", { className: styles.selectionCellInner, "data-row-index": rowIndex, "data-col-index": 0, onClick: STOP_PROPAGATION, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: selectedRowIds.has(rowIdStr), onCheckedChange: (c) => handleRowCheckboxChange(rowIdStr, !!c, rowIndex, lastMouseShiftRef.current), "aria-label": `Select row ${rowIndex + 1}`, children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: "\u2713" }) }) }) })), visibleCols.map((col, colIdx) => (_jsx("td", { className: columnMeta.cellClasses[col.columnId] || undefined, style: columnMeta.cellStyles[col.columnId], children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId)))] }, rowIdStr));
137
+ }) }))] }), _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.') })] })) }) }))] }) }), 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 }))] }), menuPosition &&
138
+ createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? (() => { }), onRedo: onRedo ?? (() => { }), onCopy: handleCopy, onCut: handleCut, onPaste: () => void handlePaste(), onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body)] }), isLoading && items.length > 0 && (_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 })] }) }))] }));
111
139
  }
112
140
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -19,6 +19,7 @@
19
19
  max-width: 100%;
20
20
  box-sizing: border-box;
21
21
  background: var(--ogrid-bg, #fff);
22
+ will-change: scroll-position;
22
23
  }
23
24
  .tableWrapper[data-overflow-x=true] {
24
25
  overflow-x: auto;
@@ -76,7 +77,7 @@
76
77
  .dataTable thead.stickyHeader {
77
78
  position: sticky;
78
79
  top: 0;
79
- z-index: 2;
80
+ z-index: 6;
80
81
  background: var(--ogrid-bg-subtle, #f3f2f1);
81
82
  }
82
83
 
@@ -90,6 +91,17 @@
90
91
  left: 0;
91
92
  z-index: 2;
92
93
  background: var(--ogrid-bg, #ffffff);
94
+ will-change: transform;
95
+ }
96
+
97
+ /* Freeze/pinned header cells need top: 0 + z-index: 3 so they stick in BOTH
98
+ directions — left (from .freezeCol/.pinnedCol) AND top (from .stickyHeader).
99
+ Without explicit top, the child's own position: sticky overrides the parent's. */
100
+ .dataTable thead.stickyHeader .freezeColFirst,
101
+ .dataTable thead.stickyHeader .pinnedColLeft,
102
+ .dataTable thead.stickyHeader .pinnedColRight {
103
+ top: 0;
104
+ z-index: 7;
93
105
  }
94
106
 
95
107
  .dataTable thead .freezeColFirst {
@@ -104,6 +116,7 @@
104
116
  left: 0;
105
117
  z-index: 2;
106
118
  background: var(--ogrid-bg, #ffffff);
119
+ will-change: transform;
107
120
  }
108
121
  .dataTable .pinnedColLeft::after {
109
122
  content: "";
@@ -125,6 +138,7 @@
125
138
  right: 0;
126
139
  z-index: 2;
127
140
  background: var(--ogrid-bg, #ffffff);
141
+ will-change: transform;
128
142
  }
129
143
  .dataTable .pinnedColRight::before {
130
144
  content: "";
@@ -6,8 +6,8 @@ import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
6
6
  import { PaginationControls } from '../PaginationControls/PaginationControls';
7
7
  import { useOGrid, OGridLayout, } from '@alaarab/ogrid-core';
8
8
  const OGridInner = forwardRef(function OGridInner(props, ref) {
9
- const { dataGridProps, page, pageSize, displayTotalCount, setPage, setPageSize, columnChooserColumns, visibleColumns, handleVisibilityChange, columnChooserPlacement, title, toolbar, className, entityLabelPlural, pageSizeOptions, sideBarProps, } = useOGrid(props, ref);
10
- return (_jsx(OGridLayout, { className: className, gap: 8, sideBar: sideBarProps, title: title, toolbar: toolbar, toolbarEnd: columnChooserPlacement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooserColumns, visibleColumns: visibleColumns, onVisibilityChange: handleVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: page, pageSize: pageSize, totalCount: displayTotalCount, onPageChange: setPage, onPageSizeChange: (size) => {
9
+ const { dataGridProps, page, pageSize, displayTotalCount, setPage, setPageSize, columnChooserColumns, visibleColumns, handleVisibilityChange, columnChooserPlacement, toolbar, toolbarBelow, className, entityLabelPlural, pageSizeOptions, sideBarProps, } = useOGrid(props, ref);
10
+ return (_jsx(OGridLayout, { className: className, sideBar: sideBarProps, toolbar: toolbar, toolbarBelow: toolbarBelow, toolbarEnd: columnChooserPlacement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooserColumns, visibleColumns: visibleColumns, onVisibilityChange: handleVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: page, pageSize: pageSize, totalCount: displayTotalCount, onPageChange: setPage, onPageSizeChange: (size) => {
11
11
  setPageSize(size);
12
12
  setPage(1);
13
13
  }, pageSizeOptions: pageSizeOptions, entityLabelPlural: entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
package/dist/esm/index.js CHANGED
@@ -4,5 +4,5 @@ export { DataGridTable } from './DataGridTable/DataGridTable';
4
4
  export { ColumnChooser } from './ColumnChooser/ColumnChooser';
5
5
  export { ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter';
6
6
  export { PaginationControls } from './PaginationControls/PaginationControls';
7
- // Re-export from core
8
- export { toUserLike, isInSelectionRange, normalizeSelectionRange, toDataGridFilterProps, useFilterOptions, getCellValue, flattenColumns, escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, } from '@alaarab/ogrid-core';
7
+ // Re-export everything from core
8
+ export * from '@alaarab/ogrid-core';
@@ -1,7 +1,5 @@
1
1
  import * as React from 'react';
2
2
  import type { IOGridDataGridProps } from '@alaarab/ogrid-core';
3
- /** @deprecated Use IOGridDataGridProps from @alaarab/ogrid-core for new code. */
4
- export type IDataGridTableProps<T> = IOGridDataGridProps<T>;
5
3
  declare function DataGridTableInner<T>(props: IOGridDataGridProps<T>): React.ReactElement;
6
4
  export declare const DataGridTable: typeof DataGridTableInner;
7
5
  export {};
@@ -1,6 +1,6 @@
1
1
  export { OGrid, type IOGridProps } from './OGrid/OGrid';
2
- export { DataGridTable, type IDataGridTableProps } from './DataGridTable/DataGridTable';
2
+ export { DataGridTable } from './DataGridTable/DataGridTable';
3
3
  export { ColumnChooser, type IColumnChooserProps } from './ColumnChooser/ColumnChooser';
4
4
  export { ColumnHeaderFilter, type IColumnHeaderFilterProps } from './ColumnHeaderFilter/ColumnHeaderFilter';
5
5
  export { PaginationControls, type IPaginationControlsProps } from './PaginationControls/PaginationControls';
6
- export { type ColumnFilterType, type IColumnFilterDef, type IColumnMeta, type IColumnDef, type IColumnGroupDef, type IColumnDefinition, type ICellValueChangedEvent, type ICellEditorProps, type UserLike, type IFilters, type IFetchParams, type IPageResult, type IDataSource, type IGridColumnState, type IOGridApi, type RowSelectionMode, type IRowSelectionChangeEvent, type StatusBarPanel, type IStatusBarProps, type IActiveCell, type ISelectionRange, toUserLike, isInSelectionRange, normalizeSelectionRange, toDataGridFilterProps, useFilterOptions, type UseFilterOptionsResult, getCellValue, flattenColumns, escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, type CsvColumn, } from '@alaarab/ogrid-core';
6
+ export * from '@alaarab/ogrid-core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid",
3
- "version": "1.5.0",
3
+ "version": "1.7.1",
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-core": "^1.5.0",
44
+ "@alaarab/ogrid-core": "^1.7.1",
45
45
  "@radix-ui/react-checkbox": "^1.1.2",
46
46
  "@radix-ui/react-popover": "^1.1.2"
47
47
  },