@alaarab/ogrid-react-fluent 2.0.23 → 2.1.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.
|
@@ -5,10 +5,10 @@ import { TableSettingsRegular, ChevronDownRegular, ChevronUpRegular } from '@flu
|
|
|
5
5
|
import { useColumnChooserState } from '@alaarab/ogrid-react';
|
|
6
6
|
import styles from './ColumnChooser.module.css';
|
|
7
7
|
export const ColumnChooser = (props) => {
|
|
8
|
-
const { columns, visibleColumns, onVisibilityChange, className } = props;
|
|
8
|
+
const { columns, visibleColumns, onVisibilityChange, onSetVisibleColumns, className } = props;
|
|
9
9
|
const buttonRef = useRef(null);
|
|
10
10
|
const dropdownRef = useRef(null);
|
|
11
|
-
const { open, handleToggle, handleClose, handleCheckboxChange: setColumnVisible, handleSelectAll, handleClearAll, visibleCount, totalCount, } = useColumnChooserState({ columns, visibleColumns, onVisibilityChange });
|
|
11
|
+
const { open, handleToggle, handleClose, handleCheckboxChange: setColumnVisible, handleSelectAll, handleClearAll, visibleCount, totalCount, } = useColumnChooserState({ columns, visibleColumns, onVisibilityChange, onSetVisibleColumns });
|
|
12
12
|
useEffect(() => {
|
|
13
13
|
if (!open)
|
|
14
14
|
return;
|
|
@@ -30,5 +30,5 @@ export const ColumnChooser = (props) => {
|
|
|
30
30
|
const handleCheckboxChange = (columnKey) => (_ev, data) => {
|
|
31
31
|
setColumnVisible(columnKey)(data.checked === true);
|
|
32
32
|
};
|
|
33
|
-
return (_jsxs("div", { className: `${styles.container} ${className || ''}`, children: [_jsxs(Button, { ref: buttonRef, appearance: "outline", icon: _jsx(TableSettingsRegular, {}), onClick: handleToggle, "aria-expanded": open, "aria-haspopup": "listbox", children: ["Column Visibility (", visibleCount, " of ", totalCount, ")", open ? _jsx(ChevronUpRegular, {}) : _jsx(ChevronDownRegular, {})] }), open && (_jsxs("div", { ref: dropdownRef, className: styles.dropdown, children: [_jsxs("div", { className: styles.header, children: ["Select Columns (", visibleCount, " of ", totalCount, ")"] }), _jsx("div", { className: styles.optionsList, children: columns.map((column) => (_jsx("div", { className: styles.optionItem, children: _jsx(Checkbox, { label: column.name, checked: visibleColumns.has(column.columnId), onChange: handleCheckboxChange(column.columnId) }) }, column.columnId))) }), _jsxs("div", { className: styles.actions, children: [_jsx(Button, { appearance: "subtle", size: "small", onClick: handleClearAll, children: "Clear All" }), _jsx(Button, { appearance: "primary", size: "small", onClick: handleSelectAll, children: "Select All" })] })] }))] }));
|
|
33
|
+
return (_jsxs("div", { className: `${styles.container} ${className || ''}`, children: [_jsxs(Button, { ref: buttonRef, appearance: "outline", icon: _jsx(TableSettingsRegular, {}), onClick: handleToggle, "aria-expanded": open, "aria-haspopup": "listbox", children: ["Column Visibility (", visibleCount, " of ", totalCount, ")", open ? _jsx(ChevronUpRegular, {}) : _jsx(ChevronDownRegular, {})] }), open && (_jsxs("div", { ref: dropdownRef, className: styles.dropdown, children: [_jsxs("div", { className: styles.header, children: ["Select Columns (", visibleCount, " of ", totalCount, ")"] }), _jsx("div", { className: styles.optionsList, children: columns.map((column) => (_jsx("div", { className: styles.optionItem, children: _jsx(Checkbox, { label: column.name, checked: visibleColumns.has(column.columnId), onChange: handleCheckboxChange(column.columnId), disabled: column.required === true }) }, column.columnId))) }), _jsxs("div", { className: styles.actions, children: [_jsx(Button, { appearance: "subtle", size: "small", onClick: handleClearAll, children: "Clear All" }), _jsx(Button, { appearance: "primary", size: "small", onClick: handleSelectAll, children: "Select All" })] })] }))] }));
|
|
34
34
|
};
|
|
@@ -11,18 +11,19 @@ export const ColumnHeaderFilter = React.memo((props) => {
|
|
|
11
11
|
const { columnName, filterType, options, isLoadingOptions = false, selectedUser, } = props;
|
|
12
12
|
const state = useColumnHeaderFilterState(getColumnHeaderFilterStateParams(props));
|
|
13
13
|
const { headerRef, popoverRef, isFilterOpen, setFilterOpen, hasActiveFilter, handlers, } = state;
|
|
14
|
+
const { handlePopoverClick, handleInputFocus, handleInputMouseDown, handleInputClick, handleInputKeyDown, } = handlers;
|
|
14
15
|
const filterBtnRef = React.useRef(null);
|
|
15
16
|
// Fluent-specific renderers that pass additional event propagation handlers
|
|
16
17
|
const fluentRenderers = React.useMemo(() => ({
|
|
17
|
-
renderMultiSelect: (p) => (_jsx(MultiSelectFilterPopover, { searchText: p.searchText, onSearchChange: p.onSearchChange, options: p.options, filteredOptions: p.filteredOptions, selected: p.selected, onOptionToggle: p.onOptionToggle, onSelectAll: p.onSelectAll, onClearSelection: p.onClearSelection, onApply: p.onApply, isLoading: p.isLoading, onPopoverClick:
|
|
18
|
-
renderText: (p) => (_jsx(TextFilterPopover, { value: p.value, onValueChange: p.onValueChange, onApply: p.onApply, onClear: p.onClear, onPopoverClick:
|
|
19
|
-
renderPeople: (p) => (_jsx(PeopleFilterPopover, { selectedUser: p.selectedUser, searchText: p.searchText, onSearchChange: p.onSearchChange, suggestions: p.suggestions, isLoading: p.isLoading, onUserSelect: p.onUserSelect, onClearUser: p.onClearUser, onPopoverClick:
|
|
20
|
-
renderDate: (p) => (_jsx("div", { onClick:
|
|
18
|
+
renderMultiSelect: (p) => (_jsx(MultiSelectFilterPopover, { searchText: p.searchText, onSearchChange: p.onSearchChange, options: p.options, filteredOptions: p.filteredOptions, selected: p.selected, onOptionToggle: p.onOptionToggle, onSelectAll: p.onSelectAll, onClearSelection: p.onClearSelection, onApply: p.onApply, isLoading: p.isLoading, onPopoverClick: handlePopoverClick, onInputFocus: handleInputFocus, onInputMouseDown: handleInputMouseDown, onInputClick: handleInputClick, onInputKeyDown: handleInputKeyDown })),
|
|
19
|
+
renderText: (p) => (_jsx(TextFilterPopover, { value: p.value, onValueChange: p.onValueChange, onApply: p.onApply, onClear: p.onClear, onPopoverClick: handlePopoverClick, onInputFocus: handleInputFocus, onInputMouseDown: handleInputMouseDown, onInputClick: handleInputClick, onInputKeyDown: handleInputKeyDown })),
|
|
20
|
+
renderPeople: (p) => (_jsx(PeopleFilterPopover, { selectedUser: p.selectedUser, searchText: p.searchText, onSearchChange: p.onSearchChange, suggestions: p.suggestions, isLoading: p.isLoading, onUserSelect: p.onUserSelect, onClearUser: p.onClearUser, onPopoverClick: handlePopoverClick, inputRef: p.inputRef })),
|
|
21
|
+
renderDate: (p) => (_jsx("div", { onClick: handlePopoverClick, children: _jsx(DateFilterContent, { tempDateFrom: p.tempDateFrom, setTempDateFrom: p.setTempDateFrom, tempDateTo: p.tempDateTo, setTempDateTo: p.setTempDateTo, onApply: p.onApply, onClear: p.onClear, classNames: {
|
|
21
22
|
popoverActions: styles.popoverActions,
|
|
22
23
|
clearButton: styles.clearButton,
|
|
23
24
|
applyButton: styles.applyButton,
|
|
24
25
|
} }) })),
|
|
25
|
-
}), [
|
|
26
|
+
}), [handlePopoverClick, handleInputFocus, handleInputMouseDown, handleInputClick, handleInputKeyDown]);
|
|
26
27
|
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 }) }), _jsx("div", { className: styles.headerActions, children: filterType !== 'none' && (_jsxs(_Fragment, { children: [_jsxs("button", { ref: filterBtnRef, type: "button", className: `${styles.filterIcon} ${hasActiveFilter ? styles.filterActive : ''} ${isFilterOpen ? styles.filterOpen : ''}`, onClick: handlers.handleFilterIconClick, "aria-label": `Filter ${columnName}`, title: `Filter ${columnName}`, children: [_jsx(FilterRegular, {}), hasActiveFilter && _jsx("span", { className: styles.filterBadge })] }), _jsx(Popover, { open: isFilterOpen, onOpenChange: (_, data) => { if (!data.open)
|
|
27
28
|
setFilterOpen(false); }, positioning: { target: filterBtnRef.current ?? undefined, position: 'below', align: 'start', offset: 4 }, trapFocus: false, children: _jsxs(PopoverSurface, { ref: popoverRef, className: styles.filterPopover, onClick: handlers.handlePopoverClick, style: { padding: 0 }, children: [_jsxs("div", { className: styles.popoverHeader, children: ["Filter: ", columnName] }), renderFilterContent(filterType, state, options ?? [], isLoadingOptions, selectedUser, fluentRenderers)] }) })] })) })] }));
|
|
28
29
|
});
|
|
@@ -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
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { Table, TableHeader, TableRow, TableHeaderCell, TableBody, TableCell, Checkbox, Popover, PopoverSurface, } from '@fluentui/react-components';
|
|
6
6
|
import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
|
|
@@ -11,7 +11,7 @@ import { GridContextMenu } from './GridContextMenu';
|
|
|
11
11
|
import { EmptyState } from './EmptyState';
|
|
12
12
|
import { LoadingOverlay } from './LoadingOverlay';
|
|
13
13
|
import { DropIndicator } from './DropIndicator';
|
|
14
|
-
import { useDataGridTableOrchestration, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary,
|
|
14
|
+
import { useDataGridTableOrchestration, useColumnMeta, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, GRID_ROOT_STYLE, CURSOR_CELL_STYLE, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, NOOP, STOP_PROPAGATION, } from '@alaarab/ogrid-react';
|
|
15
15
|
import styles from './DataGridTable.module.css';
|
|
16
16
|
// --- Memoized row component (skips re-render for rows unaffected by selection changes) ---
|
|
17
17
|
function GridRowInner(props) {
|
|
@@ -24,55 +24,21 @@ const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
|
|
|
24
24
|
function DataGridTableInner(props) {
|
|
25
25
|
const o = useDataGridTableOrchestration({ props });
|
|
26
26
|
const { wrapperRef, tableContainerRef, lastMouseShiftRef, interaction, pinning, handleResizeStart, getColumnWidth, isReorderDragging, dropIndicatorX, handleHeaderMouseDown, virtualScrollEnabled, visibleRange, items, getRowId, emptyState, rowSelection, isLoading, loadingMessage, ariaLabel, ariaLabelledBy, visibleColumns, columnOrder, columnReorder, density, rowHeight, rowNumberOffset, headerRows, allowOverflowX, fitToContent, editCallbacks, interactionHandlers, cellDescriptorInputRef, pendingEditorValueRef, popoverAnchorElRef, handleSingleRowClick, handlePasteVoid, visibleCols, totalColCount, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, columnSizingOverrides, measuredColumnWidths, selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, editingCell, setPopoverAnchorEl, cancelPopoverEdit, setActiveCell, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging, menuPosition, closeContextMenu, headerFilterInput, statusBarConfig, showEmptyInGrid, onCellError, headerMenu, } = o;
|
|
27
|
-
// Pre-compute column styles and classNames (avoids per-cell object creation
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// from shrinking when new data loads (e.g. server-side pagination).
|
|
43
|
-
const measuredW = measuredColumnWidths[col.columnId];
|
|
44
|
-
const baseMinWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
45
|
-
const effectiveMinWidth = hasResizeOverride ? columnWidth : Math.max(baseMinWidth, measuredW ?? 0);
|
|
46
|
-
cellStyles[col.columnId] = {
|
|
47
|
-
minWidth: effectiveMinWidth,
|
|
48
|
-
width: hasExplicitWidth ? columnWidth : undefined,
|
|
49
|
-
maxWidth: hasExplicitWidth ? columnWidth : undefined,
|
|
50
|
-
textAlign: col.type === 'numeric' ? 'right' : col.type === 'boolean' ? 'center' : undefined,
|
|
51
|
-
// Fluent UI's TableCell injects atomic CSS `position: relative` which overrides the
|
|
52
|
-
// shared `.pinnedColLeft { position: sticky }` class. Inline style wins over atomic CSS.
|
|
53
|
-
...(isPinned ? { position: 'sticky' } : undefined),
|
|
54
|
-
...(isPinnedLeft && pinning.leftOffsets[col.columnId] != null ? { left: pinning.leftOffsets[col.columnId] } : undefined),
|
|
55
|
-
...(isPinnedRight && pinning.rightOffsets[col.columnId] != null ? { right: pinning.rightOffsets[col.columnId] } : undefined),
|
|
56
|
-
};
|
|
57
|
-
hdrStyles[col.columnId] = {
|
|
58
|
-
minWidth: effectiveMinWidth,
|
|
59
|
-
width: hasExplicitWidth ? columnWidth : undefined,
|
|
60
|
-
maxWidth: hasExplicitWidth ? columnWidth : undefined,
|
|
61
|
-
...(isPinned ? { position: 'sticky' } : undefined),
|
|
62
|
-
...(isPinnedLeft && pinning.leftOffsets[col.columnId] != null ? { left: pinning.leftOffsets[col.columnId] } : undefined),
|
|
63
|
-
...(isPinnedRight && pinning.rightOffsets[col.columnId] != null ? { right: pinning.rightOffsets[col.columnId] } : undefined),
|
|
64
|
-
};
|
|
65
|
-
const parts = [];
|
|
66
|
-
if (isPinnedLeft)
|
|
67
|
-
parts.push(styles.pinnedColLeft);
|
|
68
|
-
if (isPinnedRight)
|
|
69
|
-
parts.push(styles.pinnedColRight);
|
|
70
|
-
const cn = parts.join(' ');
|
|
71
|
-
cellClasses[col.columnId] = cn;
|
|
72
|
-
hdrClasses[col.columnId] = cn;
|
|
73
|
-
}
|
|
74
|
-
return { cellStyles, cellClasses, hdrStyles, hdrClasses };
|
|
75
|
-
}, [visibleCols, getColumnWidth, columnSizingOverrides, measuredColumnWidths, pinning.pinnedColumns, pinning.leftOffsets, pinning.rightOffsets]);
|
|
27
|
+
// Pre-compute column styles and classNames via shared hook (avoids per-cell object creation).
|
|
28
|
+
// addStickyPosition=true: Fluent UI's TableCell injects atomic `position: relative` via CSS-in-JS,
|
|
29
|
+
// overriding the shared `.pinnedColLeft { position: sticky }` class. Inline style wins over atomic CSS.
|
|
30
|
+
const columnMeta = useColumnMeta({
|
|
31
|
+
visibleCols,
|
|
32
|
+
getColumnWidth,
|
|
33
|
+
columnSizingOverrides,
|
|
34
|
+
measuredColumnWidths,
|
|
35
|
+
pinnedColumns: pinning.pinnedColumns,
|
|
36
|
+
leftOffsets: pinning.leftOffsets,
|
|
37
|
+
rightOffsets: pinning.rightOffsets,
|
|
38
|
+
pinnedColLeftClass: styles.pinnedColLeft,
|
|
39
|
+
pinnedColRightClass: styles.pinnedColRight,
|
|
40
|
+
addStickyPosition: true,
|
|
41
|
+
});
|
|
76
42
|
// renderCellContent reads volatile state from refs -- keeps function identity stable so
|
|
77
43
|
// GridRow's React.memo comparator can skip rows whose selection state hasn't changed.
|
|
78
44
|
const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
|
|
@@ -98,9 +64,7 @@ function DataGridTableInner(props) {
|
|
|
98
64
|
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" }))] }));
|
|
99
65
|
}
|
|
100
66
|
return (_jsx(CellErrorBoundary, { onError: onCellError, children: content }, `${rowId}-${col.columnId}`));
|
|
101
|
-
},
|
|
102
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
|
|
103
|
-
[editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
|
|
67
|
+
}, [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError, cellDescriptorInputRef, pendingEditorValueRef, popoverAnchorElRef]);
|
|
104
68
|
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 : ''} ${styles[`density-${density}`] || ''}`, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, "data-empty": showEmptyInGrid ? 'true' : undefined, "data-loading": isLoading && items.length === 0 ? 'true' : undefined, "data-column-count": totalColCount, "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: {
|
|
105
69
|
['--data-table-column-count']: totalColCount,
|
|
106
70
|
['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
|
|
@@ -112,6 +76,8 @@ function DataGridTableInner(props) {
|
|
|
112
76
|
return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
|
|
113
77
|
}
|
|
114
78
|
// Leaf cell
|
|
79
|
+
if (!cell.columnDef)
|
|
80
|
+
return null;
|
|
115
81
|
const col = cell.columnDef;
|
|
116
82
|
// Determine aria-sort value for sorted columns
|
|
117
83
|
const isSorted = props.sortBy === col.columnId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-react-fluent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "OGrid React Fluent implementation – DataGrid-powered data table with sorting, filtering, pagination, column chooser, and CSV export.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"types": "./dist/types/index.d.ts",
|
|
11
11
|
"import": "./dist/esm/index.js",
|
|
12
12
|
"require": "./dist/esm/index.js"
|
|
13
|
-
}
|
|
13
|
+
},
|
|
14
|
+
"./styles/*": "./dist/esm/*"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
16
17
|
"build": "rimraf dist && tsc -p tsconfig.build.json && node scripts/compile-styles.js",
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"node": ">=18"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@alaarab/ogrid-react": "2.
|
|
44
|
+
"@alaarab/ogrid-react": "2.1.1"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
47
|
"@fluentui/react-components": "^9.0.0",
|
|
@@ -67,5 +68,12 @@
|
|
|
67
68
|
},
|
|
68
69
|
"publishConfig": {
|
|
69
70
|
"access": "public"
|
|
70
|
-
}
|
|
71
|
+
},
|
|
72
|
+
"repository": {
|
|
73
|
+
"type": "git",
|
|
74
|
+
"url": "https://github.com/alaarab/ogrid.git",
|
|
75
|
+
"directory": "packages/react-fluent"
|
|
76
|
+
},
|
|
77
|
+
"homepage": "https://ogrid.dev",
|
|
78
|
+
"bugs": "https://github.com/alaarab/ogrid/issues"
|
|
71
79
|
}
|