@alaarab/ogrid-react-fluent 2.0.8 → 2.0.11

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.
@@ -44,6 +44,6 @@ export const ColumnHeaderFilter = React.memo((props) => {
44
44
  return null;
45
45
  };
46
46
  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)
47
- 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, children: [_jsxs("div", { className: styles.popoverHeader, children: ["Filter: ", columnName] }), renderPopoverContent()] }) })] }))] })] }));
47
+ 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] }), renderPopoverContent()] }) })] }))] })] }));
48
48
  });
49
49
  ColumnHeaderFilter.displayName = 'ColumnHeaderFilter';
@@ -158,6 +158,9 @@
158
158
  .popoverSearch .searchInput {
159
159
  width: 100%;
160
160
  }
161
+ .popoverSearch .searchInput :global .fui-Input__input {
162
+ border-width: 1px !important;
163
+ }
161
164
 
162
165
  .nativeInputWrapper {
163
166
  display: flex;
@@ -1,53 +1,28 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useRef, useEffect } from 'react';
3
- import { COLUMN_HEADER_MENU_ITEMS } from '@alaarab/ogrid-core';
4
- import { makeStyles, tokens } from '@fluentui/react-components';
5
- const useStyles = makeStyles({
6
- menu: {
7
- position: 'fixed',
8
- minWidth: '140px',
9
- backgroundColor: tokens.colorNeutralBackground1,
10
- borderRadius: tokens.borderRadiusMedium,
11
- padding: '4px',
12
- boxShadow: tokens.shadow16,
13
- zIndex: 100,
14
- },
15
- menuItem: {
16
- display: 'flex',
17
- alignItems: 'center',
18
- height: '28px',
19
- padding: '0 8px',
20
- fontSize: tokens.fontSizeBase200,
21
- color: tokens.colorNeutralForeground1,
22
- borderRadius: tokens.borderRadiusSmall,
23
- cursor: 'pointer',
24
- userSelect: 'none',
25
- outline: 'none',
26
- backgroundColor: 'transparent',
27
- border: 'none',
28
- width: '100%',
29
- textAlign: 'left',
30
- ':hover:not([disabled])': {
31
- backgroundColor: tokens.colorNeutralBackground1Hover,
32
- },
33
- ':active:not([disabled])': {
34
- backgroundColor: tokens.colorNeutralBackground1Pressed,
35
- },
36
- ':disabled': {
37
- color: tokens.colorNeutralForegroundDisabled,
38
- cursor: 'not-allowed',
39
- },
40
- },
41
- });
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useMemo, useEffect, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { getColumnHeaderMenuItems } from '@alaarab/ogrid-core';
5
+ import styles from './ColumnHeaderMenu.module.css';
6
+ /**
7
+ * Column header dropdown menu for pin/sort/autosize actions.
8
+ * Uses positioned div with portal rendering.
9
+ */
42
10
  export function ColumnHeaderMenu(props) {
43
- const { isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, canPinLeft, canPinRight, canUnpin } = props;
44
- const menuRef = useRef(null);
45
- const styles = useStyles();
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);
46
13
  useEffect(() => {
47
- if (!isOpen)
14
+ if (!isOpen || !anchorElement) {
15
+ setPosition(null);
48
16
  return;
17
+ }
18
+ const rect = anchorElement.getBoundingClientRect();
19
+ setPosition({
20
+ top: rect.bottom + 4,
21
+ left: rect.left,
22
+ });
49
23
  const handleClickOutside = (e) => {
50
- if (menuRef.current && !menuRef.current.contains(e.target)) {
24
+ const target = e.target;
25
+ if (anchorElement && !anchorElement.contains(target)) {
51
26
  onClose();
52
27
  }
53
28
  };
@@ -62,15 +37,35 @@ export function ColumnHeaderMenu(props) {
62
37
  document.removeEventListener('mousedown', handleClickOutside);
63
38
  document.removeEventListener('keydown', handleEscape);
64
39
  };
65
- }, [isOpen, onClose]);
66
- if (!isOpen || !anchorElement)
67
- return null;
68
- const rect = anchorElement.getBoundingClientRect();
69
- const menuStyle = {
70
- top: rect.bottom + 4,
71
- left: rect.left,
40
+ }, [isOpen, anchorElement, onClose]);
41
+ const menuInput = useMemo(() => ({
42
+ canPinLeft,
43
+ canPinRight,
44
+ canUnpin,
45
+ currentSort,
46
+ isSortable,
47
+ isResizable,
48
+ }), [canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]);
49
+ const items = useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
50
+ const handlers = {
51
+ pinLeft: onPinLeft,
52
+ pinRight: onPinRight,
53
+ unpin: onUnpin,
54
+ sortAsc: onSortAsc,
55
+ sortDesc: onSortDesc,
56
+ clearSort: onClearSort,
57
+ autosizeThis: onAutosizeThis,
58
+ autosizeAll: onAutosizeAll,
72
59
  };
73
- const handlers = [onPinLeft, onPinRight, onUnpin];
74
- const disabled = [!canPinLeft, !canPinRight, !canUnpin];
75
- return (_jsx("div", { ref: menuRef, className: styles.menu, style: menuStyle, children: COLUMN_HEADER_MENU_ITEMS.map((item, idx) => (_jsx("button", { className: styles.menuItem, onClick: handlers[idx], disabled: disabled[idx], children: item.label }, item.id))) }));
60
+ if (!isOpen || !position)
61
+ return null;
62
+ return createPortal(_jsx("div", { className: styles.content, style: {
63
+ position: 'fixed',
64
+ top: position.top,
65
+ left: position.left,
66
+ zIndex: 1000,
67
+ }, children: items.map((item, idx) => (_jsxs(React.Fragment, { children: [_jsx("button", { className: styles.item, disabled: item.disabled, onClick: () => {
68
+ handlers[item.id]();
69
+ onClose();
70
+ }, children: item.label }), item.divider && idx < items.length - 1 && (_jsx("div", { className: styles.separator }))] }, item.id))) }), document.body);
76
71
  }
@@ -0,0 +1,46 @@
1
+ .content {
2
+ min-width: 140px;
3
+ background: white;
4
+ border: 1px solid #e0e0e0;
5
+ border-radius: 6px;
6
+ padding: 4px;
7
+ box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
8
+ z-index: 1000;
9
+ animation-duration: 400ms;
10
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
11
+ will-change: transform, opacity;
12
+ }
13
+
14
+ .item {
15
+ font-size: 13px;
16
+ line-height: 1;
17
+ color: #111;
18
+ border-radius: 4px;
19
+ border: none;
20
+ background: transparent;
21
+ display: flex;
22
+ align-items: center;
23
+ height: 28px;
24
+ padding: 0 8px;
25
+ position: relative;
26
+ user-select: none;
27
+ outline: none;
28
+ cursor: pointer;
29
+ width: 100%;
30
+ text-align: left;
31
+ }
32
+ .item:disabled {
33
+ color: #999;
34
+ pointer-events: none;
35
+ cursor: not-allowed;
36
+ }
37
+ .item:hover:not(:disabled) {
38
+ background-color: #f5f5f5;
39
+ color: #111;
40
+ }
41
+
42
+ .separator {
43
+ height: 1px;
44
+ background-color: #e0e0e0;
45
+ margin: 4px 0;
46
+ }
@@ -309,11 +309,17 @@ function DataGridTableInner(props) {
309
309
  return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
310
310
  }
311
311
  return (_jsx("th", { rowSpan: headerRows.length - rowIdx, className: styles.leafHeaderCellSpan, scope: "col", children: cell.columnDef?.name }, cellIdx));
312
- })] }, `group-${rowIdx}`))), _jsx(DataGridRow, { children: ({ renderHeaderCell, columnId }) => (_jsx(DataGridHeaderCell, { className: headerClassMap[String(columnId)] || undefined, children: renderHeaderCell() })) })] }), _jsx(DataGridBody, { children: ({ item }) => {
312
+ })] }, `group-${rowIdx}`))), _jsx(DataGridRow, { children: ({ renderHeaderCell, columnId }) => {
313
+ const isSorted = props.sortBy === String(columnId);
314
+ const ariaSort = isSorted
315
+ ? (props.sortDirection === 'asc' ? 'ascending' : 'descending')
316
+ : undefined;
317
+ return (_jsx(DataGridHeaderCell, { className: headerClassMap[String(columnId)] || undefined, "aria-sort": ariaSort, children: renderHeaderCell() }));
318
+ } })] }), _jsx(DataGridBody, { children: ({ item }) => {
313
319
  const rowId = getRowId(item);
314
320
  const rowIndex = rowIndexByRowId.get(rowId) ?? -1;
315
321
  return (_jsx(GridRow, { item: item, rowId: rowId, rowIndex: rowIndex, 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));
316
- } })] }), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx("div", { style: { height: visibleRange.offsetBottom }, "aria-hidden": true })), isReorderDragging && dropIndicatorX != null && (_jsx("div", { className: styles.dropIndicator, style: { left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0) } })), _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 &&
317
- 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), createPortal(_jsx(ColumnHeaderMenu, { isOpen: pinning.headerMenu.isOpen, anchorElement: pinning.headerMenu.anchorElement, onClose: pinning.headerMenu.close, onPinLeft: pinning.headerMenu.handlePinLeft, onPinRight: pinning.headerMenu.handlePinRight, onUnpin: pinning.headerMenu.handleUnpin, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin }), 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 })] }) }))] }));
322
+ } })] }), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx("div", { style: { height: visibleRange.offsetBottom }, "aria-hidden": true })), isReorderDragging && dropIndicatorX != null && (_jsx("div", { className: styles.dropIndicator, style: { left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0) } })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset, items: items, visibleColumns: visibleColumns, columnSizingOverrides: columnSizingOverrides, columnOrder: columnOrder }), 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 &&
323
+ 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), createPortal(_jsx(ColumnHeaderMenu, { isOpen: pinning.headerMenu.isOpen, anchorElement: pinning.headerMenu.anchorElement, onClose: pinning.headerMenu.close, onPinLeft: pinning.headerMenu.handlePinLeft, onPinRight: pinning.headerMenu.handlePinRight, onUnpin: pinning.headerMenu.handleUnpin, onSortAsc: pinning.headerMenu.handleSortAsc, onSortDesc: pinning.headerMenu.handleSortDesc, onClearSort: pinning.headerMenu.handleClearSort, onAutosizeThis: pinning.headerMenu.handleAutosizeThis, onAutosizeAll: pinning.headerMenu.handleAutosizeAll, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin, currentSort: pinning.headerMenu.currentSort, isSortable: pinning.headerMenu.isSortable, isResizable: pinning.headerMenu.isResizable }), 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 })] }) }))] }));
318
324
  }
319
325
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -103,8 +103,10 @@
103
103
  .tableWrapper[data-column-count] :global .fui-DataGridHeaderCell {
104
104
  min-width: var(--data-table-cell-min-width, 80px);
105
105
  white-space: nowrap !important;
106
- position: relative;
106
+ position: sticky; /* Changed from relative - enables vertical sticky for all headers */
107
+ top: 0; /* Sticky vertically */
107
108
  background-color: var(--colorSubtleBackgroundSelected, #f3f2f1);
109
+ z-index: 8; /* Stack above body cells */
108
110
  font-size: 14px;
109
111
  border-right: 1px solid var(--colorNeutralStroke1, #c4c4c4);
110
112
  }
@@ -368,8 +370,8 @@
368
370
 
369
371
  /* Freeze panes: sticky header when freezeRows >= 1 */
370
372
  .stickyHeader {
371
- position: sticky;
372
- top: 0;
373
+ /* Removed position: sticky; top: 0; - breaks horizontal sticky on pinned columns.
374
+ Instead, apply sticky to individual header cells (.fui-DataGridHeaderCell). */
373
375
  z-index: 8;
374
376
  background-color: var(--colorSubtleBackgroundSelected, #f3f2f1);
375
377
  }
@@ -394,7 +396,7 @@
394
396
  .stickyHeader .pinnedLeft,
395
397
  .stickyHeader .pinnedRight {
396
398
  top: 0;
397
- z-index: 9;
399
+ z-index: 10; /* Increased from 9 to stack above base header cells (z-index: 8) */
398
400
  }
399
401
 
400
402
  .activeRow :global .fui-DataGridCell:not(:has(.activeCellContent)) {
@@ -656,8 +658,8 @@
656
658
  font-size: 16px;
657
659
  line-height: 1;
658
660
  color: var(--colorNeutralForeground3, #666);
659
- opacity: 0;
660
- transition: opacity 0.15s, background-color 0.15s;
661
+ opacity: 1;
662
+ transition: background-color 0.15s;
661
663
  border-radius: 3px;
662
664
  display: flex;
663
665
  align-items: center;
@@ -667,16 +669,11 @@
667
669
  }
668
670
  .headerMenuTrigger:hover {
669
671
  background: var(--colorNeutralBackground1Hover, #f3f2f1);
670
- opacity: 1;
671
672
  }
672
673
  .headerMenuTrigger:active {
673
674
  background: var(--colorNeutralBackground1Pressed, #e1dfdd);
674
675
  }
675
676
 
676
- .tableWrapper :global(.fui-DataGridHeaderCell:hover) .headerMenuTrigger {
677
- opacity: 1;
678
- }
679
-
680
677
  .tableWrapper :global(.fui-DataGridHeaderCell.pinnedColLeft),
681
678
  .tableWrapper :global(.fui-DataGridCell.pinnedColLeft) {
682
679
  border-left: 2px solid var(--colorBrandForeground1, #0078d4) !important;
@@ -701,4 +698,27 @@
701
698
  }
702
699
  .density-comfortable .cellContent {
703
700
  padding: 12px 16px;
701
+ }
702
+
703
+ /* ─── Accessibility: Focus Visible Styles ─────────────────────── */
704
+ .tableWrapper :global .fui-DataGridHeaderCell:focus-visible,
705
+ .tableWrapper :global .fui-DataGridCell:focus-visible {
706
+ outline: 2px solid var(--colorBrandStroke1, #0078d4);
707
+ outline-offset: -2px;
708
+ z-index: 11;
709
+ }
710
+ .tableWrapper :global .fui-Button:focus-visible,
711
+ .tableWrapper :global .fui-MenuButton:focus-visible {
712
+ outline: 2px solid var(--colorBrandStroke1, #0078d4);
713
+ outline-offset: 2px;
714
+ }
715
+ .tableWrapper :global .fui-Checkbox:focus-visible {
716
+ outline: 2px solid var(--colorBrandStroke1, #0078d4);
717
+ outline-offset: 2px;
718
+ }
719
+
720
+ .cellContent:focus-visible {
721
+ outline: 2px solid var(--colorBrandStroke1, #0078d4);
722
+ outline-offset: -2px;
723
+ z-index: 3;
704
724
  }
@@ -1,20 +1,26 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
- import { useMemo, useCallback } from 'react';
4
3
  import { Button, Select } from '@fluentui/react-components';
5
4
  import { ChevronLeftRegular, ChevronRightRegular, ChevronDoubleLeftRegular, ChevronDoubleRightRegular, } from '@fluentui/react-icons';
6
- import { getPaginationViewModel } from '@alaarab/ogrid-react';
5
+ import { usePaginationControls } from '@alaarab/ogrid-react';
7
6
  import styles from './PaginationControls.module.css';
8
7
  export const PaginationControls = React.memo((props) => {
9
8
  const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions, entityLabelPlural, className } = props;
10
- const labelPlural = entityLabelPlural ?? 'items';
11
- const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined), [currentPage, pageSize, totalCount, pageSizeOptions]);
12
- const handlePageSizeChange = useCallback((_e, data) => {
13
- onPageSizeChange(Number(data.value));
14
- }, [onPageSizeChange]);
9
+ const { labelPlural, vm, handlePageSizeChange } = usePaginationControls({
10
+ currentPage,
11
+ pageSize,
12
+ totalCount,
13
+ onPageChange,
14
+ onPageSizeChange,
15
+ pageSizeOptions,
16
+ entityLabelPlural,
17
+ });
18
+ const handlePageSizeChangeEvent = (_e, data) => {
19
+ handlePageSizeChange(Number(data.value));
20
+ };
15
21
  if (!vm) {
16
22
  return null;
17
23
  }
18
24
  const { pageNumbers, showStartEllipsis, showEndEllipsis, totalPages, startItem, endItem } = vm;
19
- return (_jsxs("div", { className: `${styles.pagination} ${className || ''}`, role: "navigation", "aria-label": "Pagination", children: [_jsxs("div", { className: styles.paginationInfo, children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs("div", { className: styles.paginationControls, children: [_jsx(Button, { appearance: "outline", shape: "circular", size: "small", icon: _jsx(ChevronDoubleLeftRegular, {}), onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", className: styles.navBtn }), _jsx(Button, { appearance: "outline", shape: "circular", size: "small", icon: _jsx(ChevronLeftRegular, {}), onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", className: styles.navBtn }), _jsxs("div", { className: styles.pageNumbers, children: [showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx(Button, { appearance: "outline", size: "small", shape: "rounded", onClick: () => onPageChange(1), "aria-label": "Page 1", className: styles.pageBtn, children: "1" }), _jsx("span", { className: styles.ellipsis, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx(Button, { appearance: currentPage === pageNum ? 'primary' : 'outline', size: "small", shape: "rounded", onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, className: styles.pageBtn, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.ellipsis, "aria-hidden": true, children: "\u2026" }), _jsx(Button, { appearance: "outline", size: "small", shape: "rounded", onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, className: styles.pageBtn, children: totalPages })] }))] }), _jsx(Button, { appearance: "outline", shape: "circular", size: "small", icon: _jsx(ChevronRightRegular, {}), onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", className: styles.navBtn }), _jsx(Button, { appearance: "outline", shape: "circular", size: "small", icon: _jsx(ChevronDoubleRightRegular, {}), onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", className: styles.navBtn })] }), _jsxs("div", { className: styles.pageSizeSelector, children: [_jsx("span", { className: styles.pageSizeLabel, children: "Rows" }), _jsx(Select, { value: String(pageSize), onChange: handlePageSizeChange, size: "small", appearance: "outline", "aria-label": "Rows per page", className: styles.pageSizeSelect, children: vm.pageSizeOptions.map((n) => (_jsx("option", { value: n, children: n }, n))) })] })] }));
25
+ return (_jsxs("div", { className: `${styles.pagination} ${className || ''}`, role: "navigation", "aria-label": "Pagination", children: [_jsxs("div", { className: styles.paginationInfo, children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs("div", { className: styles.paginationControls, children: [_jsx(Button, { appearance: "outline", shape: "circular", size: "small", icon: _jsx(ChevronDoubleLeftRegular, {}), onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", className: styles.navBtn }), _jsx(Button, { appearance: "outline", shape: "circular", size: "small", icon: _jsx(ChevronLeftRegular, {}), onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", className: styles.navBtn }), _jsxs("div", { className: styles.pageNumbers, children: [showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx(Button, { appearance: "outline", size: "small", shape: "rounded", onClick: () => onPageChange(1), "aria-label": "Page 1", className: styles.pageBtn, children: "1" }), _jsx("span", { className: styles.ellipsis, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx(Button, { appearance: currentPage === pageNum ? 'primary' : 'outline', size: "small", shape: "rounded", onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, className: styles.pageBtn, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.ellipsis, "aria-hidden": true, children: "\u2026" }), _jsx(Button, { appearance: "outline", size: "small", shape: "rounded", onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, className: styles.pageBtn, children: totalPages })] }))] }), _jsx(Button, { appearance: "outline", shape: "circular", size: "small", icon: _jsx(ChevronRightRegular, {}), onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", className: styles.navBtn }), _jsx(Button, { appearance: "outline", shape: "circular", size: "small", icon: _jsx(ChevronDoubleRightRegular, {}), onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", className: styles.navBtn })] }), _jsxs("div", { className: styles.pageSizeSelector, children: [_jsx("span", { className: styles.pageSizeLabel, children: "Rows" }), _jsx(Select, { value: String(pageSize), onChange: handlePageSizeChangeEvent, size: "small", appearance: "outline", "aria-label": "Rows per page", className: styles.pageSizeSelect, children: vm.pageSizeOptions.map((n) => (_jsx("option", { value: n, children: n }, n))) })] })] }));
20
26
  });
@@ -1,4 +1,4 @@
1
- import * as React from 'react';
1
+ import React from 'react';
2
2
  export interface ColumnHeaderMenuProps {
3
3
  isOpen: boolean;
4
4
  anchorElement: HTMLElement | null;
@@ -6,8 +6,20 @@ export interface ColumnHeaderMenuProps {
6
6
  onPinLeft: () => void;
7
7
  onPinRight: () => void;
8
8
  onUnpin: () => void;
9
+ onSortAsc: () => void;
10
+ onSortDesc: () => void;
11
+ onClearSort: () => void;
12
+ onAutosizeThis: () => void;
13
+ onAutosizeAll: () => void;
9
14
  canPinLeft: boolean;
10
15
  canPinRight: boolean;
11
16
  canUnpin: boolean;
17
+ currentSort: 'asc' | 'desc' | null;
18
+ isSortable: boolean;
19
+ isResizable: boolean;
12
20
  }
13
- export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): React.ReactElement | null;
21
+ /**
22
+ * Column header dropdown menu for pin/sort/autosize actions.
23
+ * Uses positioned div with portal rendering.
24
+ */
25
+ export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): React.ReactPortal | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-fluent",
3
- "version": "2.0.8",
3
+ "version": "2.0.11",
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",
@@ -40,7 +40,7 @@
40
40
  "node": ">=18"
41
41
  },
42
42
  "dependencies": {
43
- "@alaarab/ogrid-react": "2.0.7"
43
+ "@alaarab/ogrid-react": "2.0.11"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@fluentui/react-components": "^9.0.0",
@@ -55,8 +55,8 @@
55
55
  "@testing-library/jest-dom": "^6.9.1",
56
56
  "@testing-library/react": "^16.3.2",
57
57
  "@testing-library/user-event": "^14.6.1",
58
- "@types/react": "^18.3.18",
59
- "@types/react-dom": "^18.3.5",
58
+ "@types/react": "^19.0.0",
59
+ "@types/react-dom": "^19.0.0",
60
60
  "eslint-plugin-storybook": "10.2.8",
61
61
  "react": "^18.3.1",
62
62
  "react-dom": "^18.3.1",