@alaarab/ogrid-fluent 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.
@@ -8,7 +8,7 @@ import { MultiSelectFilterPopover } from './MultiSelectFilterPopover';
8
8
  import { PeopleFilterPopover } from './PeopleFilterPopover';
9
9
  import styles from './ColumnHeaderFilter.module.css';
10
10
  export const ColumnHeaderFilter = React.memo((props) => {
11
- const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options, isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, } = props;
11
+ const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options, isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, dateValue, onDateChange, } = props;
12
12
  const state = useColumnHeaderFilterState({
13
13
  filterType,
14
14
  isSorted,
@@ -23,6 +23,8 @@ export const ColumnHeaderFilter = React.memo((props) => {
23
23
  selectedUser,
24
24
  onUserChange,
25
25
  peopleSearch,
26
+ dateValue,
27
+ onDateChange,
26
28
  });
27
29
  const { headerRef, popoverRef, peopleInputRef, isFilterOpen, tempSelected, setTempTextValue, searchText, setSearchText, filteredOptions, peopleSuggestions, isPeopleLoading, peopleSearchText, setPeopleSearchText, hasActiveFilter, popoverPosition, handlers, } = state;
28
30
  const renderPopoverContent = () => {
@@ -35,6 +37,9 @@ export const ColumnHeaderFilter = React.memo((props) => {
35
37
  if (filterType === 'people') {
36
38
  return (_jsx(PeopleFilterPopover, { selectedUser: selectedUser, searchText: peopleSearchText, onSearchChange: setPeopleSearchText, suggestions: peopleSuggestions, isLoading: isPeopleLoading, onUserSelect: handlers.handleUserSelect, onClearUser: handlers.handleClearUser, onPopoverClick: handlers.handlePopoverClick, inputRef: peopleInputRef }));
37
39
  }
40
+ if (filterType === 'date') {
41
+ return (_jsxs("div", { onClick: handlers.handlePopoverClick, 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" })] })] }));
42
+ }
38
43
  return null;
39
44
  };
40
45
  return (_jsxs("div", { className: styles.columnHeader, ref: headerRef, children: [_jsx("div", { className: styles.headerContent, children: _jsx(Tooltip, { content: columnName, relationship: "label", withArrow: true, children: _jsx("span", { className: styles.columnNameTooltipTrigger, children: _jsx("span", { className: styles.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: isSorted ? (isSortedDescending ? _jsx(ArrowDownRegular, {}) : _jsx(ArrowUpRegular, {})) : (_jsx(ArrowSortRegular, {})) })), filterType !== 'none' && (_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(FilterRegular, {}), hasActiveFilter && _jsx("span", { className: styles.filterBadge })] }))] }), isFilterOpen && filterType !== 'none' && (_jsxs("div", { className: styles.filterPopover, ref: popoverRef, onClick: handlers.handlePopoverClick, style: popoverPosition
@@ -7,14 +7,16 @@ import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
7
7
  import { InlineCellEditor } from './InlineCellEditor';
8
8
  import { StatusBar } from './StatusBar';
9
9
  import { GridContextMenu } from './GridContextMenu';
10
- import { useDataGridState, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, } from '@alaarab/ogrid-core';
10
+ import { useDataGridState, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from '@alaarab/ogrid-core';
11
11
  import styles from './DataGridTable.module.css';
12
12
  function DataGridTableInner(props) {
13
13
  const wrapperRef = useRef(null);
14
14
  const tableContainerRef = useRef(null);
15
15
  const state = useDataGridState({ props, wrapperRef });
16
- const { flatColumns, visibleCols, totalColCount, hasCheckboxCol, rowIndexByRowId, selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, setEditingCell, pendingEditorValue, setPendingEditorValue, activeCell, setActiveCell, handleCellMouseDown, handleSelectAllCells, contextMenu, setContextMenu, handleCellContextMenu, closeContextMenu, canUndo, canRedo, onUndo, onRedo, handleCopy, handleCut, handlePaste, handleGridKeyDown, handleFillHandleMouseDown, containerWidth, minTableWidth, columnSizingOverrides, setColumnSizingOverrides, statusBarConfig, showEmptyInGrid, hasCellSelection, selectionRange, copyRange, cutRange, colOffset, headerFilterInput, cellDescriptorInput, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl, } = state;
17
- const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
16
+ const { flatColumns, visibleCols, totalColCount, hasCheckboxCol, rowIndexByRowId, selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, setEditingCell, pendingEditorValue, setPendingEditorValue, activeCell, 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;
17
+ const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
18
+ const headerRows = buildHeaderRows(props.columns, props.visibleColumns);
19
+ const hasGroupHeaders = headerRows.length > 1;
18
20
  const fitToContent = layoutMode === 'content';
19
21
  const columnSizingOptions = useMemo(() => {
20
22
  const acc = {};
@@ -35,16 +37,7 @@ function DataGridTableInner(props) {
35
37
  });
36
38
  return acc;
37
39
  }, [visibleCols, columnSizingOverrides, hasCheckboxCol]);
38
- const desiredTableWidth = useMemo(() => {
39
- const PADDING = 16;
40
- const checkboxW = hasCheckboxCol ? 48 : 0;
41
- return visibleCols.reduce((sum, c) => {
42
- const s = columnSizingOptions[c.columnId];
43
- const w = s?.idealWidth ?? s?.defaultWidth ?? c.idealWidth ?? c.defaultWidth ?? c.minWidth ?? 80;
44
- return sum + Math.max(c.minWidth ?? 80, w) + PADDING;
45
- }, checkboxW);
46
- }, [visibleCols, columnSizingOptions, hasCheckboxCol]);
47
- const allowOverflowX = containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
40
+ const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
48
41
  const fluentColumns = useMemo(() => {
49
42
  const dataCols = visibleCols.map((col, colIdx) => createTableColumn({
50
43
  columnId: col.columnId,
@@ -55,47 +48,18 @@ function DataGridTableInner(props) {
55
48
  const rowIndex = rowIndexByRowId.get(rowId) ?? -1;
56
49
  const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
57
50
  if (descriptor.mode === 'editing-inline') {
58
- return (_jsx(InlineCellEditor, { value: descriptor.value, item: item, column: col, rowIndex: descriptor.rowIndex, editorType: descriptor.editorType ?? 'text', onCommit: (newValue) => commitCellEdit(item, col.columnId, descriptor.value, newValue, descriptor.rowIndex, descriptor.globalColIndex), onCancel: () => setEditingCell(null) }));
51
+ return _jsx(InlineCellEditor, { ...buildInlineEditorProps(item, col, descriptor, { commitCellEdit, setEditingCell }) });
59
52
  }
60
53
  if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
61
- const oldValue = descriptor.value;
62
- const displayValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
54
+ const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValue, { setPendingEditorValue, commitCellEdit, cancelPopoverEdit });
63
55
  const CustomEditor = col.cellEditor;
64
- const editorProps = {
65
- value: displayValue,
66
- onValueChange: setPendingEditorValue,
67
- onCommit: () => {
68
- const newValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
69
- commitCellEdit(item, col.columnId, oldValue, newValue, descriptor.rowIndex, descriptor.globalColIndex);
70
- },
71
- onCancel: cancelPopoverEdit,
72
- item,
73
- column: col,
74
- cellEditorParams: col.cellEditorParams,
75
- };
76
56
  return (_jsxs(_Fragment, { children: [_jsx("div", { ref: (el) => { if (el)
77
57
  setPopoverAnchorEl(el); }, style: { minHeight: '100%', minWidth: 40 }, "aria-hidden": true }), _jsx(Popover, { open: !!popoverAnchorEl, onOpenChange: (_, data) => { if (!data.open)
78
58
  cancelPopoverEdit(); }, positioning: { target: popoverAnchorEl ?? undefined }, children: _jsx(PopoverSurface, { children: _jsx(CustomEditor, { ...editorProps }) }) })] }));
79
59
  }
80
- const cellStyle = col.cellStyle
81
- ? typeof col.cellStyle === 'function'
82
- ? col.cellStyle(item)
83
- : col.cellStyle
84
- : undefined;
85
- let content;
86
- if (col.renderCell)
87
- content = col.renderCell(item);
88
- else {
89
- const value = descriptor.displayValue;
90
- if (col.valueFormatter)
91
- content = col.valueFormatter(value, item);
92
- else if (value !== null && value !== undefined)
93
- content = String(value);
94
- else
95
- content = null;
96
- }
97
- if (cellStyle)
98
- content = _jsx("span", { style: cellStyle, children: content });
60
+ const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
61
+ const cellStyle = resolveCellStyle(col, item);
62
+ const styledContent = cellStyle ? _jsx("span", { style: cellStyle, children: content }) : content;
99
63
  const cellClassNames = [
100
64
  styles.cellContent,
101
65
  descriptor.isActive && !descriptor.isInRange ? styles.activeCellContent : '',
@@ -105,10 +69,13 @@ function DataGridTableInner(props) {
105
69
  ]
106
70
  .filter(Boolean)
107
71
  .join(' ');
108
- if (descriptor.canEditAny) {
109
- return (_jsxs("div", { className: cellClassNames, "data-row-index": descriptor.rowIndex, "data-col-index": descriptor.globalColIndex, ...(descriptor.isInRange ? { 'data-in-range': 'true' } : {}), role: "button", tabIndex: descriptor.isActive ? 0 : -1, onMouseDown: (e) => handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex), onClick: () => setActiveCell({ rowIndex: descriptor.rowIndex, columnIndex: descriptor.globalColIndex }), onDoubleClick: () => setEditingCell({ rowId: descriptor.rowId, columnId: col.columnId }), onContextMenu: handleCellContextMenu, style: { cursor: 'cell' }, children: [content, descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
110
- }
111
- return (_jsx("div", { className: cellClassNames, "data-row-index": descriptor.rowIndex, "data-col-index": descriptor.globalColIndex, ...(descriptor.isInRange ? { 'data-in-range': 'true' } : {}), 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 }));
72
+ const colType = col.type;
73
+ const interactionProps = getCellInteractionProps(descriptor, col.columnId, { handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu });
74
+ return (_jsxs("div", { className: cellClassNames, ...interactionProps, style: {
75
+ ...(descriptor.canEditAny ? { cursor: 'cell' } : undefined),
76
+ ...(colType === 'numeric' ? { justifyContent: 'flex-end', textAlign: 'right' } : undefined),
77
+ ...(colType === 'boolean' ? { justifyContent: 'center', textAlign: 'center' } : undefined),
78
+ }, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
112
79
  },
113
80
  }));
114
81
  if (hasCheckboxCol) {
@@ -217,48 +184,53 @@ function DataGridTableInner(props) {
217
184
  : fitToContent
218
185
  ? 'max-content'
219
186
  : '100%',
220
- }, onKeyDown: handleGridKeyDown, children: [isLoading && items.length > 0 && (_jsx("div", { className: styles.loadingOverlay, "aria-live": "polite", children: _jsxs("div", { className: styles.loadingOverlayContent, children: [_jsx(Spinner, { size: "small" }), _jsx("span", { className: styles.loadingOverlayText, children: loadingMessage })] }) })), _jsx("div", { className: styles.tableScrollContent, children: _jsx("div", { className: isLoading && items.length > 0 ? styles.loadingDimmed : undefined, children: _jsxs("div", { className: styles.tableWidthAnchor, ref: tableContainerRef, children: [_jsxs(DataGrid, { items: items, columns: fluentColumns, resizableColumns: true, resizableColumnsOptions: { autoFitColumns: layoutMode === 'fill' && !allowOverflowX }, columnSizingOptions: columnSizingOptions, onColumnResize: handleColumnResize, getRowId: (item) => String(getRowId(item)), focusMode: "composite", className: styles.dataGrid, children: [_jsx(DataGridHeader, { className: freezeRows != null && freezeRows >= 1 ? styles.stickyHeader : undefined, children: _jsx(DataGridRow, { children: ({ renderHeaderCell, columnId }) => {
221
- const colIdx = visibleCols.findIndex((c) => c.columnId === columnId);
222
- const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx >= 0 && colIdx < freezeCols;
223
- const col = colIdx >= 0 ? visibleCols[colIdx] : undefined;
224
- const isPinnedLeft = col?.pinned === 'left';
225
- const isPinnedRight = col?.pinned === 'right';
226
- return (_jsx(DataGridHeaderCell, { className: [
227
- columnId === '__selection__' ? styles.selectionHeaderCellWrapper : '',
228
- isFreezeCol ? styles.freezeCol : '',
229
- isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
230
- isPinnedLeft ? styles.pinnedCell : '',
231
- isPinnedLeft ? styles.pinnedLeft : '',
232
- isPinnedRight ? styles.pinnedCell : '',
233
- isPinnedRight ? styles.pinnedRight : '',
234
- ].filter(Boolean).join(' '), children: renderHeaderCell() }));
235
- } }) }), _jsx(DataGridBody, { children: ({ item }) => {
236
- const rowId = getRowId(item);
237
- const isSelected = selectedRowIds.has(rowId);
238
- return (_jsx(DataGridRow, { className: `${isSelected ? styles.selectedRow : ''} ${activeCell !== null && (rowIndexByRowId.get(rowId) ?? -1) === activeCell.rowIndex
239
- ? styles.activeRow
240
- : ''}`, onClick: () => {
241
- if (rowSelection === 'single') {
242
- const isCurrentlySelected = selectedRowIds.has(rowId);
243
- updateSelection(isCurrentlySelected ? new Set() : new Set([rowId]));
244
- }
245
- }, children: ({ renderCell, columnId }) => {
246
- const colIdx = visibleCols.findIndex((c) => c.columnId === columnId);
247
- const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx >= 0 && colIdx < freezeCols;
248
- const col = colIdx >= 0 ? visibleCols[colIdx] : undefined;
249
- const isPinnedLeft = col?.pinned === 'left';
250
- const isPinnedRight = col?.pinned === 'right';
251
- return (_jsx(DataGridCell, { className: [
252
- columnId === '__selection__' ? styles.selectionCellWrapper : '',
253
- isFreezeCol ? styles.freezeCol : '',
254
- isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
255
- isPinnedLeft ? styles.pinnedCell : '',
256
- isPinnedLeft ? styles.pinnedLeft : '',
257
- isPinnedRight ? styles.pinnedCell : '',
258
- isPinnedRight ? styles.pinnedRight : '',
259
- ].filter(Boolean).join(' '), children: renderCell(item) }));
260
- } }, rowId));
261
- } })] }), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), 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 })), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { className: styles.emptyStateInGridMessageSticky, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.emptyStateInGridIcon, "aria-hidden": true, children: "\uD83D\uDCCB" }), _jsx("div", { className: styles.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: styles.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: styles.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }))] }) }) }), contextMenu &&
187
+ }, onKeyDown: handleGridKeyDown, children: [isLoading && items.length > 0 && (_jsx("div", { className: styles.loadingOverlay, "aria-live": "polite", children: _jsxs("div", { className: styles.loadingOverlayContent, children: [_jsx(Spinner, { size: "small" }), _jsx("span", { className: styles.loadingOverlayText, children: loadingMessage })] }) })), _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(DataGrid, { items: items, columns: fluentColumns, resizableColumns: true, resizableColumnsOptions: { autoFitColumns: layoutMode === 'fill' && !allowOverflowX }, columnSizingOptions: columnSizingOptions, onColumnResize: handleColumnResize, getRowId: (item) => String(getRowId(item)), focusMode: "composite", className: styles.dataGrid, children: [_jsxs(DataGridHeader, { className: styles.stickyHeader, children: [hasGroupHeaders && headerRows.slice(0, -1).map((row, rowIdx) => (_jsxs("tr", { className: styles.groupHeaderRow, children: [rowIdx === 0 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1, style: { width: 48, minWidth: 48 } })), row.map((cell, cellIdx) => {
188
+ if (cell.isGroup) {
189
+ return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
190
+ }
191
+ return (_jsx("th", { rowSpan: headerRows.length - rowIdx, className: styles.leafHeaderCellSpan, scope: "col", children: cell.columnDef?.name }, cellIdx));
192
+ })] }, `group-${rowIdx}`))), _jsx(DataGridRow, { children: ({ renderHeaderCell, columnId }) => {
193
+ const colIdx = visibleCols.findIndex((c) => c.columnId === columnId);
194
+ const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx >= 0 && colIdx < freezeCols;
195
+ const col = colIdx >= 0 ? visibleCols[colIdx] : undefined;
196
+ const isPinnedLeft = col?.pinned === 'left';
197
+ const isPinnedRight = col?.pinned === 'right';
198
+ return (_jsx(DataGridHeaderCell, { className: [
199
+ columnId === '__selection__' ? styles.selectionHeaderCellWrapper : '',
200
+ isFreezeCol ? styles.freezeCol : '',
201
+ isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
202
+ isPinnedLeft ? styles.pinnedCell : '',
203
+ isPinnedLeft ? styles.pinnedLeft : '',
204
+ isPinnedRight ? styles.pinnedCell : '',
205
+ isPinnedRight ? styles.pinnedRight : '',
206
+ ].filter(Boolean).join(' '), children: renderHeaderCell() }));
207
+ } })] }), _jsx(DataGridBody, { children: ({ item }) => {
208
+ const rowId = getRowId(item);
209
+ const isSelected = selectedRowIds.has(rowId);
210
+ return (_jsx(DataGridRow, { className: `${isSelected ? styles.selectedRow : ''} ${activeCell !== null && (rowIndexByRowId.get(rowId) ?? -1) === activeCell.rowIndex
211
+ ? styles.activeRow
212
+ : ''}`, onClick: () => {
213
+ if (rowSelection === 'single') {
214
+ const isCurrentlySelected = selectedRowIds.has(rowId);
215
+ updateSelection(isCurrentlySelected ? new Set() : new Set([rowId]));
216
+ }
217
+ }, children: ({ renderCell, columnId }) => {
218
+ const colIdx = visibleCols.findIndex((c) => c.columnId === columnId);
219
+ const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx >= 0 && colIdx < freezeCols;
220
+ const col = colIdx >= 0 ? visibleCols[colIdx] : undefined;
221
+ const isPinnedLeft = col?.pinned === 'left';
222
+ const isPinnedRight = col?.pinned === 'right';
223
+ return (_jsx(DataGridCell, { className: [
224
+ columnId === '__selection__' ? styles.selectionCellWrapper : '',
225
+ isFreezeCol ? styles.freezeCol : '',
226
+ isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
227
+ isPinnedLeft ? styles.pinnedCell : '',
228
+ isPinnedLeft ? styles.pinnedLeft : '',
229
+ isPinnedRight ? styles.pinnedCell : '',
230
+ isPinnedRight ? styles.pinnedRight : '',
231
+ ].filter(Boolean).join(' '), children: renderCell(item) }));
232
+ } }, rowId));
233
+ } })] }), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { className: styles.emptyStateInGridMessageSticky, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.emptyStateInGridIcon, "aria-hidden": true, children: "\uD83D\uDCCB" }), _jsx("div", { className: styles.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: styles.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: styles.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }))] }) }), 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 &&
262
234
  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)] }));
263
235
  }
264
236
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -12,8 +12,6 @@
12
12
  position: relative;
13
13
  /* When table fits (data-auto-fit): fill 100% so last column gets space. When overflow: size to content for scroll. */
14
14
  width: max-content;
15
- /* No min-width: 100% — anchor sizes to grid content so status bar aligns with the DataGrid border.
16
- .tableScrollContent provides the full-width background color so no gap is visible. */
17
15
  background-color: var(--colorNeutralBackground1, #ffffff);
18
16
  }
19
17
 
@@ -25,17 +23,19 @@
25
23
  /* Always use full container width (matches pagination); columns fill the space */
26
24
  .tableWrapper {
27
25
  position: relative;
26
+ flex: 1;
27
+ min-height: 0;
28
28
  /* Default: no horizontal scroll unless we explicitly allow overflow (wide tables). */
29
29
  overflow-x: hidden;
30
- overflow-y: visible;
30
+ overflow-y: auto;
31
31
  width: 100%;
32
32
  min-width: 0;
33
33
  max-width: 100%;
34
- border-radius: var(--borderRadiusMedium, 4px);
35
34
  box-sizing: border-box;
36
35
  /* Border is applied to the grid itself so we don't draw an empty bordered area
37
36
  when the grid content is narrower than the container. */
38
37
  border: none;
38
+ background-color: var(--colorNeutralBackground1, #ffffff);
39
39
  -webkit-overflow-scrolling: touch;
40
40
  /* Wide tables: allow horizontal scroll */
41
41
  }
@@ -63,9 +63,6 @@
63
63
  max-width: 100% !important;
64
64
  min-width: var(--data-table-min-width, max-content) !important;
65
65
  box-sizing: border-box !important;
66
- /* Visual container border belongs to the grid (not the full-width wrapper). */
67
- border: 1px solid var(--colorNeutralStroke2, #e0e0e0) !important;
68
- border-radius: var(--borderRadiusMedium, 4px) !important;
69
66
  overflow: hidden;
70
67
  }
71
68
  .tableWrapper[data-column-count] :global {
@@ -216,6 +213,23 @@
216
213
  min-width: 0;
217
214
  }
218
215
 
216
+ .groupHeaderRow th {
217
+ background: var(--ogrid-header-bg, #f5f5f5);
218
+ }
219
+
220
+ .groupHeaderCell {
221
+ text-align: center;
222
+ font-weight: 600;
223
+ border-bottom: 2px solid var(--ogrid-border, #e0e0e0);
224
+ padding: 6px 10px;
225
+ }
226
+
227
+ .leafHeaderCellSpan {
228
+ font-weight: 600;
229
+ padding: 6px 10px;
230
+ background: var(--ogrid-header-bg, #f5f5f5);
231
+ }
232
+
219
233
  .selectionHeaderCellWrapper {
220
234
  width: 48px !important;
221
235
  min-width: 48px !important;
@@ -380,9 +394,7 @@
380
394
  font-size: 12px;
381
395
  color: var(--colorNeutralForeground2, #616161);
382
396
  background-color: var(--colorSubtleBackgroundSelected, #f3f2f1);
383
- border: 1px solid var(--colorNeutralStroke2, #e0e0e0);
384
- border-top: none;
385
- border-radius: 0 0 var(--borderRadiusMedium, 4px) var(--borderRadiusMedium, 4px);
397
+ border-top: 1px solid var(--colorNeutralStroke2, #e0e0e0);
386
398
  min-height: 28px;
387
399
  user-select: none;
388
400
  }
@@ -470,7 +482,6 @@
470
482
  background: rgba(255, 255, 255, 0.7);
471
483
  backdrop-filter: blur(1px);
472
484
  pointer-events: all;
473
- border-radius: var(--borderRadiusMedium, 4px);
474
485
  }
475
486
 
476
487
  .loadingOverlayContent {
@@ -1,22 +1,75 @@
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
- import { Input, Select, Checkbox } from '@fluentui/react-components';
4
- import { useInlineCellEditorState } from '@alaarab/ogrid-core';
3
+ import { Select, Checkbox } from '@fluentui/react-components';
4
+ import { useInlineCellEditorState, useRichSelectState } from '@alaarab/ogrid-core';
5
+ // Match cell content 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, { checked: checked, onChange: (_, data) => commit(data.checked), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), cancel()) }));
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: (_, data) => commit(data.value), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), cancel()), children: values.map((v) => (_jsx("option", { value: String(v), children: String(v) }, String(v)))) }));
69
+ return (_jsx("div", { style: editorWrapperStyle, children: _jsx(Select, { value: value !== null && value !== undefined ? String(value) : '', onChange: (_, data) => commit(data.value), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), cancel()), children: values.map((v) => (_jsx("option", { value: String(v), children: String(v) }, String(v)))) }) }));
70
+ }
71
+ if (editorType === 'date') {
72
+ 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
73
  }
21
- return (_jsx("div", { ref: wrapperRef, children: _jsx(Input, { value: localValue, onChange: (_, data) => setLocalValue(data.value), onBlur: handleBlur, onKeyDown: handleKeyDown, size: "small", style: { minWidth: 60 } }) }));
74
+ 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
75
  }
@@ -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: 16, title: title, toolbar: toolbar, columnChooser: _jsx(ColumnChooser, { columns: columnChooserColumns, visibleColumns: visibleColumns, onVisibilityChange: handleVisibilityChange }), pagination: _jsx(PaginationControls, { currentPage: page, pageSize: pageSize, totalCount: displayTotalCount, onPageChange: setPage, onPageSizeChange: (size) => {
9
+ const { dataGridProps, page, pageSize, displayTotalCount, setPage, setPageSize, columnChooserColumns, visibleColumns, handleVisibilityChange, title, toolbar, className, entityLabelPlural, pageSizeOptions, sideBarProps, columnChooserPlacement, } = 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);
@@ -6,9 +6,9 @@ import { ChevronLeftRegular, ChevronRightRegular, ChevronDoubleLeftRegular, Chev
6
6
  import { getPaginationViewModel } from '@alaarab/ogrid-core';
7
7
  import styles from './PaginationControls.module.css';
8
8
  export const PaginationControls = React.memo((props) => {
9
- const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, entityLabelPlural, className } = props;
9
+ const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions, entityLabelPlural, className } = props;
10
10
  const labelPlural = entityLabelPlural ?? 'items';
11
- const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount), [currentPage, pageSize, totalCount]);
11
+ const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined), [currentPage, pageSize, totalCount, pageSizeOptions]);
12
12
  const handlePageSizeChange = useCallback((_e, data) => {
13
13
  onPageSizeChange(Number(data.value));
14
14
  }, [onPageSizeChange]);
@@ -7,9 +7,7 @@
7
7
  width: 100%;
8
8
  min-width: 0;
9
9
  box-sizing: border-box;
10
- padding: 14px 0;
11
- margin-top: 16px;
12
- border-top: 1px solid var(--colorNeutralStroke2, #e5e5e5);
10
+ padding: 0;
13
11
  }
14
12
 
15
13
  .paginationInfo {
@@ -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;
@@ -5,6 +5,7 @@ export interface IPaginationControlsProps {
5
5
  totalCount: number;
6
6
  onPageChange: (page: number) => void;
7
7
  onPageSizeChange: (pageSize: number) => void;
8
+ pageSizeOptions?: number[];
8
9
  entityLabelPlural?: string;
9
10
  className?: string;
10
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-fluent",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "OGrid Fluent UI implementation – DataGrid-powered data table with sorting, filtering, pagination, column chooser, and CSV export.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -51,12 +51,12 @@
51
51
  "devDependencies": {
52
52
  "@fluentui/react-components": "^9.72.10",
53
53
  "@fluentui/react-icons": "^2.0.317",
54
- "@storybook/react": "^8.5.3",
55
- "@storybook/react-vite": "^8.5.3",
54
+ "@storybook/react-vite": "10.2.8",
56
55
  "sass": "^1.83.4",
57
- "scheduler": "^0.25.0",
58
- "storybook": "^8.5.3",
59
- "vite": "^6.1.0"
56
+ "scheduler": "^0.27.0",
57
+ "storybook": "10.2.8",
58
+ "eslint-plugin-storybook": "10.2.8",
59
+ "vite": "^7.0.0"
60
60
  },
61
61
  "publishConfig": {
62
62
  "access": "public"