@alaarab/ogrid 1.3.2 → 1.4.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/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +7 -2
- package/dist/esm/DataGridTable/DataGridTable.js +54 -68
- package/dist/esm/DataGridTable/DataGridTable.module.css +16 -17
- package/dist/esm/DataGridTable/InlineCellEditor.js +67 -4
- package/dist/esm/OGrid/OGrid.js +3 -3
- package/dist/esm/PaginationControls/PaginationControls.js +2 -2
- package/dist/esm/PaginationControls/PaginationControls.module.css +1 -3
- package/dist/types/ColumnHeaderFilter/ColumnHeaderFilter.d.ts +3 -1
- package/dist/types/DataGridTable/InlineCellEditor.d.ts +1 -1
- package/dist/types/DataGridTable/StatusBar.d.ts +7 -0
- package/dist/types/PaginationControls/PaginationControls.d.ts +1 -0
- package/package.json +5 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, 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 * as Popover from '@radix-ui/react-popover';
|
|
4
4
|
import { useColumnHeaderFilterState } from '@alaarab/ogrid-core';
|
|
@@ -15,7 +15,7 @@ function FilterIcon() {
|
|
|
15
15
|
return _jsx("span", { "aria-hidden": true, children: '\u25BE' });
|
|
16
16
|
}
|
|
17
17
|
export const ColumnHeaderFilter = React.memo((props) => {
|
|
18
|
-
const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options = [], isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, } = props;
|
|
18
|
+
const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options = [], isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, dateValue, onDateChange, } = props;
|
|
19
19
|
const state = useColumnHeaderFilterState({
|
|
20
20
|
filterType,
|
|
21
21
|
isSorted,
|
|
@@ -30,6 +30,8 @@ export const ColumnHeaderFilter = React.memo((props) => {
|
|
|
30
30
|
selectedUser,
|
|
31
31
|
onUserChange,
|
|
32
32
|
peopleSearch,
|
|
33
|
+
dateValue,
|
|
34
|
+
onDateChange,
|
|
33
35
|
});
|
|
34
36
|
const { headerRef, popoverRef, peopleInputRef, isFilterOpen, setFilterOpen, tempSelected, tempTextValue, setTempTextValue, searchText, setSearchText, filteredOptions, peopleSuggestions, isPeopleLoading, peopleSearchText, setPeopleSearchText, hasActiveFilter, handlers, } = state;
|
|
35
37
|
const safeOptions = options ?? [];
|
|
@@ -43,6 +45,9 @@ export const ColumnHeaderFilter = React.memo((props) => {
|
|
|
43
45
|
if (filterType === 'people') {
|
|
44
46
|
return (_jsx(PeopleFilterPopover, { selectedUser: selectedUser, searchText: peopleSearchText, onSearchChange: setPeopleSearchText, suggestions: peopleSuggestions, isLoading: isPeopleLoading, onUserSelect: handlers.handleUserSelect, onClearUser: handlers.handleClearUser, inputRef: peopleInputRef }));
|
|
45
47
|
}
|
|
48
|
+
if (filterType === 'date') {
|
|
49
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { padding: '8px 12px', display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }, children: ["From:", _jsx("input", { type: "date", value: state.tempDateFrom, onChange: (e) => state.setTempDateFrom(e.target.value), style: { flex: 1 } })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }, children: ["To:", _jsx("input", { type: "date", value: state.tempDateTo, onChange: (e) => state.setTempDateTo(e.target.value), style: { flex: 1 } })] })] }), _jsxs("div", { className: styles.popoverActions, children: [_jsx("button", { className: styles.clearButton, onClick: handlers.handleDateClear, disabled: !state.tempDateFrom && !state.tempDateTo, children: "Clear" }), _jsx("button", { className: styles.applyButton, onClick: handlers.handleDateApply, children: "Apply" })] })] }));
|
|
50
|
+
}
|
|
46
51
|
return null;
|
|
47
52
|
};
|
|
48
53
|
return (_jsxs("div", { className: styles.columnHeader, ref: headerRef, children: [_jsx("div", { className: styles.headerContent, children: _jsx("span", { className: styles.columnName, title: columnName, "data-header-label": true, children: columnName }) }), _jsxs("div", { className: styles.headerActions, children: [onSort && (_jsx("button", { type: "button", className: `${styles.sortIcon} ${isSorted ? styles.sortActive : ''}`, onClick: handlers.handleSortClick, "aria-label": `Sort by ${columnName}`, title: isSorted ? (isSortedDescending ? 'Sorted descending' : 'Sorted ascending') : 'Sort', children: _jsx(SortIcon, { isSorted: isSorted, isDesc: isSortedDescending }) })), filterType !== 'none' && (_jsxs(Popover.Root, { open: isFilterOpen, onOpenChange: setFilterOpen, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: `${styles.filterIcon} ${hasActiveFilter ? styles.filterActive : ''} ${isFilterOpen ? styles.filterOpen : ''}`, onClick: handlers.handleFilterIconClick, "aria-label": `Filter ${columnName}`, title: `Filter ${columnName}`, children: [_jsx(FilterIcon, {}), hasActiveFilter && _jsx("span", { className: styles.filterBadge })] }) }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { ref: popoverRef, className: styles.popoverContent, sideOffset: 4, align: "start", onOpenAutoFocus: (e) => e.preventDefault(), children: [_jsxs("div", { className: styles.popoverHeader, children: ["Filter: ", columnName] }), renderPopoverContent()] }) })] }))] })] }));
|
|
@@ -8,60 +8,38 @@ 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, MarchingAntsOverlay, } from '@alaarab/ogrid-core';
|
|
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
13
|
function DataGridTableInner(props) {
|
|
14
14
|
const wrapperRef = useRef(null);
|
|
15
15
|
const tableContainerRef = useRef(null);
|
|
16
16
|
const state = useDataGridState({ props, wrapperRef });
|
|
17
17
|
const lastMouseShiftRef = useRef(false);
|
|
18
|
-
const { visibleCols, totalColCount, hasCheckboxCol, selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, setEditingCell, pendingEditorValue, setPendingEditorValue, setActiveCell, handleCellMouseDown, handleSelectAllCells, contextMenu,
|
|
19
|
-
const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
|
|
20
|
-
const
|
|
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;
|
|
19
|
+
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);
|
|
21
|
+
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
21
22
|
const fitToContent = layoutMode === 'content';
|
|
22
23
|
const { handleResizeStart, getColumnWidth } = useColumnResize({
|
|
23
24
|
columnSizingOverrides,
|
|
24
25
|
setColumnSizingOverrides,
|
|
25
26
|
});
|
|
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]);
|
|
26
29
|
const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
|
|
27
30
|
const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
|
|
28
31
|
if (descriptor.mode === 'editing-inline') {
|
|
29
|
-
return
|
|
32
|
+
return _jsx(InlineCellEditor, { ...buildInlineEditorProps(item, col, descriptor, editCallbacks) });
|
|
30
33
|
}
|
|
31
34
|
if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
|
|
32
|
-
const
|
|
33
|
-
const displayValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
|
|
35
|
+
const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValue, editCallbacks);
|
|
34
36
|
const CustomEditor = col.cellEditor;
|
|
35
|
-
const editorProps = {
|
|
36
|
-
value: displayValue,
|
|
37
|
-
onValueChange: setPendingEditorValue,
|
|
38
|
-
onCommit: () => {
|
|
39
|
-
const newValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
|
|
40
|
-
commitCellEdit(item, col.columnId, oldValue, newValue, descriptor.rowIndex, descriptor.globalColIndex);
|
|
41
|
-
},
|
|
42
|
-
onCancel: cancelPopoverEdit,
|
|
43
|
-
item,
|
|
44
|
-
column: col,
|
|
45
|
-
cellEditorParams: col.cellEditorParams,
|
|
46
|
-
};
|
|
47
37
|
return (_jsxs(Popover.Root, { open: !!popoverAnchorEl, onOpenChange: (open) => { if (!open)
|
|
48
38
|
cancelPopoverEdit(); }, children: [_jsx(Popover.Anchor, { asChild: true, children: _jsx("div", { ref: (el) => el && setPopoverAnchorEl(el), style: { minHeight: '100%', minWidth: 40 }, "aria-hidden": true }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Content, { sideOffset: 4, onOpenAutoFocus: (e) => e.preventDefault(), children: _jsx(CustomEditor, { ...editorProps }) }) })] }));
|
|
49
39
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
else {
|
|
54
|
-
const value = descriptor.displayValue;
|
|
55
|
-
if (col.valueFormatter)
|
|
56
|
-
content = col.valueFormatter(value, item);
|
|
57
|
-
else if (value !== null && value !== undefined)
|
|
58
|
-
content = String(value);
|
|
59
|
-
else
|
|
60
|
-
content = null;
|
|
61
|
-
}
|
|
62
|
-
const cellStyle = col.cellStyle ? (typeof col.cellStyle === 'function' ? col.cellStyle(item) : col.cellStyle) : undefined;
|
|
63
|
-
if (cellStyle)
|
|
64
|
-
content = _jsx("span", { style: cellStyle, children: content });
|
|
40
|
+
const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
|
|
41
|
+
const cellStyle = resolveCellStyle(col, item);
|
|
42
|
+
const styledContent = cellStyle ? _jsx("span", { style: cellStyle, children: content }) : content;
|
|
65
43
|
const cellClassNames = [
|
|
66
44
|
styles.cellContent,
|
|
67
45
|
descriptor.isActive && !descriptor.isInRange ? styles.activeCellContent : '',
|
|
@@ -69,47 +47,30 @@ function DataGridTableInner(props) {
|
|
|
69
47
|
descriptor.isInCutRange ? styles.cellCut : '',
|
|
70
48
|
descriptor.isInCopyRange ? styles.cellCopied : '',
|
|
71
49
|
].filter(Boolean).join(' ');
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return (_jsx("div", { className: cellClassNames, "data-row-index": descriptor.rowIndex, "data-col-index": descriptor.globalColIndex, "data-in-range": descriptor.isInRange ? 'true' : undefined, tabIndex: descriptor.isActive ? 0 : -1, onMouseDown: (e) => handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex), onClick: () => setActiveCell({ rowIndex: descriptor.rowIndex, columnIndex: descriptor.globalColIndex }), onContextMenu: handleCellContextMenu, children: content }));
|
|
76
|
-
}, [cellDescriptorInput, pendingEditorValue, popoverAnchorEl, handleCellMouseDown, handleCellContextMenu, handleFillHandleMouseDown, setActiveCell, setEditingCell, setPendingEditorValue, setPopoverAnchorEl, commitCellEdit, cancelPopoverEdit]);
|
|
50
|
+
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" }))] }));
|
|
52
|
+
}, [cellDescriptorInput, pendingEditorValue, popoverAnchorEl, editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit]);
|
|
77
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: {
|
|
78
54
|
['--data-table-column-count']: totalColCount,
|
|
79
55
|
['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
|
|
80
56
|
['--data-table-min-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'max-content' : fitToContent ? 'max-content' : '100%',
|
|
81
57
|
['--data-table-total-min-width']: `${minTableWidth}px`,
|
|
82
|
-
}, 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 })] }) })),
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
isFreezeCol ? styles.freezeCol : '',
|
|
90
|
-
isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
|
|
91
|
-
isPinnedLeft ? styles.pinnedColLeft : '',
|
|
92
|
-
isPinnedRight ? styles.pinnedColRight : '',
|
|
93
|
-
].filter(Boolean).join(' '), style: {
|
|
94
|
-
minWidth: col.minWidth ?? 80,
|
|
95
|
-
width: hasExplicitWidth ? columnWidth : undefined,
|
|
96
|
-
maxWidth: hasExplicitWidth ? columnWidth : undefined,
|
|
97
|
-
}, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
|
|
98
|
-
})] }) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
|
|
99
|
-
const rowIdStr = getRowId(item);
|
|
100
|
-
const isSelected = selectedRowIds.has(rowIdStr);
|
|
101
|
-
return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', onClick: () => {
|
|
102
|
-
if (rowSelection === 'single') {
|
|
103
|
-
const id = getRowId(item);
|
|
104
|
-
updateSelection(selectedRowIds.has(id) ? new Set() : new Set([id]));
|
|
105
|
-
}
|
|
106
|
-
}, 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) => {
|
|
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);
|
|
107
65
|
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
108
66
|
const isPinnedLeft = col.pinned === 'left';
|
|
109
67
|
const isPinnedRight = col.pinned === 'right';
|
|
110
68
|
const columnWidth = getColumnWidth(col);
|
|
111
69
|
const hasExplicitWidth = !!(columnSizingOverrides[col.columnId] || col.idealWidth != null || col.defaultWidth != null);
|
|
112
|
-
|
|
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: [
|
|
113
74
|
isFreezeCol ? styles.freezeCol : '',
|
|
114
75
|
isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
|
|
115
76
|
isPinnedLeft ? styles.pinnedColLeft : '',
|
|
@@ -118,9 +79,34 @@ function DataGridTableInner(props) {
|
|
|
118
79
|
minWidth: col.minWidth ?? 80,
|
|
119
80
|
width: hasExplicitWidth ? columnWidth : undefined,
|
|
120
81
|
maxWidth: hasExplicitWidth ? columnWidth : undefined,
|
|
121
|
-
}, children:
|
|
122
|
-
})] },
|
|
123
|
-
|
|
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 }))] }), contextMenu &&
|
|
124
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)] }));
|
|
125
111
|
}
|
|
126
112
|
export const DataGridTable = React.memo(DataGridTableInner);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
@charset "UTF-8";
|
|
2
1
|
.tableScrollContent {
|
|
3
2
|
display: flex;
|
|
4
3
|
flex-direction: column;
|
|
@@ -9,13 +8,15 @@
|
|
|
9
8
|
|
|
10
9
|
.tableWrapper {
|
|
11
10
|
position: relative;
|
|
11
|
+
flex: 1;
|
|
12
|
+
min-height: 0;
|
|
12
13
|
overflow-x: hidden;
|
|
13
|
-
overflow-y:
|
|
14
|
+
overflow-y: auto;
|
|
14
15
|
width: 100%;
|
|
15
16
|
min-width: 0;
|
|
16
17
|
max-width: 100%;
|
|
17
|
-
border-radius: 6px;
|
|
18
18
|
box-sizing: border-box;
|
|
19
|
+
background: var(--ogrid-bg, #fff);
|
|
19
20
|
}
|
|
20
21
|
.tableWrapper[data-overflow-x=true] {
|
|
21
22
|
overflow-x: auto;
|
|
@@ -27,8 +28,9 @@
|
|
|
27
28
|
.tableWidthAnchor {
|
|
28
29
|
position: relative;
|
|
29
30
|
width: max-content;
|
|
30
|
-
/*
|
|
31
|
-
|
|
31
|
+
/* min-width uses the same CSS var as the table: 100% in fill mode, max-content otherwise.
|
|
32
|
+
Safe now that StatusBar is outside this anchor. */
|
|
33
|
+
min-width: var(--data-table-min-width, max-content);
|
|
32
34
|
background: var(--ogrid-bg, #fff);
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -38,8 +40,6 @@
|
|
|
38
40
|
min-width: var(--data-table-min-width, max-content);
|
|
39
41
|
border-collapse: separate;
|
|
40
42
|
border-spacing: 0;
|
|
41
|
-
border: 1px solid var(--ogrid-border, #e0e0e0);
|
|
42
|
-
border-radius: 6px;
|
|
43
43
|
box-sizing: border-box;
|
|
44
44
|
table-layout: auto;
|
|
45
45
|
}
|
|
@@ -66,12 +66,6 @@
|
|
|
66
66
|
border-right: none;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/* When status bar follows, remove bottom border-radius so the frame connects */
|
|
70
|
-
.tableWidthAnchor:has(.statusBar) .dataTable {
|
|
71
|
-
border-bottom-left-radius: 0;
|
|
72
|
-
border-bottom-right-radius: 0;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
69
|
.dataTable thead {
|
|
76
70
|
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
77
71
|
}
|
|
@@ -154,6 +148,14 @@
|
|
|
154
148
|
border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
|
|
155
149
|
}
|
|
156
150
|
|
|
151
|
+
.groupHeaderCell {
|
|
152
|
+
text-align: center;
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
border-bottom: 2px solid var(--ogrid-border, #e0e0e0);
|
|
155
|
+
padding: 6px 10px;
|
|
156
|
+
background: var(--ogrid-header-bg, #f5f5f5);
|
|
157
|
+
}
|
|
158
|
+
|
|
157
159
|
/* Column resize handle */
|
|
158
160
|
.resizeHandle {
|
|
159
161
|
position: absolute;
|
|
@@ -301,9 +303,7 @@
|
|
|
301
303
|
font-size: 12px;
|
|
302
304
|
color: var(--ogrid-muted, #616161);
|
|
303
305
|
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
304
|
-
border: 1px solid var(--ogrid-border, #e0e0e0);
|
|
305
|
-
border-top: none;
|
|
306
|
-
border-radius: 0 0 6px 6px;
|
|
306
|
+
border-top: 1px solid var(--ogrid-border, #e0e0e0);
|
|
307
307
|
min-height: 28px;
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -387,7 +387,6 @@
|
|
|
387
387
|
background: rgba(255, 255, 255, 0.7);
|
|
388
388
|
backdrop-filter: blur(1px);
|
|
389
389
|
pointer-events: all;
|
|
390
|
-
border-radius: 6px;
|
|
391
390
|
}
|
|
392
391
|
|
|
393
392
|
.loadingOverlayContent {
|
|
@@ -1,22 +1,85 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import * as Checkbox from '@radix-ui/react-checkbox';
|
|
4
|
-
import { useInlineCellEditorState } from '@alaarab/ogrid-core';
|
|
4
|
+
import { useInlineCellEditorState, useRichSelectState } from '@alaarab/ogrid-core';
|
|
5
|
+
// Match .cellContent layout so column width doesn't shift during editing
|
|
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
|
+
};
|
|
5
27
|
export function InlineCellEditor(props) {
|
|
6
28
|
const { value, column, editorType, onCommit, onCancel } = props;
|
|
7
29
|
const wrapperRef = React.useRef(null);
|
|
8
30
|
const { localValue, setLocalValue, handleKeyDown, handleBlur, commit, cancel } = useInlineCellEditorState({ value, editorType, onCommit, onCancel });
|
|
31
|
+
const richSelectValues = column.cellEditorParams?.values ?? [];
|
|
32
|
+
const richSelectFormatValue = column.cellEditorParams?.formatValue;
|
|
33
|
+
const richSelect = useRichSelectState({
|
|
34
|
+
values: richSelectValues,
|
|
35
|
+
formatValue: richSelectFormatValue,
|
|
36
|
+
initialValue: value,
|
|
37
|
+
onCommit,
|
|
38
|
+
onCancel,
|
|
39
|
+
});
|
|
9
40
|
React.useEffect(() => {
|
|
10
41
|
const input = wrapperRef.current?.querySelector('input');
|
|
11
42
|
input?.focus();
|
|
12
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
|
+
}
|
|
13
63
|
if (editorType === 'checkbox') {
|
|
14
64
|
const checked = value === true;
|
|
15
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" }) }));
|
|
16
66
|
}
|
|
17
67
|
if (editorType === 'select') {
|
|
18
68
|
const values = column.cellEditorParams?.values ?? [];
|
|
19
|
-
return (_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,
|
|
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 }) }));
|
|
20
83
|
}
|
|
21
|
-
return (_jsx("div", { ref: wrapperRef, children: _jsx("input", { type: "text", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style:
|
|
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 }) }));
|
|
22
85
|
}
|
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, page, pageSize, displayTotalCount, setPage, setPageSize, columnChooserColumns, visibleColumns, handleVisibilityChange, title, toolbar, className, entityLabelPlural, } = useOGrid(props, ref);
|
|
10
|
-
return (_jsx(OGridLayout, { className: className, gap:
|
|
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) => {
|
|
11
11
|
setPageSize(size);
|
|
12
12
|
setPage(1);
|
|
13
|
-
}, entityLabelPlural: entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
|
|
13
|
+
}, pageSizeOptions: pageSizeOptions, entityLabelPlural: entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
|
|
14
14
|
});
|
|
15
15
|
OGridInner.displayName = 'OGrid';
|
|
16
16
|
export const OGrid = React.memo(OGridInner);
|
|
@@ -16,9 +16,9 @@ function ChevronDoubleRight() {
|
|
|
16
16
|
return _jsx("span", { "aria-hidden": true, children: "\u00BB" });
|
|
17
17
|
}
|
|
18
18
|
export const PaginationControls = React.memo((props) => {
|
|
19
|
-
const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, entityLabelPlural, className } = props;
|
|
19
|
+
const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions, entityLabelPlural, className } = props;
|
|
20
20
|
const labelPlural = entityLabelPlural ?? 'items';
|
|
21
|
-
const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount), [currentPage, pageSize, totalCount]);
|
|
21
|
+
const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined), [currentPage, pageSize, totalCount, pageSizeOptions]);
|
|
22
22
|
const handlePageSizeChange = useCallback((e) => {
|
|
23
23
|
onPageSizeChange(Number(e.target.value));
|
|
24
24
|
}, [onPageSizeChange]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type { UserLike, ColumnFilterType } from '@alaarab/ogrid-core';
|
|
2
|
+
import type { UserLike, ColumnFilterType, IDateFilterValue } from '@alaarab/ogrid-core';
|
|
3
3
|
export interface IColumnHeaderFilterProps {
|
|
4
4
|
columnKey: string;
|
|
5
5
|
columnName: string;
|
|
@@ -16,5 +16,7 @@ export interface IColumnHeaderFilterProps {
|
|
|
16
16
|
selectedUser?: UserLike;
|
|
17
17
|
onUserChange?: (user: UserLike | undefined) => void;
|
|
18
18
|
peopleSearch?: (query: string) => Promise<UserLike[]>;
|
|
19
|
+
dateValue?: IDateFilterValue;
|
|
20
|
+
onDateChange?: (value: IDateFilterValue | undefined) => void;
|
|
19
21
|
}
|
|
20
22
|
export declare const ColumnHeaderFilter: React.FC<IColumnHeaderFilterProps>;
|
|
@@ -5,7 +5,7 @@ export interface InlineCellEditorProps<T> {
|
|
|
5
5
|
item: T;
|
|
6
6
|
column: IColumnDef<T>;
|
|
7
7
|
rowIndex: number;
|
|
8
|
-
editorType: 'text' | 'select' | 'checkbox';
|
|
8
|
+
editorType: 'text' | 'select' | 'checkbox' | 'richSelect' | 'date';
|
|
9
9
|
onCommit: (value: unknown) => void;
|
|
10
10
|
onCancel: () => void;
|
|
11
11
|
}
|
|
@@ -4,5 +4,12 @@ export interface StatusBarProps {
|
|
|
4
4
|
filteredCount?: number;
|
|
5
5
|
selectedCount?: number;
|
|
6
6
|
selectedCellCount?: number;
|
|
7
|
+
aggregation?: {
|
|
8
|
+
sum: number;
|
|
9
|
+
avg: number;
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
count: number;
|
|
13
|
+
} | null;
|
|
7
14
|
}
|
|
8
15
|
export declare function StatusBar(props: StatusBarProps): React.ReactElement;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -55,8 +55,7 @@
|
|
|
55
55
|
"identity-obj-proxy": "^3.0.0",
|
|
56
56
|
"ts-jest": "^29.2.5",
|
|
57
57
|
"sass": "^1.83.4",
|
|
58
|
-
"@storybook/react": "
|
|
59
|
-
"@storybook/react-vite": "^8.5.3",
|
|
58
|
+
"@storybook/react-vite": "10.2.8",
|
|
60
59
|
"@types/react": "^18.3.18",
|
|
61
60
|
"@types/react-dom": "^18.3.5",
|
|
62
61
|
"jest": "^29.7.0",
|
|
@@ -64,9 +63,10 @@
|
|
|
64
63
|
"react": "^18.3.1",
|
|
65
64
|
"react-dom": "^18.3.1",
|
|
66
65
|
"rimraf": "^6.0.1",
|
|
67
|
-
"storybook": "
|
|
66
|
+
"storybook": "10.2.8",
|
|
68
67
|
"typescript": "^5.7.3",
|
|
69
|
-
"vite": "^
|
|
68
|
+
"vite": "^7.0.0",
|
|
69
|
+
"eslint-plugin-storybook": "10.2.8"
|
|
70
70
|
},
|
|
71
71
|
"publishConfig": {
|
|
72
72
|
"access": "public"
|