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