@alaarab/ogrid-react-fluent 2.0.18 → 2.0.21

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.
@@ -1,14 +1,14 @@
1
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 { Popover, PopoverSurface } from '@fluentui/react-components';
4
- import { ArrowUpRegular, ArrowDownRegular, ArrowSortRegular, FilterRegular } from '@fluentui/react-icons';
4
+ import { FilterRegular } from '@fluentui/react-icons';
5
5
  import { useColumnHeaderFilterState, getColumnHeaderFilterStateParams, DateFilterContent, renderFilterContent, } from '@alaarab/ogrid-react';
6
6
  import { TextFilterPopover } from './TextFilterPopover';
7
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, options, isLoadingOptions = false, selectedUser, } = props;
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
14
  const filterBtnRef = React.useRef(null);
@@ -23,7 +23,7 @@ export const ColumnHeaderFilter = React.memo((props) => {
23
23
  applyButton: styles.applyButton,
24
24
  } }) })),
25
25
  }), [handlers]);
26
- 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: isSorted ? (isSortedDescending ? _jsx(ArrowDownRegular, {}) : _jsx(ArrowUpRegular, {})) : (_jsx(ArrowSortRegular, {})) })), 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
- 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)] }) })] }))] })] }));
26
+ 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
+ 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
28
  });
29
29
  ColumnHeaderFilter.displayName = 'ColumnHeaderFilter';
@@ -58,36 +58,6 @@
58
58
  flex-shrink: 0;
59
59
  }
60
60
 
61
- .sortIcon {
62
- display: flex;
63
- align-items: center;
64
- justify-content: center;
65
- width: 24px;
66
- height: 24px;
67
- padding: 4px;
68
- border: none;
69
- border-radius: var(--borderRadiusSmall, 4px);
70
- background-color: transparent;
71
- color: var(--colorNeutralForeground2, #616161);
72
- cursor: pointer;
73
- flex-shrink: 0;
74
- transition: all 0.15s ease;
75
- }
76
- .sortIcon svg {
77
- font-size: 14px;
78
- }
79
- .sortIcon:hover {
80
- background-color: var(--colorSubtleBackgroundHover, #f5f5f5);
81
- color: var(--colorNeutralForeground2Hover, #424242);
82
- }
83
- .sortIcon.sortActive {
84
- background-color: var(--colorSubtleBackgroundSelected, #e0e0e0);
85
- color: var(--colorNeutralForeground2, #616161);
86
- }
87
- .sortIcon.sortActive:hover {
88
- background-color: var(--colorSubtleBackgroundHover, #f5f5f5);
89
- }
90
-
91
61
  .filterIcon {
92
62
  display: flex;
93
63
  align-items: center;
@@ -270,10 +240,27 @@
270
240
  }
271
241
 
272
242
  .loadingContainer {
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ gap: 8px;
273
247
  padding: 20px;
274
- text-align: center;
275
248
  }
276
249
 
250
+ .filterSpinner {
251
+ width: 20px;
252
+ height: 20px;
253
+ border: 2px solid var(--colorNeutralStroke1, #d1d1d1);
254
+ border-top-color: var(--colorBrandBackground, #0f6cbd);
255
+ border-radius: 50%;
256
+ animation: ogrid-filter-spin 0.8s linear infinite;
257
+ }
258
+
259
+ @keyframes ogrid-filter-spin {
260
+ to {
261
+ transform: rotate(360deg);
262
+ }
263
+ }
277
264
  .noResults {
278
265
  padding: 16px;
279
266
  text-align: center;
@@ -306,6 +293,32 @@
306
293
  gap: 8px;
307
294
  }
308
295
 
296
+ .avatar {
297
+ width: 32px;
298
+ height: 32px;
299
+ border-radius: 50%;
300
+ background-color: var(--colorBrandBackground, #0f6cbd);
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ flex-shrink: 0;
305
+ overflow: hidden;
306
+ }
307
+
308
+ .avatarImg {
309
+ width: 100%;
310
+ height: 100%;
311
+ object-fit: cover;
312
+ border-radius: 50%;
313
+ }
314
+
315
+ .avatarInitials {
316
+ color: var(--colorNeutralForegroundOnBrand, #ffffff);
317
+ font-size: 12px;
318
+ font-weight: 600;
319
+ line-height: 1;
320
+ }
321
+
309
322
  .userText {
310
323
  display: flex;
311
324
  flex-direction: column;
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Input, Checkbox, Spinner } from '@fluentui/react-components';
2
+ import { Input, Checkbox } from '@fluentui/react-components';
3
3
  import { SearchRegular } from '@fluentui/react-icons';
4
4
  import { useListVirtualizer } from '@alaarab/ogrid-react';
5
5
  import styles from './ColumnHeaderFilter.module.css';
6
- const ITEM_HEIGHT = 32;
6
+ const ITEM_HEIGHT = 40;
7
7
  export const MultiSelectFilterPopover = ({ searchText, onSearchChange, options, filteredOptions, selected, onOptionToggle, onSelectAll, onClearSelection, onApply, isLoading, onPopoverClick, onInputFocus, onInputMouseDown, onInputClick, onInputKeyDown, }) => {
8
8
  const virt = useListVirtualizer({ count: filteredOptions.length, itemHeight: ITEM_HEIGHT });
9
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.popoverSearch, onClick: onPopoverClick, children: [_jsx(Input, { placeholder: "Search...", value: searchText, onChange: (e, data) => onSearchChange(data.value ?? ''), onFocus: onInputFocus, onMouseDown: onInputMouseDown, onClick: onInputClick, onKeyDown: onInputKeyDown, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }), _jsxs("div", { className: styles.resultCount, children: [filteredOptions.length, " of ", options.length, " options"] })] }), _jsxs("div", { className: styles.selectAllRow, onClick: onPopoverClick, children: [_jsxs("button", { type: "button", className: styles.selectAllButton, onClick: onSelectAll, children: ["Select All (", filteredOptions.length, ")"] }), _jsx("button", { type: "button", className: styles.selectAllButton, onClick: onClearSelection, children: "Clear" })] }), _jsx("div", { ref: virt.containerRef, onScroll: virt.onScroll, className: styles.popoverOptions, onClick: onPopoverClick, children: isLoading ? (_jsx("div", { className: styles.loadingContainer, children: _jsx(Spinner, { size: "small", label: "Loading..." }) })) : filteredOptions.length === 0 ? (_jsx("div", { className: styles.noResults, children: "No options found" })) : (_jsx("div", { style: { height: virt.totalHeight, position: 'relative' }, children: virt.visibleItems.map(({ index, offsetTop }) => {
9
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.popoverSearch, onClick: onPopoverClick, children: [_jsx(Input, { placeholder: "Search...", value: searchText, onChange: (e, data) => onSearchChange(data.value ?? ''), onFocus: onInputFocus, onMouseDown: onInputMouseDown, onClick: onInputClick, onKeyDown: onInputKeyDown, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }), _jsxs("div", { className: styles.resultCount, children: [filteredOptions.length, " of ", options.length, " options"] })] }), _jsxs("div", { className: styles.selectAllRow, onClick: onPopoverClick, children: [_jsxs("button", { type: "button", className: styles.selectAllButton, onClick: onSelectAll, children: ["Select All (", filteredOptions.length, ")"] }), _jsx("button", { type: "button", className: styles.selectAllButton, onClick: onClearSelection, children: "Clear" })] }), _jsx("div", { ref: virt.containerRef, onScroll: virt.onScroll, className: styles.popoverOptions, onClick: onPopoverClick, children: isLoading ? (_jsxs("div", { className: styles.loadingContainer, children: [_jsx("div", { className: styles.filterSpinner }), _jsx("span", { style: { fontSize: 12, color: 'var(--colorNeutralForeground2, #616161)' }, children: "Loading..." })] })) : filteredOptions.length === 0 ? (_jsx("div", { className: styles.noResults, children: "No options found" })) : (_jsx("div", { style: { height: virt.totalHeight, position: 'relative' }, children: virt.visibleItems.map(({ index, offsetTop }) => {
10
10
  const option = filteredOptions[index];
11
- return (_jsx("div", { className: styles.popoverOption, style: { position: 'absolute', top: offsetTop, width: '100%', boxSizing: 'border-box' }, children: _jsx(Checkbox, { label: option, checked: selected.has(option), onChange: (ev, data) => {
11
+ return (_jsx("div", { className: styles.popoverOption, style: { position: 'absolute', top: offsetTop, width: '100%', height: ITEM_HEIGHT, boxSizing: 'border-box', display: 'flex', alignItems: 'center' }, children: _jsx(Checkbox, { label: option, checked: selected.has(option), onChange: (ev, data) => {
12
12
  ev.stopPropagation();
13
13
  onOptionToggle(option, data.checked === true);
14
14
  } }) }, option));
@@ -1,9 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Spinner, Avatar } from '@fluentui/react-components';
3
2
  import { SearchRegular, FilterRegular } from '@fluentui/react-icons';
4
3
  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) => {
4
+ function getInitials(name) {
5
+ const parts = name.trim().split(/\s+/);
6
+ if (parts.length >= 2)
7
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
8
+ return (parts[0]?.[0] ?? '').toUpperCase();
9
+ }
10
+ 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("div", { className: styles.avatar, children: selectedUser.photo
11
+ ? _jsx("img", { src: selectedUser.photo, alt: "", className: styles.avatarImg })
12
+ : _jsx("span", { className: styles.avatarInitials, children: getInitials(selectedUser.displayName) }) }), _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() ? (_jsxs("div", { className: styles.loadingContainer, children: [_jsx("div", { className: styles.filterSpinner }), _jsx("span", { style: { fontSize: 12, color: 'var(--colorNeutralForeground2, #616161)' }, children: "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
13
  e.stopPropagation();
7
14
  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" }) }))] }));
15
+ }, children: _jsxs("div", { className: styles.userInfo, children: [_jsx("div", { className: styles.avatar, children: user.photo
16
+ ? _jsx("img", { src: user.photo, alt: "", className: styles.avatarImg })
17
+ : _jsx("span", { className: styles.avatarInitials, children: getInitials(user.displayName) }) }), _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
18
  PeopleFilterPopover.displayName = 'PeopleFilterPopover';
@@ -1,77 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useMemo, useEffect, useState, useRef } from 'react';
3
- import { createPortal } from 'react-dom';
4
- import { getColumnHeaderMenuItems } from '@alaarab/ogrid-react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BaseColumnHeaderMenu } from '@alaarab/ogrid-react';
5
3
  import styles from './ColumnHeaderMenu.module.css';
4
+ /** Portal into the closest FluentProvider so --ogrid-* bridged variables are available */
5
+ const getFluentPortalTarget = (anchorElement) => anchorElement.closest('.fui-FluentProvider') ?? document.body;
6
6
  /**
7
7
  * Column header dropdown menu for pin/sort/autosize actions.
8
- * Uses positioned div with portal rendering.
8
+ * Thin wrapper over BaseColumnHeaderMenu portals into FluentProvider.
9
9
  */
10
10
  export function ColumnHeaderMenu(props) {
11
- const { isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, onSortAsc, onSortDesc, onClearSort, onAutosizeThis, onAutosizeAll, canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable, } = props;
12
- const [position, setPosition] = useState(null);
13
- const menuRef = useRef(null);
14
- useEffect(() => {
15
- if (!isOpen || !anchorElement) {
16
- setPosition(null);
17
- return;
18
- }
19
- const rect = anchorElement.getBoundingClientRect();
20
- setPosition({
21
- top: rect.bottom + 4,
22
- left: rect.left,
23
- });
24
- const handleClickOutside = (e) => {
25
- const target = e.target;
26
- // Don't close if clicking inside the menu itself (portal) — let onClick fire first
27
- if (menuRef.current && menuRef.current.contains(target))
28
- return;
29
- if (anchorElement && !anchorElement.contains(target)) {
30
- onClose();
31
- }
32
- };
33
- const handleEscape = (e) => {
34
- if (e.key === 'Escape') {
35
- onClose();
36
- }
37
- };
38
- document.addEventListener('mousedown', handleClickOutside);
39
- document.addEventListener('keydown', handleEscape);
40
- return () => {
41
- document.removeEventListener('mousedown', handleClickOutside);
42
- document.removeEventListener('keydown', handleEscape);
43
- };
44
- }, [isOpen, anchorElement, onClose]);
45
- const menuInput = useMemo(() => ({
46
- canPinLeft,
47
- canPinRight,
48
- canUnpin,
49
- currentSort,
50
- isSortable,
51
- isResizable,
52
- }), [canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]);
53
- const items = useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
54
- const handlers = {
55
- pinLeft: onPinLeft,
56
- pinRight: onPinRight,
57
- unpin: onUnpin,
58
- sortAsc: onSortAsc,
59
- sortDesc: onSortDesc,
60
- clearSort: onClearSort,
61
- autosizeThis: onAutosizeThis,
62
- autosizeAll: onAutosizeAll,
63
- };
64
- if (!isOpen || !position)
65
- return null;
66
- // Portal into the closest FluentProvider so --ogrid-* bridged variables are available
67
- const portalTarget = anchorElement?.closest('.fui-FluentProvider') ?? document.body;
68
- return createPortal(_jsx("div", { ref: menuRef, className: styles.content, style: {
69
- position: 'fixed',
70
- top: position.top,
71
- left: position.left,
72
- zIndex: 1000,
73
- }, children: items.map((item, idx) => (_jsxs(React.Fragment, { children: [_jsx("button", { className: styles.item, disabled: item.disabled, onClick: () => {
74
- handlers[item.id]();
75
- onClose();
76
- }, children: item.label }), item.divider && idx < items.length - 1 && (_jsx("div", { className: styles.separator }))] }, item.id))) }), portalTarget);
11
+ return (_jsx(BaseColumnHeaderMenu, { ...props, classNames: styles, getPortalTarget: getFluentPortalTarget }));
77
12
  }
@@ -11,18 +11,19 @@ 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, DEFAULT_MIN_COLUMN_WIDTH, GRID_ROOT_STYLE, CURSOR_CELL_STYLE, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, NOOP, } from '@alaarab/ogrid-react';
14
+ import { useDataGridTableOrchestration, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, 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
+ // --- Memoized row component (skips re-render for rows unaffected by selection changes) ---
16
17
  function GridRowInner(props) {
17
18
  const { item, rowIndex, rowId, isSelected, visibleCols, columnMeta, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, hasRowNumbersCol, rowNumberOffset, } = props;
18
- return (_jsxs(TableRow, { className: isSelected ? styles.selectedRow : undefined, "data-row-id": rowId, onClick: handleSingleRowClick, children: [hasCheckboxCol && (_jsx(TableCell, { className: styles.selectionCellWrapper, children: _jsx("div", { className: styles.selectionCellInner, "data-row-index": rowIndex, "data-col-index": 0, onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox, { checked: isSelected, onChange: (e, data) => {
19
+ return (_jsxs(TableRow, { className: isSelected ? styles.selectedRow : undefined, "data-row-id": rowId, onClick: handleSingleRowClick, children: [hasCheckboxCol && (_jsx(TableCell, { className: styles.selectionCellWrapper, children: _jsx("div", { className: styles.selectionCellInner, "data-row-index": rowIndex, "data-col-index": 0, onClick: STOP_PROPAGATION, children: _jsx(Checkbox, { checked: isSelected, onChange: (e, data) => {
19
20
  handleRowCheckboxChange(rowId, !!data.checked, rowIndex, lastMouseShiftRef.current);
20
21
  }, "aria-label": `Select row ${rowIndex + 1}` }) }) })), hasRowNumbersCol && (_jsx(TableCell, { className: styles.rowNumberCellWrapper, children: _jsx("div", { className: styles.rowNumberCellInner, children: rowNumberOffset + rowIndex + 1 }) })), visibleCols.map((col, colIdx) => (_jsx(TableCell, { "data-column-id": col.columnId, className: columnMeta.cellClasses[col.columnId] || undefined, style: columnMeta.cellStyles[col.columnId], children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId)))] }));
21
22
  }
22
23
  const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
23
24
  function DataGridTableInner(props) {
24
25
  const o = useDataGridTableOrchestration({ props });
25
- 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, 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;
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;
26
27
  // Pre-compute column styles and classNames (avoids per-cell object creation in the row loop)
27
28
  const columnMeta = useMemo(() => {
28
29
  const cellStyles = {};
@@ -105,6 +106,7 @@ function DataGridTableInner(props) {
105
106
  ['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
106
107
  ['--data-table-min-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'max-content' : fitToContent ? 'max-content' : '100%',
107
108
  ['--data-table-total-min-width']: `${minTableWidth}px`,
109
+ ...(rowHeight ? { ['--ogrid-row-height']: `${rowHeight}px` } : {}),
108
110
  }, 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(Table, { role: "grid", className: styles.dataTable, children: [_jsx(TableHeader, { className: styles.stickyHeader, children: headerRows.map((row, rowIdx) => (_jsxs(TableRow, { children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx(TableHeaderCell, { className: styles.selectionHeaderCellWrapper, children: _jsx("div", { className: styles.selectionHeaderCellInner, children: _jsx(Checkbox, { checked: allSelected ? true : someSelected ? 'mixed' : false, onChange: (_, data) => handleSelectAll(!!data.checked), "aria-label": "Select all rows" }) }) }, "__selection__")), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1 }, "__selection_placeholder__")), rowIdx === headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableHeaderCell, { className: styles.rowNumberHeaderCellWrapper, children: _jsx("div", { className: styles.rowNumberHeaderCellInner, children: "#" }) }, "__row_number__")), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol && (_jsx("th", { rowSpan: headerRows.length - 1 }, "__row_number_placeholder__")), row.map((cell, cellIdx) => {
109
111
  if (cell.isGroup) {
110
112
  return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
@@ -48,6 +48,9 @@
48
48
  background-color: var(--ogrid-bg, #fff);
49
49
  color: var(--ogrid-fg, rgba(0, 0, 0, 0.87));
50
50
  }
51
+ .dataTable tbody tr {
52
+ height: var(--ogrid-row-height, auto);
53
+ }
51
54
 
52
55
  .groupHeaderCell {
53
56
  text-align: center;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BaseDropIndicator } from '@alaarab/ogrid-react';
2
3
  import styles from './DataGridTable.module.css';
3
4
  export function DropIndicator({ dropIndicatorX, wrapperLeft }) {
4
- return (_jsx("div", { className: styles.dropIndicator, style: { left: dropIndicatorX - wrapperLeft } }));
5
+ return _jsx(BaseDropIndicator, { dropIndicatorX: dropIndicatorX, wrapperLeft: wrapperLeft, className: styles.dropIndicator });
5
6
  }
@@ -1,5 +1,14 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BaseEmptyState } from '@alaarab/ogrid-react';
2
3
  import styles from './DataGridTable.module.css';
4
+ const classNames = {
5
+ emptyStateInGrid: styles.emptyStateInGrid,
6
+ emptyStateInGridInner: styles.emptyStateInGridMessageSticky,
7
+ emptyStateInGridIcon: styles.emptyStateInGridIcon,
8
+ emptyStateInGridTitle: styles.emptyStateInGridTitle,
9
+ emptyStateInGridMessage: styles.emptyStateInGridMessage,
10
+ emptyStateInGridLink: styles.emptyStateInGridLink,
11
+ };
3
12
  export function EmptyState({ emptyState }) {
4
- return (_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.') })] })) }) }));
13
+ return _jsx(BaseEmptyState, { emptyState: emptyState, classNames: classNames, icon: '\uD83D\uDCCB' });
5
14
  }
@@ -1,6 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Spinner } from '@fluentui/react-components';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BaseLoadingOverlay } from '@alaarab/ogrid-react';
3
3
  import styles from './DataGridTable.module.css';
4
+ const classNames = {
5
+ loadingOverlay: styles.loadingOverlay,
6
+ loadingOverlayContent: styles.loadingOverlayContent,
7
+ spinner: styles.spinner,
8
+ loadingOverlayText: styles.loadingOverlayText,
9
+ };
4
10
  export function LoadingOverlay({ message }) {
5
- return (_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: message })] }) }));
11
+ return _jsx(BaseLoadingOverlay, { message: message, classNames: classNames });
6
12
  }
@@ -1,7 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { getStatusBarParts } from '@alaarab/ogrid-react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { StatusBar as BaseStatusBar } from '@alaarab/ogrid-react';
3
3
  import styles from './DataGridTable.module.css';
4
+ const statusBarClassNames = {
5
+ statusBar: styles.statusBar,
6
+ statusBarItem: styles.statusBarItem,
7
+ statusBarLabel: styles.statusBarLabel,
8
+ statusBarValue: styles.statusBarValue,
9
+ };
4
10
  export function StatusBar(props) {
5
- const parts = getStatusBarParts(props);
6
- return (_jsx("div", { className: styles.statusBar, role: "status", "aria-live": "polite", children: parts.map((p) => (_jsxs("span", { className: styles.statusBarItem, children: [_jsx("span", { className: styles.statusBarLabel, children: p.label }), _jsx("span", { className: styles.statusBarValue, children: p.value.toLocaleString() })] }, p.key))) }));
11
+ return _jsx(BaseStatusBar, { ...props, classNames: statusBarClassNames });
7
12
  }
@@ -1,16 +1,9 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import * as React from 'react';
3
- import { forwardRef } from 'react';
1
+ import { createOGrid } from '@alaarab/ogrid-react';
4
2
  import { DataGridTable } from '../DataGridTable/DataGridTable';
5
3
  import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
6
4
  import { PaginationControls } from '../PaginationControls/PaginationControls';
7
- import { useOGrid, OGridLayout, } from '@alaarab/ogrid-react';
8
- const OGridInner = forwardRef(function OGridInner(props, ref) {
9
- const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
10
- return (_jsx(OGridLayout, { className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: (size) => {
11
- pagination.setPageSize(size);
12
- pagination.setPage(1);
13
- }, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
5
+ export const OGrid = createOGrid({
6
+ DataGridTable: DataGridTable,
7
+ ColumnChooser: ColumnChooser,
8
+ PaginationControls,
14
9
  });
15
- OGridInner.displayName = 'OGrid';
16
- export const OGrid = React.memo(OGridInner);
package/dist/esm/index.js CHANGED
@@ -5,5 +5,7 @@ export { ColumnChooser } from './ColumnChooser/ColumnChooser';
5
5
  export { ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter';
6
6
  export { ColumnHeaderMenu } from './ColumnHeaderMenu/ColumnHeaderMenu';
7
7
  export { PaginationControls } from './PaginationControls/PaginationControls';
8
- // Re-export everything from core
8
+ // Re-export all from base package for consumer convenience.
9
+ // Note: This prevents tree-shaking of unused utilities.
10
+ // Consider explicit named exports in a future major version.
9
11
  export * from '@alaarab/ogrid-react';
@@ -1,25 +1,7 @@
1
- import React from 'react';
2
- export interface ColumnHeaderMenuProps {
3
- isOpen: boolean;
4
- anchorElement: HTMLElement | null;
5
- onClose: () => void;
6
- onPinLeft: () => void;
7
- onPinRight: () => void;
8
- onUnpin: () => void;
9
- onSortAsc: () => void;
10
- onSortDesc: () => void;
11
- onClearSort: () => void;
12
- onAutosizeThis: () => void;
13
- onAutosizeAll: () => void;
14
- canPinLeft: boolean;
15
- canPinRight: boolean;
16
- canUnpin: boolean;
17
- currentSort: 'asc' | 'desc' | null;
18
- isSortable: boolean;
19
- isResizable: boolean;
20
- }
1
+ import type { BaseColumnHeaderMenuProps } from '@alaarab/ogrid-react';
2
+ export type ColumnHeaderMenuProps = Omit<BaseColumnHeaderMenuProps, 'classNames' | 'getPortalTarget'>;
21
3
  /**
22
4
  * Column header dropdown menu for pin/sort/autosize actions.
23
- * Uses positioned div with portal rendering.
5
+ * Thin wrapper over BaseColumnHeaderMenu portals into FluentProvider.
24
6
  */
25
- export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): React.ReactPortal | null;
7
+ export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,7 @@
1
1
  import * as React from 'react';
2
+ import type { BaseEmptyStateProps } from '@alaarab/ogrid-react';
2
3
  interface EmptyStateProps {
3
- emptyState: {
4
- render?: () => React.ReactNode;
5
- message?: React.ReactNode;
6
- hasActiveFilters?: boolean;
7
- onClearAll?: () => void;
8
- };
4
+ emptyState: BaseEmptyStateProps['emptyState'];
9
5
  }
10
6
  export declare function EmptyState({ emptyState }: EmptyStateProps): React.ReactElement;
11
7
  export {};
@@ -1,12 +1,4 @@
1
1
  import * as React from 'react';
2
- import type { IColumnDef } from '@alaarab/ogrid-react';
3
- export interface InlineCellEditorProps<T> {
4
- value: unknown;
5
- item: T;
6
- column: IColumnDef<T>;
7
- rowIndex: number;
8
- editorType: 'text' | 'select' | 'checkbox' | 'richSelect' | 'date';
9
- onCommit: (value: unknown) => void;
10
- onCancel: () => void;
11
- }
2
+ import type { InlineCellEditorProps } from '@alaarab/ogrid-react';
3
+ export type { InlineCellEditorProps } from '@alaarab/ogrid-react';
12
4
  export declare function InlineCellEditor<T>(props: InlineCellEditorProps<T>): React.ReactElement;
@@ -1,16 +1,4 @@
1
1
  import * as React from 'react';
2
- export interface StatusBarProps {
3
- totalCount: number;
4
- filteredCount?: number;
5
- selectedCount?: number;
6
- selectedCellCount?: number;
7
- aggregation?: {
8
- sum: number;
9
- avg: number;
10
- min: number;
11
- max: number;
12
- count: number;
13
- } | null;
14
- suppressRowCount?: boolean;
15
- }
2
+ import type { StatusBarProps as BaseStatusBarProps } from '@alaarab/ogrid-react';
3
+ export type StatusBarProps = Omit<BaseStatusBarProps, 'classNames'>;
16
4
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
@@ -1,5 +1,2 @@
1
- import * as React from 'react';
2
- import { type IOGridProps, type IOGridApi } from '@alaarab/ogrid-react';
3
1
  export type { IOGridProps } from '@alaarab/ogrid-react';
4
- declare const OGridInner: React.ForwardRefExoticComponent<IOGridProps<unknown> & React.RefAttributes<IOGridApi<unknown>>>;
5
- export declare const OGrid: typeof OGridInner;
2
+ export declare const OGrid: import("react").ForwardRefExoticComponent<import("@alaarab/ogrid-react").IOGridProps<unknown> & import("react").RefAttributes<import("@alaarab/ogrid-react").IOGridApi<unknown>>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-fluent",
3
- "version": "2.0.18",
3
+ "version": "2.0.21",
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",
@@ -40,7 +40,7 @@
40
40
  "node": ">=18"
41
41
  },
42
42
  "dependencies": {
43
- "@alaarab/ogrid-react": "2.0.18"
43
+ "@alaarab/ogrid-react": "2.0.21"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@fluentui/react-components": "^9.0.0",