@alaarab/ogrid-react-fluent 2.0.0-beta

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.
Files changed (34) hide show
  1. package/README.md +85 -0
  2. package/dist/esm/ColumnChooser/ColumnChooser.js +152 -0
  3. package/dist/esm/ColumnChooser/ColumnChooser.module.css +15 -0
  4. package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +49 -0
  5. package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.module.css +374 -0
  6. package/dist/esm/ColumnHeaderFilter/MultiSelectFilterPopover.js +9 -0
  7. package/dist/esm/ColumnHeaderFilter/PeopleFilterPopover.js +9 -0
  8. package/dist/esm/ColumnHeaderFilter/TextFilterPopover.js +12 -0
  9. package/dist/esm/ColumnHeaderFilter/index.js +1 -0
  10. package/dist/esm/DataGridTable/DataGridTable.js +265 -0
  11. package/dist/esm/DataGridTable/DataGridTable.module.css +592 -0
  12. package/dist/esm/DataGridTable/GridContextMenu.js +35 -0
  13. package/dist/esm/DataGridTable/InlineCellEditor.js +6 -0
  14. package/dist/esm/DataGridTable/StatusBar.js +7 -0
  15. package/dist/esm/FluentDataTable/FluentDataTable.js +18 -0
  16. package/dist/esm/FluentDataTable/index.js +1 -0
  17. package/dist/esm/PaginationControls/PaginationControls.js +20 -0
  18. package/dist/esm/PaginationControls/PaginationControls.module.css +74 -0
  19. package/dist/esm/index.js +8 -0
  20. package/dist/types/ColumnChooser/ColumnChooser.d.ts +10 -0
  21. package/dist/types/ColumnHeaderFilter/ColumnHeaderFilter.d.ts +22 -0
  22. package/dist/types/ColumnHeaderFilter/MultiSelectFilterPopover.d.ts +19 -0
  23. package/dist/types/ColumnHeaderFilter/PeopleFilterPopover.d.ts +14 -0
  24. package/dist/types/ColumnHeaderFilter/TextFilterPopover.d.ts +13 -0
  25. package/dist/types/ColumnHeaderFilter/index.d.ts +1 -0
  26. package/dist/types/DataGridTable/DataGridTable.d.ts +5 -0
  27. package/dist/types/DataGridTable/GridContextMenu.d.ts +10 -0
  28. package/dist/types/DataGridTable/InlineCellEditor.d.ts +12 -0
  29. package/dist/types/DataGridTable/StatusBar.d.ts +16 -0
  30. package/dist/types/FluentDataTable/FluentDataTable.d.ts +7 -0
  31. package/dist/types/FluentDataTable/index.d.ts +1 -0
  32. package/dist/types/PaginationControls/PaginationControls.d.ts +12 -0
  33. package/dist/types/index.d.ts +6 -0
  34. package/package.json +64 -0
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Spinner, Avatar } from '@fluentui/react-components';
3
+ import { SearchRegular, FilterRegular } from '@fluentui/react-icons';
4
+ import styles from './ColumnHeaderFilter.module.css';
5
+ export const PeopleFilterPopover = ({ selectedUser, searchText, onSearchChange, suggestions, isLoading, onUserSelect, onClearUser, onPopoverClick, inputRef, }) => (_jsxs(_Fragment, { children: [selectedUser && (_jsxs("div", { className: styles.selectedUserSection, onClick: onPopoverClick, children: [_jsx("div", { className: styles.selectedUserLabel, children: "Currently filtered by:" }), _jsxs("div", { className: styles.selectedUser, children: [_jsxs("div", { className: styles.userInfo, children: [_jsx(Avatar, { name: selectedUser.displayName, image: { src: selectedUser.photo }, size: 32 }), _jsxs("div", { className: styles.userText, children: [_jsx("div", { children: selectedUser.displayName }), _jsx("div", { className: styles.userSecondary, children: selectedUser.email })] })] }), _jsx("button", { type: "button", className: styles.removeUserButton, onClick: onClearUser, "aria-label": "Remove filter", children: _jsx(FilterRegular, {}) })] })] })), _jsx("div", { className: styles.popoverSearch, onClick: onPopoverClick, children: _jsxs("div", { className: styles.nativeInputWrapper, children: [_jsx(SearchRegular, { className: styles.nativeInputIcon }), _jsx("input", { ref: inputRef, type: "text", placeholder: "Search for a person...", value: searchText, onChange: (e) => onSearchChange(e.target.value), onFocus: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), autoComplete: "off", className: styles.nativeInput })] }) }), _jsx("div", { className: styles.popoverOptions, onClick: onPopoverClick, children: isLoading && searchText.trim() ? (_jsx("div", { className: styles.loadingContainer, children: _jsx(Spinner, { size: "small", label: "Searching..." }) })) : suggestions.length === 0 && searchText.trim() ? (_jsx("div", { className: styles.noResults, children: "No results found" })) : searchText.trim() ? (suggestions.map((user) => (_jsx("div", { className: styles.personOption, onClick: (e) => {
6
+ e.stopPropagation();
7
+ onUserSelect(user);
8
+ }, children: _jsxs("div", { className: styles.userInfo, children: [_jsx(Avatar, { name: user.displayName, image: { src: user.photo }, size: 32 }), _jsxs("div", { className: styles.userText, children: [_jsx("div", { children: user.displayName }), _jsx("div", { className: styles.userSecondary, children: user.email })] })] }) }, user.id ?? user.email ?? user.displayName)))) : (_jsx("div", { className: styles.noResults, children: "Type to search..." })) }), selectedUser && (_jsx("div", { className: styles.popoverActions, onClick: onPopoverClick, children: _jsx("button", { type: "button", className: styles.clearButton, onClick: onClearUser, children: "Clear Filter" }) }))] }));
9
+ PeopleFilterPopover.displayName = 'PeopleFilterPopover';
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Input } from '@fluentui/react-components';
3
+ import { SearchRegular } from '@fluentui/react-icons';
4
+ import styles from './ColumnHeaderFilter.module.css';
5
+ export const TextFilterPopover = ({ value, onValueChange, onApply, onClear, onPopoverClick, onInputFocus, onInputMouseDown, onInputClick, onInputKeyDown, }) => (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.popoverSearch, onClick: onPopoverClick, children: _jsx(Input, { placeholder: "Enter search term...", value: value, onChange: (e, data) => onValueChange(data.value ?? ''), onKeyDown: (e) => {
6
+ onInputKeyDown(e);
7
+ if (e.key === 'Enter') {
8
+ e.preventDefault();
9
+ onApply();
10
+ }
11
+ }, onFocus: onInputFocus, onMouseDown: onInputMouseDown, onClick: onInputClick, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }) }), _jsxs("div", { className: styles.popoverActions, onClick: onPopoverClick, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: onClear, disabled: !value, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: onApply, children: "Apply" })] })] }));
12
+ TextFilterPopover.displayName = 'TextFilterPopover';
@@ -0,0 +1 @@
1
+ export { ColumnHeaderFilter } from './ColumnHeaderFilter';
@@ -0,0 +1,265 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { useMemo, useRef, useEffect, useCallback } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { DataGrid, DataGridHeader, DataGridRow, DataGridHeaderCell, DataGridBody, DataGridCell, createTableColumn, Spinner, Checkbox, Popover, PopoverSurface, } from '@fluentui/react-components';
6
+ import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
7
+ import { InlineCellEditor } from './InlineCellEditor';
8
+ import { StatusBar } from './StatusBar';
9
+ import { GridContextMenu } from './GridContextMenu';
10
+ import { useDataGridState, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
11
+ import styles from './DataGridTable.module.css';
12
+ // Module-scope stable constants (avoid per-render allocations)
13
+ const gridRootStyle = {
14
+ position: 'relative',
15
+ flex: 1,
16
+ minHeight: 0,
17
+ display: 'flex',
18
+ flexDirection: 'column',
19
+ };
20
+ const CURSOR_CELL_STYLE = { cursor: 'cell' };
21
+ const NUMERIC_STYLE = { justifyContent: 'flex-end', textAlign: 'right' };
22
+ const BOOLEAN_STYLE = { justifyContent: 'center', textAlign: 'center' };
23
+ const EDITABLE_NUMERIC_STYLE = { cursor: 'cell', justifyContent: 'flex-end', textAlign: 'right' };
24
+ const EDITABLE_BOOLEAN_STYLE = { cursor: 'cell', justifyContent: 'center', textAlign: 'center' };
25
+ const POPOVER_ANCHOR_STYLE = { minHeight: '100%', minWidth: 40 };
26
+ const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
27
+ const NOOP = () => { };
28
+ function GridRowInner(props) {
29
+ const { item, rowId, rowIndex, isSelected, cellClassMap, handleSingleRowClick, activeCell } = props;
30
+ const rowClassName = `${isSelected ? styles.selectedRow : ''}${activeCell !== null && rowIndex === activeCell.rowIndex ? (isSelected ? ` ${styles.activeRow}` : styles.activeRow) : ''}` || undefined;
31
+ return (
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ _jsx(DataGridRow, { className: rowClassName, onClick: () => handleSingleRowClick(rowId), children: ({ renderCell, columnId }) => (_jsx(DataGridCell, { className: cellClassMap[String(columnId)] || undefined, children: renderCell(item) })) }));
34
+ }
35
+ const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
36
+ function DataGridTableInner(props) {
37
+ const wrapperRef = useRef(null);
38
+ const tableContainerRef = useRef(null);
39
+ const state = useDataGridState({ props, wrapperRef });
40
+ const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels } = state;
41
+ const { flatColumns, visibleCols, totalColCount, hasCheckboxCol, colOffset, rowIndexByRowId, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
42
+ const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
43
+ const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
44
+ const { activeCell, setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
45
+ const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
46
+ const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
47
+ const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
48
+ const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
49
+ // Memoize header rows (recursive tree traversal)
50
+ const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
51
+ const hasGroupHeaders = headerRows.length > 1;
52
+ const fitToContent = layoutMode === 'content';
53
+ const columnSizingOptions = useMemo(() => {
54
+ const acc = {};
55
+ if (hasCheckboxCol) {
56
+ acc['__selection__'] = { minWidth: CHECKBOX_COLUMN_WIDTH, defaultWidth: CHECKBOX_COLUMN_WIDTH, idealWidth: CHECKBOX_COLUMN_WIDTH };
57
+ }
58
+ visibleCols.forEach((c) => {
59
+ const minW = c.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
60
+ const defaultW = c.defaultWidth ?? 120;
61
+ const base = c.idealWidth ?? Math.max(minW, defaultW);
62
+ const override = columnSizingOverrides[c.columnId];
63
+ const w = override ? Math.max(minW, override.widthPx) : base;
64
+ acc[c.columnId] = {
65
+ minWidth: minW,
66
+ defaultWidth: w,
67
+ idealWidth: w,
68
+ };
69
+ });
70
+ return acc;
71
+ }, [visibleCols, columnSizingOverrides, hasCheckboxCol]);
72
+ const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
73
+ // Pre-compute column class maps (avoids per-cell .filter(Boolean).join(' '))
74
+ const { cellClassMap, headerClassMap } = useMemo(() => {
75
+ const cm = {};
76
+ const hm = {};
77
+ const im = new Map();
78
+ for (let i = 0; i < visibleCols.length; i++) {
79
+ const col = visibleCols[i];
80
+ const isFreezeCol = freezeCols != null && freezeCols >= 1 && i < freezeCols;
81
+ const isPinnedLeft = col.pinned === 'left';
82
+ const isPinnedRight = col.pinned === 'right';
83
+ const parts = [];
84
+ if (isFreezeCol)
85
+ parts.push(styles.freezeCol);
86
+ if (isFreezeCol && i === 0)
87
+ parts.push(styles.freezeColFirst);
88
+ if (isPinnedLeft) {
89
+ parts.push(styles.pinnedCell);
90
+ parts.push(styles.pinnedLeft);
91
+ }
92
+ if (isPinnedRight) {
93
+ parts.push(styles.pinnedCell);
94
+ parts.push(styles.pinnedRight);
95
+ }
96
+ cm[col.columnId] = parts.join(' ');
97
+ hm[col.columnId] = parts.join(' ');
98
+ im.set(col.columnId, i);
99
+ }
100
+ cm['__selection__'] = styles.selectionCellWrapper;
101
+ hm['__selection__'] = styles.selectionHeaderCellWrapper;
102
+ return { cellClassMap: cm, headerClassMap: hm, colIndexMap: im };
103
+ }, [visibleCols, freezeCols]);
104
+ // Refs for volatile state (read inside fluentColumns render closures without adding to deps)
105
+ const cellDescriptorInputRef = useLatestRef(cellDescriptorInput);
106
+ const selectedRowIdsRef = useLatestRef(selectedRowIds);
107
+ const activeCellRef = useLatestRef(activeCell);
108
+ const pendingEditorValueRef = useLatestRef(pendingEditorValue);
109
+ const popoverAnchorElRef = useLatestRef(popoverAnchorEl);
110
+ const allSelectedRef = useLatestRef(allSelected);
111
+ const someSelectedRef = useLatestRef(someSelected);
112
+ // Callback refs — stabilize fluentColumns memo (these change identity on state updates
113
+ // but the columns structure doesn't need rebuilding for that)
114
+ const headerFilterInputRef = useLatestRef(headerFilterInput);
115
+ const commitCellEditRef = useLatestRef(commitCellEdit);
116
+ const cancelPopoverEditRef = useLatestRef(cancelPopoverEdit);
117
+ const handleCellMouseDownRef = useLatestRef(handleCellMouseDown);
118
+ const handleFillHandleMouseDownRef = useLatestRef(handleFillHandleMouseDown);
119
+ const handleCellContextMenuRef = useLatestRef(handleCellContextMenu);
120
+ const setActiveCellRef = useLatestRef(setActiveCell);
121
+ const setEditingCellRef = useLatestRef(setEditingCell);
122
+ const setPendingEditorValueRef = useLatestRef(setPendingEditorValue);
123
+ const handleSelectAllRef = useLatestRef(handleSelectAll);
124
+ const handleRowCheckboxChangeRef = useLatestRef(handleRowCheckboxChange);
125
+ const rowIndexByRowIdRef = useLatestRef(rowIndexByRowId);
126
+ const fluentColumns = useMemo(() => {
127
+ const dataCols = visibleCols.map((col, colIdx) => createTableColumn({
128
+ columnId: col.columnId,
129
+ compare: col.compare ?? (() => 0),
130
+ renderHeaderCell: () => (_jsx("div", { "data-column-id": col.columnId, children: _jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInputRef.current) }) })),
131
+ renderCell: (item) => {
132
+ const rowId = getRowId(item);
133
+ const rowIndex = rowIndexByRowIdRef.current.get(rowId) ?? -1;
134
+ const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
135
+ let cellContent;
136
+ if (descriptor.mode === 'editing-inline') {
137
+ cellContent = _jsx(InlineCellEditor, { ...buildInlineEditorProps(item, col, descriptor, { commitCellEdit: commitCellEditRef.current, setEditingCell: setEditingCellRef.current }) });
138
+ }
139
+ else if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
140
+ const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValueRef.current, { setPendingEditorValue: setPendingEditorValueRef.current, commitCellEdit: commitCellEditRef.current, cancelPopoverEdit: cancelPopoverEditRef.current });
141
+ const CustomEditor = col.cellEditor;
142
+ cellContent = (_jsxs(_Fragment, { children: [_jsx("div", { ref: (el) => { if (el)
143
+ setPopoverAnchorEl(el); }, style: POPOVER_ANCHOR_STYLE, "aria-hidden": true }), _jsx(Popover, { open: !!popoverAnchorElRef.current, onOpenChange: (_, data) => { if (!data.open)
144
+ cancelPopoverEditRef.current(); }, positioning: { target: popoverAnchorElRef.current ?? undefined }, children: _jsx(PopoverSurface, { children: _jsx(CustomEditor, { ...editorProps }) }) })] }));
145
+ }
146
+ else {
147
+ const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
148
+ const cellStyle = resolveCellStyle(col, item);
149
+ const styledContent = cellStyle ? _jsx("span", { style: cellStyle, children: content }) : content;
150
+ const cellClassNames = `${styles.cellContent}${descriptor.isActive && !descriptor.isInRange ? ` ${styles.activeCellContent}` : ''}${descriptor.isInRange ? ` ${styles.cellInRange}` : ''}${descriptor.isInCutRange ? ` ${styles.cellCut}` : ''}${descriptor.isInCopyRange ? ` ${styles.cellCopied}` : ''}`;
151
+ const colType = col.type;
152
+ const interactionProps = getCellInteractionProps(descriptor, col.columnId, { handleCellMouseDown: handleCellMouseDownRef.current, setActiveCell: setActiveCellRef.current, setEditingCell: setEditingCellRef.current, handleCellContextMenu: handleCellContextMenuRef.current });
153
+ // Select stable style constant by type + editability
154
+ const computedStyle = descriptor.canEditAny
155
+ ? (colType === 'numeric' ? EDITABLE_NUMERIC_STYLE : colType === 'boolean' ? EDITABLE_BOOLEAN_STYLE : CURSOR_CELL_STYLE)
156
+ : (colType === 'numeric' ? NUMERIC_STYLE : colType === 'boolean' ? BOOLEAN_STYLE : undefined);
157
+ cellContent = (_jsxs("div", { className: cellClassNames, ...interactionProps, style: computedStyle, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: (e) => handleFillHandleMouseDownRef.current(e), "aria-label": "Fill handle" }))] }));
158
+ }
159
+ return (_jsx(CellErrorBoundary, { onError: onCellError, children: cellContent }, `${rowId}-${col.columnId}`));
160
+ },
161
+ }));
162
+ if (hasCheckboxCol) {
163
+ const checkboxCol = createTableColumn({
164
+ columnId: '__selection__',
165
+ compare: () => 0,
166
+ renderHeaderCell: () => (_jsx("div", { className: styles.selectionHeaderCell, children: _jsx(Checkbox, { checked: allSelectedRef.current ? true : someSelectedRef.current ? 'mixed' : false, onChange: (_, data) => handleSelectAllRef.current(!!data.checked), "aria-label": "Select all rows" }) })),
167
+ renderCell: (item) => {
168
+ const rowId = getRowId(item);
169
+ const rowIndex = rowIndexByRowIdRef.current.get(rowId) ?? -1;
170
+ const isChecked = selectedRowIdsRef.current.has(rowId);
171
+ const ac = activeCellRef.current;
172
+ const isActive = ac?.rowIndex === rowIndex && ac?.columnIndex === 0;
173
+ return (_jsx("div", { className: `${styles.selectionCell} ${isActive ? styles.activeCellContent : ''}`, "data-row-index": rowIndex, "data-col-index": 0, onClick: (e) => {
174
+ e.stopPropagation();
175
+ setActiveCellRef.current({ rowIndex, columnIndex: 0 });
176
+ }, children: _jsx(Checkbox, { checked: isChecked, onChange: (e, data) => {
177
+ handleRowCheckboxChangeRef.current(rowId, !!data.checked, rowIndex, e.nativeEvent.shiftKey);
178
+ }, "aria-label": `Select row ${rowIndex + 1}` }) }));
179
+ },
180
+ });
181
+ return [checkboxCol, ...dataCols];
182
+ }
183
+ return dataCols;
184
+ // eslint-disable-next-line react-hooks/exhaustive-deps
185
+ }, [visibleCols, hasCheckboxCol, getRowId, setPopoverAnchorEl]); // All volatile state/callbacks read via refs
186
+ // Stable row-click handler
187
+ const handleSingleRowClick = useCallback((rowId) => {
188
+ if (rowSelection !== 'single')
189
+ return;
190
+ const ids = selectedRowIdsRef.current;
191
+ updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
192
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- selectedRowIdsRef is a stable ref
193
+ }, [rowSelection, updateSelection]);
194
+ // Stable getRowId wrapper for Fluent DataGrid
195
+ const fluentGetRowId = useCallback((item) => String(getRowId(item)), [getRowId]);
196
+ // Double-click to auto-fit column width
197
+ useEffect(() => {
198
+ const root = wrapperRef.current;
199
+ if (!root)
200
+ return;
201
+ const onDblClick = (e) => {
202
+ const target = e.target;
203
+ if (!target)
204
+ return;
205
+ if (!target.closest('.fui-TableResizeHandle'))
206
+ return;
207
+ const headerCell = target.closest('[role="columnheader"]');
208
+ if (!headerCell)
209
+ return;
210
+ const colId = headerCell.querySelector('[data-column-id]')?.getAttribute('data-column-id');
211
+ if (!colId)
212
+ return;
213
+ const label = headerCell.querySelector('[data-header-label]');
214
+ const labelWidth = label ? label.scrollWidth : 0;
215
+ const EXTRA_PX = 44;
216
+ const MAX_PX = 520;
217
+ const colDef = flatColumns.find((c) => c.columnId === colId);
218
+ const minW = colDef?.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
219
+ const desired = Math.min(MAX_PX, Math.max(minW, Math.ceil(labelWidth + EXTRA_PX)));
220
+ setColumnSizingOverrides((prev) => ({
221
+ ...prev,
222
+ [colId]: { widthPx: desired },
223
+ }));
224
+ e.preventDefault();
225
+ e.stopPropagation();
226
+ };
227
+ root.addEventListener('dblclick', onDblClick, true);
228
+ return () => root.removeEventListener('dblclick', onDblClick, true);
229
+ }, [flatColumns, setColumnSizingOverrides]);
230
+ // Sync Fluent's internal resize state back to our React state so that
231
+ // re-renders (e.g. on cell click) don't reset column widths.
232
+ const handleColumnResize = useCallback((_e, data) => {
233
+ setColumnSizingOverrides((prev) => ({
234
+ ...prev,
235
+ [String(data.columnId)]: { widthPx: data.width },
236
+ }));
237
+ }, [setColumnSizingOverrides]);
238
+ return (_jsxs("div", { style: gridRootStyle, children: [_jsxs("div", { ref: wrapperRef, tabIndex: 0, 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-auto-fit": layoutMode === 'fill' && !allowOverflowX ? 'true' : undefined, "data-column-count": totalColCount, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-container-width": containerWidth, "data-min-table-width": Math.round(minTableWidth), "data-has-selection": rowSelection !== 'none' ? 'true' : undefined, onContextMenu: PREVENT_DEFAULT, style: {
239
+ ['--data-table-column-count']: totalColCount,
240
+ ['--data-table-width']: showEmptyInGrid
241
+ ? '100%'
242
+ : allowOverflowX
243
+ ? 'fit-content'
244
+ : fitToContent
245
+ ? 'fit-content'
246
+ : '100%',
247
+ ['--data-table-min-width']: showEmptyInGrid
248
+ ? '100%'
249
+ : allowOverflowX
250
+ ? 'max-content'
251
+ : fitToContent
252
+ ? 'max-content'
253
+ : '100%',
254
+ }, onKeyDown: handleGridKeyDown, children: [_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: fluentGetRowId, 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: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLUMN_WIDTH } })), row.map((cell, cellIdx) => {
255
+ if (cell.isGroup) {
256
+ return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
257
+ }
258
+ return (_jsx("th", { rowSpan: headerRows.length - rowIdx, className: styles.leafHeaderCellSpan, scope: "col", children: cell.columnDef?.name }, cellIdx));
259
+ })] }, `group-${rowIdx}`))), _jsx(DataGridRow, { children: ({ renderHeaderCell, columnId }) => (_jsx(DataGridHeaderCell, { className: headerClassMap[String(columnId)] || undefined, children: renderHeaderCell() })) })] }), _jsx(DataGridBody, { children: ({ item }) => {
260
+ const rowId = getRowId(item);
261
+ return (_jsx(GridRow, { item: item, rowId: rowId, rowIndex: rowIndexByRowId.get(rowId) ?? -1, isSelected: selectedRowIds.has(rowId), hasCheckboxCol: hasCheckboxCol, cellClassMap: cellClassMap, handleSingleRowClick: handleSingleRowClick, selectionRange: selectionRange, activeCell: activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowId));
262
+ } })] }), _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.') })] })) }) }))] }) }) }), menuPosition &&
263
+ createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? NOOP, onRedo: onRedo ?? NOOP, onCopy: handleCopy, onCut: handleCut, onPaste: handlePasteVoid, onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body)] }), statusBarConfig && (_jsx(StatusBar, { totalCount: statusBarConfig.totalCount, filteredCount: statusBarConfig.filteredCount, selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size, selectedCellCount: selectionRange ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1) : undefined, aggregation: statusBarConfig.aggregation, suppressRowCount: statusBarConfig.suppressRowCount })), isLoading && (_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 })] }) }))] }));
264
+ }
265
+ export const DataGridTable = React.memo(DataGridTableInner);