@alaarab/ogrid 1.8.1 → 1.9.0
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.
package/README.md
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>OGrid</strong> — The lightweight React data grid with enterprise features and zero enterprise cost.
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@alaarab/ogrid"><img src="https://img.shields.io/npm/v/@alaarab/ogrid?color=%23217346&label=npm" alt="npm version" /></a>
|
|
7
|
+
<a href="https://github.com/alaarab/ogrid/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License" /></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/React-17%20%7C%2018%20%7C%2019-blue" alt="React 17, 18, 19" />
|
|
9
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-blue" alt="TypeScript strict" />
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://alaarab.github.io/ogrid/">Documentation</a> · <a href="https://alaarab.github.io/ogrid/docs/getting-started/overview">Getting Started</a> · <a href="https://alaarab.github.io/ogrid/docs/api/ogrid-props">API Reference</a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
This is the **default OGrid package** built with Radix UI primitives — lightweight, no Fluent/Material dependency. Also available for [Fluent UI](https://www.npmjs.com/package/@alaarab/ogrid-fluent) and [Material UI](https://www.npmjs.com/package/@alaarab/ogrid-material). Same API, just swap the import.
|
|
19
|
+
|
|
20
|
+
## Why OGrid?
|
|
21
|
+
|
|
22
|
+
| | OGrid | AG Grid Community | AG Grid Enterprise |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| Spreadsheet selection | Built-in | - | $999/dev/year |
|
|
25
|
+
| Clipboard (copy/paste) | Built-in | - | $999/dev/year |
|
|
26
|
+
| Fill handle | Built-in | - | $999/dev/year |
|
|
27
|
+
| Undo/redo | Built-in | - | $999/dev/year |
|
|
28
|
+
| Context menu | Built-in | - | $999/dev/year |
|
|
29
|
+
| Status bar | Built-in | - | $999/dev/year |
|
|
30
|
+
| Side bar | Built-in | - | $999/dev/year |
|
|
31
|
+
| Cell editing | Built-in | Built-in | Built-in |
|
|
32
|
+
| Sorting & filtering | Built-in | Built-in | Built-in |
|
|
33
|
+
| **License** | **MIT (free)** | MIT | Commercial |
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
Sorting · Filtering (text, multi-select, date range, people picker) · Pagination · Cell editing (inline, select, checkbox, rich select, date, custom popover) · Spreadsheet selection · Clipboard · Fill handle · Undo/redo · Row selection · Column groups · Column pinning · Column resize · Column chooser · Side bar · Context menu · Status bar with aggregations · CSV export · Grid API · Server-side data · Column state persistence · Keyboard navigation (Excel-style Ctrl+Arrow) · Built-in column types (text, numeric, date, boolean) · React 17/18/19 · TypeScript strict
|
|
4
38
|
|
|
5
39
|
## Install
|
|
6
40
|
|
|
@@ -8,49 +42,37 @@
|
|
|
8
42
|
npm install @alaarab/ogrid
|
|
9
43
|
```
|
|
10
44
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
react ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
15
|
-
react-dom ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Radix UI primitives (`@radix-ui/react-checkbox`, `@radix-ui/react-popover`) are bundled as regular dependencies.
|
|
45
|
+
Radix UI primitives are bundled as regular dependencies — only `react` and `react-dom` are peer deps.
|
|
19
46
|
|
|
20
47
|
## Quick Start
|
|
21
48
|
|
|
22
49
|
```tsx
|
|
23
50
|
import { OGrid, type IColumnDef } from '@alaarab/ogrid';
|
|
24
51
|
|
|
25
|
-
const columns: IColumnDef<
|
|
26
|
-
{ columnId: 'name', name: 'Name', sortable: true,
|
|
27
|
-
{ columnId: '
|
|
52
|
+
const columns: IColumnDef<Employee>[] = [
|
|
53
|
+
{ columnId: 'name', name: 'Name', sortable: true, editable: true },
|
|
54
|
+
{ columnId: 'department', name: 'Department',
|
|
55
|
+
filterable: { type: 'multiSelect' },
|
|
56
|
+
cellEditor: 'richSelect', cellEditorParams: { values: ['Engineering', 'Sales', 'Marketing'] } },
|
|
57
|
+
{ columnId: 'salary', name: 'Salary', type: 'numeric', editable: true,
|
|
58
|
+
valueFormatter: (v) => `$${Number(v).toLocaleString()}` },
|
|
28
59
|
];
|
|
29
60
|
|
|
30
|
-
<OGrid
|
|
31
|
-
data={products}
|
|
61
|
+
<OGrid
|
|
32
62
|
columns={columns}
|
|
33
|
-
|
|
34
|
-
|
|
63
|
+
data={employees}
|
|
64
|
+
getRowId={(e) => e.id}
|
|
65
|
+
editable
|
|
66
|
+
cellSelection
|
|
67
|
+
statusBar
|
|
68
|
+
sideBar
|
|
35
69
|
/>
|
|
36
70
|
```
|
|
37
71
|
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
- **`OGrid<T>`** -- Full table with column chooser, filters, and pagination (Radix/native implementation)
|
|
41
|
-
- **`DataGridTable<T>`** -- Lower-level grid for custom state management
|
|
42
|
-
- **`ColumnChooser`** -- Column visibility dropdown
|
|
43
|
-
- **`PaginationControls`** -- Pagination UI
|
|
44
|
-
- **`ColumnHeaderFilter`** -- Column header with sort/filter (used internally)
|
|
72
|
+
## Documentation
|
|
45
73
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## Storybook
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
npm run storybook
|
|
52
|
-
```
|
|
74
|
+
Full docs, API reference, and interactive examples at **[alaarab.github.io/ogrid](https://alaarab.github.io/ogrid/)**.
|
|
53
75
|
|
|
54
76
|
## License
|
|
55
77
|
|
|
56
|
-
MIT
|
|
78
|
+
MIT — Free forever. No enterprise tiers. No feature paywalls.
|
|
@@ -8,70 +8,19 @@ 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, getHeaderFilterConfig, getCellRenderDescriptor,
|
|
11
|
+
import { useDataGridState, useColumnResize, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-core';
|
|
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' };
|
|
15
15
|
const CURSOR_CELL_STYLE = { cursor: 'cell' };
|
|
16
|
+
const POPOVER_ANCHOR_STYLE = { minHeight: '100%', minWidth: 40 };
|
|
16
17
|
const STOP_PROPAGATION = (e) => e.stopPropagation();
|
|
17
18
|
const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
|
|
19
|
+
const NOOP = () => { };
|
|
18
20
|
function GridRowInner(props) {
|
|
19
21
|
const { item, rowIndex, rowId, isSelected, visibleCols, columnMeta, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, } = props;
|
|
20
22
|
return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', "data-row-id": rowId, 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: isSelected, onCheckedChange: (c) => handleRowCheckboxChange(rowId, !!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)))] }));
|
|
21
23
|
}
|
|
22
|
-
function areGridRowPropsEqual(prev, next) {
|
|
23
|
-
// Data / structure changes — always re-render
|
|
24
|
-
if (prev.item !== next.item)
|
|
25
|
-
return false;
|
|
26
|
-
if (prev.isSelected !== next.isSelected)
|
|
27
|
-
return false;
|
|
28
|
-
if (prev.visibleCols !== next.visibleCols)
|
|
29
|
-
return false;
|
|
30
|
-
if (prev.columnMeta !== next.columnMeta)
|
|
31
|
-
return false;
|
|
32
|
-
if (prev.hasCheckboxCol !== next.hasCheckboxCol)
|
|
33
|
-
return false;
|
|
34
|
-
const ri = prev.rowIndex;
|
|
35
|
-
// Editing cell in this row?
|
|
36
|
-
if (prev.editingRowId !== next.editingRowId) {
|
|
37
|
-
if (prev.editingRowId === prev.rowId || next.editingRowId === next.rowId)
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
// Active cell in this row?
|
|
41
|
-
const prevActive = prev.activeCell?.rowIndex === ri;
|
|
42
|
-
const nextActive = next.activeCell?.rowIndex === ri;
|
|
43
|
-
if (prevActive !== nextActive)
|
|
44
|
-
return false;
|
|
45
|
-
if (prevActive && nextActive && prev.activeCell.columnIndex !== next.activeCell.columnIndex)
|
|
46
|
-
return false;
|
|
47
|
-
// Selection range touches this row?
|
|
48
|
-
const prevInSel = isRowInRange(prev.selectionRange, ri);
|
|
49
|
-
const nextInSel = isRowInRange(next.selectionRange, ri);
|
|
50
|
-
if (prevInSel !== nextInSel)
|
|
51
|
-
return false;
|
|
52
|
-
if (prevInSel && nextInSel) {
|
|
53
|
-
if (prev.selectionRange.startCol !== next.selectionRange.startCol ||
|
|
54
|
-
prev.selectionRange.endCol !== next.selectionRange.endCol)
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
// Fill handle (selection end row) + isDragging
|
|
58
|
-
const prevIsEnd = prev.selectionRange?.endRow === ri;
|
|
59
|
-
const nextIsEnd = next.selectionRange?.endRow === ri;
|
|
60
|
-
if (prevIsEnd !== nextIsEnd)
|
|
61
|
-
return false;
|
|
62
|
-
if ((prevIsEnd || nextIsEnd) && prev.isDragging !== next.isDragging)
|
|
63
|
-
return false;
|
|
64
|
-
// Cut/copy ranges touch this row?
|
|
65
|
-
if (prev.cutRange !== next.cutRange) {
|
|
66
|
-
if (isRowInRange(prev.cutRange, ri) || isRowInRange(next.cutRange, ri))
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
if (prev.copyRange !== next.copyRange) {
|
|
70
|
-
if (isRowInRange(prev.copyRange, ri) || isRowInRange(next.copyRange, ri))
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
24
|
const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
|
|
76
25
|
function DataGridTableInner(props) {
|
|
77
26
|
const wrapperRef = useRef(null);
|
|
@@ -83,8 +32,9 @@ function DataGridTableInner(props) {
|
|
|
83
32
|
const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
84
33
|
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
85
34
|
const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
|
|
35
|
+
const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
|
|
86
36
|
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
87
|
-
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid } = viewModels;
|
|
37
|
+
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
|
|
88
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;
|
|
89
39
|
// Memoize header rows (recursive tree traversal — avoid recomputing every render)
|
|
90
40
|
const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
|
|
@@ -98,12 +48,9 @@ function DataGridTableInner(props) {
|
|
|
98
48
|
const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
|
|
99
49
|
// Refs for volatile state — lets renderCellContent be stable (same function ref across
|
|
100
50
|
// selection changes) so that GridRow's React.memo comparator can skip unaffected rows.
|
|
101
|
-
const cellDescriptorInputRef =
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
pendingEditorValueRef.current = pendingEditorValue;
|
|
105
|
-
const popoverAnchorElRef = useRef(popoverAnchorEl);
|
|
106
|
-
popoverAnchorElRef.current = popoverAnchorEl;
|
|
51
|
+
const cellDescriptorInputRef = useLatestRef(cellDescriptorInput);
|
|
52
|
+
const pendingEditorValueRef = useLatestRef(pendingEditorValue);
|
|
53
|
+
const popoverAnchorElRef = useLatestRef(popoverAnchorEl);
|
|
107
54
|
// Pre-compute column styles and classNames (avoids per-cell object creation in the row loop)
|
|
108
55
|
const columnMeta = useMemo(() => {
|
|
109
56
|
const cellStyles = {};
|
|
@@ -118,13 +65,13 @@ function DataGridTableInner(props) {
|
|
|
118
65
|
const isPinnedLeft = col.pinned === 'left';
|
|
119
66
|
const isPinnedRight = col.pinned === 'right';
|
|
120
67
|
cellStyles[col.columnId] = {
|
|
121
|
-
minWidth: col.minWidth ??
|
|
68
|
+
minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
|
|
122
69
|
width: hasExplicitWidth ? columnWidth : undefined,
|
|
123
70
|
maxWidth: hasExplicitWidth ? columnWidth : undefined,
|
|
124
71
|
textAlign: col.type === 'numeric' ? 'right' : col.type === 'boolean' ? 'center' : undefined,
|
|
125
72
|
};
|
|
126
73
|
hdrStyles[col.columnId] = {
|
|
127
|
-
minWidth: col.minWidth ??
|
|
74
|
+
minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
|
|
128
75
|
width: hasExplicitWidth ? columnWidth : undefined,
|
|
129
76
|
maxWidth: hasExplicitWidth ? columnWidth : undefined,
|
|
130
77
|
};
|
|
@@ -144,8 +91,7 @@ function DataGridTableInner(props) {
|
|
|
144
91
|
return { cellStyles, cellClasses, hdrStyles, hdrClasses };
|
|
145
92
|
}, [visibleCols, getColumnWidth, columnSizingOverrides, freezeCols]);
|
|
146
93
|
// Stable row-click handler (avoids creating a new arrow function per row)
|
|
147
|
-
const selectedRowIdsRef =
|
|
148
|
-
selectedRowIdsRef.current = selectedRowIds;
|
|
94
|
+
const selectedRowIdsRef = useLatestRef(selectedRowIds);
|
|
149
95
|
const handleSingleRowClick = useCallback((e) => {
|
|
150
96
|
if (rowSelection !== 'single')
|
|
151
97
|
return;
|
|
@@ -161,28 +107,27 @@ function DataGridTableInner(props) {
|
|
|
161
107
|
// GridRow's React.memo comparator can skip rows whose selection state hasn't changed.
|
|
162
108
|
const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
|
|
163
109
|
const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
|
|
110
|
+
const rowId = getRowId(item);
|
|
111
|
+
let content;
|
|
164
112
|
if (descriptor.mode === 'editing-inline') {
|
|
165
|
-
|
|
113
|
+
content = _jsx(InlineCellEditor, { ...buildInlineEditorProps(item, col, descriptor, editCallbacks) });
|
|
166
114
|
}
|
|
167
|
-
if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
|
|
115
|
+
else if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
|
|
168
116
|
const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValueRef.current, editCallbacks);
|
|
169
117
|
const CustomEditor = col.cellEditor;
|
|
170
|
-
|
|
171
|
-
cancelPopoverEdit(); }, children: [_jsx(Popover.Anchor, { asChild: true, children: _jsx("div", { ref: (el) => el && setPopoverAnchorEl(el), style:
|
|
118
|
+
content = (_jsxs(Popover.Root, { open: !!popoverAnchorElRef.current, onOpenChange: (open) => { if (!open)
|
|
119
|
+
cancelPopoverEdit(); }, children: [_jsx(Popover.Anchor, { asChild: true, children: _jsx("div", { ref: (el) => el && setPopoverAnchorEl(el), style: POPOVER_ANCHOR_STYLE, "aria-hidden": true }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Content, { sideOffset: 4, onOpenAutoFocus: (e) => e.preventDefault(), children: _jsx(CustomEditor, { ...editorProps }) }) })] }));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const displayContent = resolveCellDisplayContent(col, item, descriptor.displayValue);
|
|
123
|
+
const cellStyle = resolveCellStyle(col, item);
|
|
124
|
+
const styledContent = cellStyle ? _jsx("span", { style: cellStyle, children: displayContent }) : displayContent;
|
|
125
|
+
const cellClassNames = `${styles.cellContent}${descriptor.isActive && !descriptor.isInRange ? ` ${styles.activeCellContent}` : ''}${descriptor.isInRange ? ` ${styles.cellInRange}` : ''}${descriptor.isInCutRange ? ` ${styles.cellCut}` : ''}${descriptor.isInCopyRange ? ` ${styles.cellCopied}` : ''}`;
|
|
126
|
+
const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
|
|
127
|
+
content = (_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" }))] }));
|
|
172
128
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const styledContent = cellStyle ? _jsx("span", { style: cellStyle, children: content }) : content;
|
|
176
|
-
const cellClassNames = [
|
|
177
|
-
styles.cellContent,
|
|
178
|
-
descriptor.isActive && !descriptor.isInRange ? styles.activeCellContent : '',
|
|
179
|
-
descriptor.isInRange ? styles.cellInRange : '',
|
|
180
|
-
descriptor.isInCutRange ? styles.cellCut : '',
|
|
181
|
-
descriptor.isInCopyRange ? styles.cellCopied : '',
|
|
182
|
-
].filter(Boolean).join(' ');
|
|
183
|
-
const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
|
|
184
|
-
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" }))] }));
|
|
185
|
-
}, [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit]);
|
|
129
|
+
return (_jsx(CellErrorBoundary, { onError: onCellError, children: content }, `${rowId}-${col.columnId}`));
|
|
130
|
+
}, [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
|
|
186
131
|
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: {
|
|
187
132
|
['--data-table-column-count']: totalColCount,
|
|
188
133
|
['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
|
|
@@ -202,6 +147,6 @@ function DataGridTableInner(props) {
|
|
|
202
147
|
const rowIdStr = getRowId(item);
|
|
203
148
|
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));
|
|
204
149
|
}) }))] }), _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 &&
|
|
205
|
-
createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ??
|
|
150
|
+
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 && 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 })] }) }))] }));
|
|
206
151
|
}
|
|
207
152
|
export const DataGridTable = React.memo(DataGridTableInner);
|
|
@@ -1,85 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import * as React from 'react';
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
2
|
import * as Checkbox from '@radix-ui/react-checkbox';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const editorWrapperStyle = {
|
|
7
|
-
width: '100%',
|
|
8
|
-
height: '100%',
|
|
9
|
-
display: 'flex',
|
|
10
|
-
alignItems: 'center',
|
|
11
|
-
padding: '6px 10px',
|
|
12
|
-
boxSizing: 'border-box',
|
|
13
|
-
overflow: 'hidden',
|
|
14
|
-
minWidth: 0,
|
|
15
|
-
};
|
|
16
|
-
const editorInputStyle = {
|
|
17
|
-
width: '100%',
|
|
18
|
-
padding: 0,
|
|
19
|
-
border: 'none',
|
|
20
|
-
background: 'transparent',
|
|
21
|
-
color: 'inherit',
|
|
22
|
-
font: 'inherit',
|
|
23
|
-
fontSize: '13px',
|
|
24
|
-
outline: 'none',
|
|
25
|
-
minWidth: 0,
|
|
26
|
-
};
|
|
3
|
+
import { BaseInlineCellEditor, selectEditorStyle } from '@alaarab/ogrid-core';
|
|
4
|
+
const selectWrapperStyle = { width: '100%', height: '100%', display: 'flex', alignItems: 'center', padding: '6px 10px', boxSizing: 'border-box', overflow: 'hidden', minWidth: 0 };
|
|
27
5
|
export function InlineCellEditor(props) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const richSelectFormatValue = column.cellEditorParams?.formatValue;
|
|
33
|
-
const richSelect = useRichSelectState({
|
|
34
|
-
values: richSelectValues,
|
|
35
|
-
formatValue: richSelectFormatValue,
|
|
36
|
-
initialValue: value,
|
|
37
|
-
onCommit,
|
|
38
|
-
onCancel,
|
|
39
|
-
});
|
|
40
|
-
React.useEffect(() => {
|
|
41
|
-
const input = wrapperRef.current?.querySelector('input');
|
|
42
|
-
input?.focus();
|
|
43
|
-
}, []);
|
|
44
|
-
if (editorType === 'richSelect') {
|
|
45
|
-
return (_jsxs("div", { ref: wrapperRef, style: { ...editorWrapperStyle, position: 'relative' }, children: [_jsx("input", { type: "text", value: richSelect.searchText, onChange: (e) => richSelect.setSearchText(e.target.value), onKeyDown: richSelect.handleKeyDown, placeholder: "Search...", autoFocus: true, style: editorInputStyle }), _jsxs("div", { style: {
|
|
46
|
-
position: 'absolute',
|
|
47
|
-
top: '100%',
|
|
48
|
-
left: 0,
|
|
49
|
-
right: 0,
|
|
50
|
-
maxHeight: 200,
|
|
51
|
-
overflowY: 'auto',
|
|
52
|
-
background: 'var(--ogrid-bg, #fff)',
|
|
53
|
-
border: '1px solid var(--ogrid-border, #ccc)',
|
|
54
|
-
zIndex: 10,
|
|
55
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
|
|
56
|
-
}, role: "listbox", children: [richSelect.filteredValues.map((v, i) => (_jsx("div", { role: "option", "aria-selected": i === richSelect.highlightedIndex, onClick: () => richSelect.selectValue(v), style: {
|
|
57
|
-
padding: '6px 8px',
|
|
58
|
-
cursor: 'pointer',
|
|
59
|
-
color: 'var(--ogrid-fg, #242424)',
|
|
60
|
-
background: i === richSelect.highlightedIndex ? 'var(--ogrid-bg-hover, #e8f0fe)' : undefined,
|
|
61
|
-
}, children: richSelect.getDisplayText(v) }, String(v)))), richSelect.filteredValues.length === 0 && (_jsx("div", { style: { padding: '6px 8px', color: 'var(--ogrid-muted, #999)' }, children: "No matches" }))] })] }));
|
|
62
|
-
}
|
|
63
|
-
if (editorType === 'checkbox') {
|
|
64
|
-
const checked = value === true;
|
|
65
|
-
return (_jsx(Checkbox.Root, { checked: checked, onCheckedChange: (c) => commit(c === true), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), cancel()), children: _jsx(Checkbox.Indicator, { children: "\u2713" }) }));
|
|
66
|
-
}
|
|
67
|
-
if (editorType === 'select') {
|
|
68
|
-
const values = column.cellEditorParams?.values ?? [];
|
|
69
|
-
return (_jsx("div", { style: editorWrapperStyle, children: _jsx("select", { value: value !== null && value !== undefined ? String(value) : '', onChange: (e) => commit(e.target.value), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), cancel()), autoFocus: true, style: {
|
|
70
|
-
width: '100%',
|
|
71
|
-
padding: 0,
|
|
72
|
-
border: 'none',
|
|
73
|
-
background: 'transparent',
|
|
74
|
-
color: 'inherit',
|
|
75
|
-
font: 'inherit',
|
|
76
|
-
fontSize: '13px',
|
|
77
|
-
cursor: 'pointer',
|
|
78
|
-
outline: 'none',
|
|
79
|
-
}, children: values.map((v) => (_jsx("option", { value: String(v), children: String(v) }, String(v)))) }) }));
|
|
80
|
-
}
|
|
81
|
-
if (editorType === 'date') {
|
|
82
|
-
return (_jsx("div", { ref: wrapperRef, style: editorWrapperStyle, children: _jsx("input", { type: "date", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: editorInputStyle, autoFocus: true }) }));
|
|
83
|
-
}
|
|
84
|
-
return (_jsx("div", { ref: wrapperRef, style: editorWrapperStyle, children: _jsx("input", { type: "text", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: editorInputStyle, autoFocus: true }) }));
|
|
6
|
+
return (_jsx(BaseInlineCellEditor, { ...props, renderCheckbox: (checked, onCommit, onCancel) => (_jsx(Checkbox.Root, { checked: checked, onCheckedChange: (c) => onCommit(c === true), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), onCancel()), children: _jsx(Checkbox.Indicator, { children: "\u2713" }) })), renderSelect: (value, values, onCommit, onCancel) => {
|
|
7
|
+
const { column } = props;
|
|
8
|
+
return (_jsx("div", { style: selectWrapperStyle, children: _jsx("select", { value: value !== null && value !== undefined ? String(value) : '', onChange: (e) => onCommit(e.target.value), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), onCancel()), autoFocus: true, style: selectEditorStyle, children: values.map((v) => (_jsx("option", { value: String(v), children: String(v) }, String(v)))) }) }));
|
|
9
|
+
} }));
|
|
85
10
|
}
|
package/dist/esm/OGrid/OGrid.js
CHANGED
|
@@ -6,11 +6,11 @@ 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,
|
|
10
|
-
return (_jsx(OGridLayout, { className: className, sideBar: sideBarProps, toolbar: toolbar, toolbarBelow: toolbarBelow, toolbarEnd:
|
|
11
|
-
setPageSize(size);
|
|
12
|
-
setPage(1);
|
|
13
|
-
}, pageSizeOptions: pageSizeOptions, entityLabelPlural: entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
|
|
9
|
+
const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
|
|
10
|
+
return (_jsx(OGridLayout, { className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: (size) => {
|
|
11
|
+
pagination.setPageSize(size);
|
|
12
|
+
pagination.setPage(1);
|
|
13
|
+
}, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
|
|
14
14
|
});
|
|
15
15
|
OGridInner.displayName = 'OGrid';
|
|
16
16
|
export const OGrid = React.memo(OGridInner);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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.8.
|
|
44
|
+
"@alaarab/ogrid-core": "^1.8.2",
|
|
45
45
|
"@radix-ui/react-checkbox": "^1.1.2",
|
|
46
46
|
"@radix-ui/react-popover": "^1.1.2"
|
|
47
47
|
},
|