@alaarab/ogrid-react 2.0.9 → 2.0.12

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 (33) hide show
  1. package/dist/esm/components/BaseInlineCellEditor.js +2 -2
  2. package/dist/esm/components/CellErrorBoundary.js +1 -1
  3. package/dist/esm/components/ColumnChooserProps.js +6 -0
  4. package/dist/esm/components/ColumnHeaderFilterContent.js +35 -0
  5. package/dist/esm/components/ColumnHeaderFilterRenderers.js +67 -0
  6. package/dist/esm/components/MarchingAntsOverlay.js +25 -44
  7. package/dist/esm/components/PaginationControlsProps.js +6 -0
  8. package/dist/esm/constants/domHelpers.js +16 -0
  9. package/dist/esm/hooks/index.js +2 -0
  10. package/dist/esm/hooks/useColumnHeaderMenuState.js +52 -2
  11. package/dist/esm/hooks/useColumnResize.js +29 -0
  12. package/dist/esm/hooks/useDataGridState.js +77 -10
  13. package/dist/esm/hooks/useDataGridTableOrchestration.js +200 -0
  14. package/dist/esm/hooks/useDebounce.js +1 -1
  15. package/dist/esm/hooks/useOGrid.js +3 -6
  16. package/dist/esm/hooks/usePaginationControls.js +19 -0
  17. package/dist/esm/index.js +6 -1
  18. package/dist/esm/utils/index.js +1 -1
  19. package/dist/types/components/ColumnChooserProps.d.ts +12 -0
  20. package/dist/types/components/ColumnHeaderFilterContent.d.ts +62 -0
  21. package/dist/types/components/ColumnHeaderFilterRenderers.d.ts +71 -0
  22. package/dist/types/components/MarchingAntsOverlay.d.ts +11 -1
  23. package/dist/types/components/PaginationControlsProps.d.ts +15 -0
  24. package/dist/types/constants/domHelpers.d.ts +17 -0
  25. package/dist/types/hooks/index.d.ts +5 -1
  26. package/dist/types/hooks/useColumnHeaderMenuState.d.ts +23 -1
  27. package/dist/types/hooks/useDataGridState.d.ts +10 -6
  28. package/dist/types/hooks/useDataGridTableOrchestration.d.ts +131 -0
  29. package/dist/types/hooks/usePaginationControls.d.ts +20 -0
  30. package/dist/types/index.d.ts +9 -2
  31. package/dist/types/types/dataGridTypes.d.ts +1 -1
  32. package/dist/types/utils/index.d.ts +1 -1
  33. package/package.json +4 -4
@@ -35,9 +35,9 @@ export const richSelectDropdownStyle = {
35
35
  maxHeight: 200,
36
36
  overflowY: 'auto',
37
37
  background: 'var(--ogrid-bg, #fff)',
38
- border: '1px solid var(--ogrid-border, #ccc)',
38
+ border: '1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))',
39
39
  zIndex: 10,
40
- boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
40
+ boxShadow: 'var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))',
41
41
  };
42
42
  export const richSelectOptionStyle = {
43
43
  padding: '6px 8px',
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
3
  const DEFAULT_FALLBACK_STYLE = {
4
- color: '#d32f2f',
4
+ color: 'var(--ogrid-error, #d32f2f)',
5
5
  fontSize: '0.75rem',
6
6
  padding: '2px 4px',
7
7
  };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared props interface for ColumnChooser across all React UI packages.
3
+ * Each UI package renders its own framework-specific trigger, popover, and checkboxes
4
+ * but shares this common prop shape.
5
+ */
6
+ export {};
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ export const DateFilterContent = ({ tempDateFrom, setTempDateFrom, tempDateTo, setTempDateTo, onApply, onClear, classNames, }) => (_jsxs(_Fragment, { children: [_jsxs("div", { style: { padding: '8px 12px', display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }, children: ["From:", _jsx("input", { type: "date", value: tempDateFrom, onChange: (e) => setTempDateFrom(e.target.value), style: { flex: 1 } })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }, children: ["To:", _jsx("input", { type: "date", value: tempDateTo, onChange: (e) => setTempDateTo(e.target.value), style: { flex: 1 } })] })] }), _jsxs("div", { className: classNames?.popoverActions, children: [_jsx("button", { className: classNames?.clearButton, onClick: onClear, disabled: !tempDateFrom && !tempDateTo, children: "Clear" }), _jsx("button", { className: classNames?.applyButton, onClick: onApply, children: "Apply" })] })] }));
3
+ DateFilterContent.displayName = 'DateFilterContent';
4
+ // ---- Utility to extract useColumnHeaderFilterState params from props ----
5
+ export function getColumnHeaderFilterStateParams(props) {
6
+ return {
7
+ filterType: props.filterType,
8
+ isSorted: props.isSorted ?? false,
9
+ isSortedDescending: props.isSortedDescending ?? false,
10
+ onSort: props.onSort,
11
+ selectedValues: props.selectedValues,
12
+ onFilterChange: props.onFilterChange,
13
+ options: props.options,
14
+ isLoadingOptions: props.isLoadingOptions ?? false,
15
+ textValue: props.textValue ?? '',
16
+ onTextChange: props.onTextChange,
17
+ selectedUser: props.selectedUser,
18
+ onUserChange: props.onUserChange,
19
+ peopleSearch: props.peopleSearch,
20
+ dateValue: props.dateValue,
21
+ onDateChange: props.onDateChange,
22
+ };
23
+ }
24
+ // ---- Helper to build date filter props from state ----
25
+ export function getDateFilterContentProps(state, classNames) {
26
+ return {
27
+ tempDateFrom: state.tempDateFrom,
28
+ setTempDateFrom: state.setTempDateFrom,
29
+ tempDateTo: state.tempDateTo,
30
+ setTempDateTo: state.setTempDateTo,
31
+ onApply: state.handlers.handleDateApply,
32
+ onClear: state.handlers.handleDateClear,
33
+ classNames,
34
+ };
35
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared filter content dispatching for ColumnHeaderFilter across all React UI packages.
3
+ *
4
+ * Each UI package provides framework-specific sub-filter components (TextFilterPopover,
5
+ * MultiSelectFilterPopover, PeopleFilterPopover, date content). This utility dispatches
6
+ * to the correct renderer based on filterType, eliminating the duplicated if/switch chain
7
+ * that was previously in each UI package's ColumnHeaderFilter component.
8
+ */
9
+ /**
10
+ * Dispatches to the appropriate filter content renderer based on filterType.
11
+ * Eliminates the duplicated if/switch chain in each UI package's ColumnHeaderFilter.
12
+ *
13
+ * @param filterType - The column's filter type
14
+ * @param state - The result from useColumnHeaderFilterState
15
+ * @param options - The filter options array (for multiSelect)
16
+ * @param isLoadingOptions - Whether options are loading
17
+ * @param selectedUser - The currently selected user (for people filter)
18
+ * @param renderers - Framework-specific renderer functions
19
+ * @returns The rendered filter content, or null for unsupported filter types
20
+ */
21
+ export function renderFilterContent(filterType, state, options, isLoadingOptions, selectedUser, renderers) {
22
+ if (filterType === 'multiSelect') {
23
+ return renderers.renderMultiSelect({
24
+ searchText: state.searchText,
25
+ onSearchChange: state.setSearchText,
26
+ options,
27
+ filteredOptions: state.filteredOptions,
28
+ selected: state.tempSelected,
29
+ onOptionToggle: state.handlers.handleCheckboxChange,
30
+ onSelectAll: state.handlers.handleSelectAll,
31
+ onClearSelection: state.handlers.handleClearSelection,
32
+ onApply: state.handlers.handleApplyMultiSelect,
33
+ isLoading: isLoadingOptions,
34
+ });
35
+ }
36
+ if (filterType === 'text') {
37
+ return renderers.renderText({
38
+ value: state.tempTextValue,
39
+ onValueChange: state.setTempTextValue,
40
+ onApply: state.handlers.handleTextApply,
41
+ onClear: state.handlers.handleTextClear,
42
+ });
43
+ }
44
+ if (filterType === 'people') {
45
+ return renderers.renderPeople({
46
+ selectedUser,
47
+ searchText: state.peopleSearchText,
48
+ onSearchChange: state.setPeopleSearchText,
49
+ suggestions: state.peopleSuggestions,
50
+ isLoading: state.isPeopleLoading,
51
+ onUserSelect: state.handlers.handleUserSelect,
52
+ onClearUser: state.handlers.handleClearUser,
53
+ inputRef: state.peopleInputRef,
54
+ });
55
+ }
56
+ if (filterType === 'date') {
57
+ return renderers.renderDate({
58
+ tempDateFrom: state.tempDateFrom,
59
+ setTempDateFrom: state.setTempDateFrom,
60
+ tempDateTo: state.tempDateTo,
61
+ setTempDateTo: state.setTempDateTo,
62
+ onApply: state.handlers.handleDateApply,
63
+ onClear: state.handlers.handleDateClear,
64
+ });
65
+ }
66
+ return null;
67
+ }
@@ -8,38 +8,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
8
8
  * Uses SVG rects positioned via cell data-attribute measurements.
9
9
  */
10
10
  import { useEffect, useRef, useState, useCallback } from 'react';
11
+ import { measureRange, injectGlobalStyles } from '@alaarab/ogrid-core';
11
12
  const MARCHING_ANTS_ANIMATION = { animation: 'ogrid-marching-ants 0.5s linear infinite' };
12
- // Inject the @keyframes rule once into <head> (deduplicates across multiple OGrid instances / module copies)
13
- function ensureKeyframes() {
14
- if (typeof document === 'undefined')
15
- return;
16
- if (document.getElementById('ogrid-marching-ants-keyframes'))
17
- return;
18
- const style = document.createElement('style');
19
- style.id = 'ogrid-marching-ants-keyframes';
20
- style.textContent =
21
- '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}';
22
- document.head.appendChild(style);
23
- }
24
- /** Measure the bounding rect of a range within a container. */
25
- function measureRange(container, range, colOffset) {
26
- const startGlobalCol = range.startCol + colOffset;
27
- const endGlobalCol = range.endCol + colOffset;
28
- const topLeft = container.querySelector(`[data-row-index="${range.startRow}"][data-col-index="${startGlobalCol}"]`);
29
- const bottomRight = container.querySelector(`[data-row-index="${range.endRow}"][data-col-index="${endGlobalCol}"]`);
30
- if (!topLeft || !bottomRight)
31
- return null;
32
- const cRect = container.getBoundingClientRect();
33
- const tlRect = topLeft.getBoundingClientRect();
34
- const brRect = bottomRight.getBoundingClientRect();
35
- return {
36
- top: tlRect.top - cRect.top,
37
- left: tlRect.left - cRect.left,
38
- width: brRect.right - tlRect.left,
39
- height: brRect.bottom - tlRect.top,
40
- };
41
- }
42
- export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, cutRange, colOffset, }) {
13
+ export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, cutRange, colOffset, items, visibleColumns, columnSizingOverrides, columnOrder, }) {
43
14
  const [selRect, setSelRect] = useState(null);
44
15
  const [clipRect, setClipRect] = useState(null);
45
16
  const rafRef = useRef(0);
@@ -53,10 +24,11 @@ export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, c
53
24
  }
54
25
  setSelRect(selectionRange ? measureRange(container, selectionRange, colOffset) : null);
55
26
  setClipRect(clipRange ? measureRange(container, clipRange, colOffset) : null);
56
- }, [selectionRange, clipRange, containerRef, colOffset]);
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ }, [selectionRange, clipRange, containerRef, colOffset, items, visibleColumns, columnSizingOverrides, columnOrder]);
57
29
  // Inject keyframes on mount
58
30
  useEffect(() => {
59
- ensureKeyframes();
31
+ injectGlobalStyles('ogrid-marching-ants-keyframes', '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}');
60
32
  }, []);
61
33
  // Measure when any range changes; re-measure on resize
62
34
  useEffect(() => {
@@ -88,23 +60,32 @@ export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, c
88
60
  selectionRange.startCol === clipRange.startCol &&
89
61
  selectionRange.endRow === clipRange.endRow &&
90
62
  selectionRange.endCol === clipRange.endCol;
91
- return (_jsxs(_Fragment, { children: [selRect && !clipRangeMatchesSel && (_jsx("svg", { style: {
63
+ // Round to integer pixels so the stroke aligns to the pixel grid and corners connect cleanly
64
+ const roundRect = (r) => ({
65
+ top: Math.round(r.top),
66
+ left: Math.round(r.left),
67
+ width: Math.round(r.width),
68
+ height: Math.round(r.height),
69
+ });
70
+ const selR = selRect ? roundRect(selRect) : null;
71
+ const clipR = clipRect ? roundRect(clipRect) : null;
72
+ return (_jsxs(_Fragment, { children: [selR && !clipRangeMatchesSel && (_jsx("svg", { style: {
92
73
  position: 'absolute',
93
- top: selRect.top,
94
- left: selRect.left,
95
- width: selRect.width,
96
- height: selRect.height,
74
+ top: selR.top,
75
+ left: selR.left,
76
+ width: selR.width,
77
+ height: selR.height,
97
78
  pointerEvents: 'none',
98
79
  zIndex: 4,
99
80
  overflow: 'visible',
100
- }, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, selRect.width - 2), height: Math.max(0, selRect.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2" }) })), clipRect && (_jsx("svg", { style: {
81
+ }, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, selR.width - 2), height: Math.max(0, selR.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2", style: { shapeRendering: 'crispEdges' } }) })), clipR && (_jsx("svg", { style: {
101
82
  position: 'absolute',
102
- top: clipRect.top,
103
- left: clipRect.left,
104
- width: clipRect.width,
105
- height: clipRect.height,
83
+ top: clipR.top,
84
+ left: clipR.left,
85
+ width: clipR.width,
86
+ height: clipR.height,
106
87
  pointerEvents: 'none',
107
88
  zIndex: 5,
108
89
  overflow: 'visible',
109
- }, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, clipRect.width - 2), height: Math.max(0, clipRect.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2", strokeDasharray: "4 4", style: MARCHING_ANTS_ANIMATION }) }))] }));
90
+ }, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, clipR.width - 2), height: Math.max(0, clipR.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2", strokeDasharray: "4 4", style: { ...MARCHING_ANTS_ANIMATION, shapeRendering: 'crispEdges' } }) }))] }));
110
91
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared props interface for PaginationControls across all React UI packages.
3
+ * Each UI package renders its own framework-specific buttons, selects, and layout
4
+ * but shares this common prop shape.
5
+ */
6
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Module-scope stable constants shared across all React UI DataGridTable implementations.
3
+ * Avoid per-render allocations by keeping these at module scope.
4
+ */
5
+ /** Root container style for the DataGridTable (flex column layout). */
6
+ export const GRID_ROOT_STYLE = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
7
+ /** Applied to cells that support editing — shows the cell cursor. */
8
+ export const CURSOR_CELL_STYLE = { cursor: 'cell' };
9
+ /** Minimum size for popover anchor elements. */
10
+ export const POPOVER_ANCHOR_STYLE = { minHeight: '100%', minWidth: 40 };
11
+ /** Prevents the default browser action for mouse events. */
12
+ export const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
13
+ /** No-operation function. */
14
+ export const NOOP = () => { };
15
+ /** Stops event propagation (e.g. click on checkbox inside a row). */
16
+ export const STOP_PROPAGATION = (e) => { e.stopPropagation(); };
@@ -27,3 +27,5 @@ export { useTableLayout } from './useTableLayout';
27
27
  export { useColumnReorder } from './useColumnReorder';
28
28
  export { useVirtualScroll } from './useVirtualScroll';
29
29
  export { useLatestRef } from './useLatestRef';
30
+ export { usePaginationControls } from './usePaginationControls';
31
+ export { useDataGridTableOrchestration } from './useDataGridTableOrchestration';
@@ -1,10 +1,11 @@
1
1
  import { useState, useCallback } from 'react';
2
+ import { measureColumnContentWidth } from '../utils';
2
3
  /**
3
- * Manages state for the column header menu (pin left/right/unpin actions).
4
+ * Manages state for the column header menu (pin, sort, autosize actions).
4
5
  * Tracks which column's menu is open, anchor element, and action handlers.
5
6
  */
6
7
  export function useColumnHeaderMenuState(params) {
7
- const { pinnedColumns, onPinColumn, onUnpinColumn } = params;
8
+ const { pinnedColumns, onPinColumn, onUnpinColumn, sortBy, sortDirection, onColumnSort, onColumnResized, onAutosizeColumn, columns, data: _data, getRowId: _getRowId, } = params;
8
9
  const [isOpen, setIsOpen] = useState(false);
9
10
  const [openForColumn, setOpenForColumn] = useState(null);
10
11
  const [anchorElement, setAnchorElement] = useState(null);
@@ -22,6 +23,10 @@ export function useColumnHeaderMenuState(params) {
22
23
  const canPinLeft = currentPinState !== 'left';
23
24
  const canPinRight = currentPinState !== 'right';
24
25
  const canUnpin = !!currentPinState;
26
+ const currentColumn = columns.find((c) => c.columnId === openForColumn);
27
+ const currentSort = openForColumn === sortBy ? sortDirection : null;
28
+ const isSortable = currentColumn?.sortable !== false;
29
+ const isResizable = currentColumn?.resizable !== false;
25
30
  const handlePinLeft = useCallback(() => {
26
31
  if (openForColumn && canPinLeft) {
27
32
  onPinColumn(openForColumn, 'left');
@@ -40,6 +45,43 @@ export function useColumnHeaderMenuState(params) {
40
45
  close();
41
46
  }
42
47
  }, [openForColumn, canUnpin, onUnpinColumn, close]);
48
+ const handleSortAsc = useCallback(() => {
49
+ if (openForColumn && isSortable) {
50
+ onColumnSort(openForColumn, 'asc');
51
+ close();
52
+ }
53
+ }, [openForColumn, isSortable, onColumnSort, close]);
54
+ const handleSortDesc = useCallback(() => {
55
+ if (openForColumn && isSortable) {
56
+ onColumnSort(openForColumn, 'desc');
57
+ close();
58
+ }
59
+ }, [openForColumn, isSortable, onColumnSort, close]);
60
+ const handleClearSort = useCallback(() => {
61
+ if (openForColumn && isSortable) {
62
+ onColumnSort(openForColumn, null);
63
+ close();
64
+ }
65
+ }, [openForColumn, isSortable, onColumnSort, close]);
66
+ const handleAutosizeThis = useCallback(() => {
67
+ const resizer = onAutosizeColumn ?? onColumnResized;
68
+ if (!openForColumn || !resizer || !isResizable)
69
+ return;
70
+ const col = columns.find((c) => c.columnId === openForColumn);
71
+ resizer(openForColumn, measureColumnContentWidth(openForColumn, col?.minWidth));
72
+ close();
73
+ }, [openForColumn, onAutosizeColumn, onColumnResized, isResizable, columns, close]);
74
+ const handleAutosizeAll = useCallback(() => {
75
+ const resizer = onAutosizeColumn ?? onColumnResized;
76
+ if (!resizer)
77
+ return;
78
+ columns.forEach((col) => {
79
+ if (col.resizable === false)
80
+ return;
81
+ resizer(col.columnId, measureColumnContentWidth(col.columnId, col.minWidth));
82
+ });
83
+ close();
84
+ }, [columns, onAutosizeColumn, onColumnResized, close]);
43
85
  return {
44
86
  isOpen,
45
87
  openForColumn,
@@ -49,8 +91,16 @@ export function useColumnHeaderMenuState(params) {
49
91
  handlePinLeft,
50
92
  handlePinRight,
51
93
  handleUnpin,
94
+ handleSortAsc,
95
+ handleSortDesc,
96
+ handleClearSort,
97
+ handleAutosizeThis,
98
+ handleAutosizeAll,
52
99
  canPinLeft,
53
100
  canPinRight,
54
101
  canUnpin,
102
+ currentSort,
103
+ isSortable,
104
+ isResizable,
55
105
  };
56
106
  }
@@ -36,6 +36,26 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
36
36
  ?? col.defaultWidth
37
37
  ?? defaultWidth;
38
38
  let latestWidth = startWidth;
39
+ // Lock all column widths to their current DOM widths on first resize.
40
+ // With table-layout:auto, resizing one column causes the browser to compress others.
41
+ // Snapshotting all widths prevents this — only the dragged column changes.
42
+ const thead = thEl?.closest('thead');
43
+ if (thead) {
44
+ const allThs = thead.querySelectorAll('th[data-column-id]');
45
+ if (allThs.length > 0) {
46
+ setColumnSizingOverrides((prev) => {
47
+ const next = { ...prev };
48
+ allThs.forEach((th) => {
49
+ const colId = th.dataset.columnId;
50
+ if (colId && !next[colId]) {
51
+ next[colId] = { widthPx: th.getBoundingClientRect().width };
52
+ }
53
+ });
54
+ next[columnId] = { widthPx: startWidth };
55
+ return next;
56
+ });
57
+ }
58
+ }
39
59
  // Lock cursor and prevent text selection during drag
40
60
  const prevCursor = document.body.style.cursor;
41
61
  const prevUserSelect = document.body.style.userSelect;
@@ -73,6 +93,15 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
73
93
  const onUp = () => {
74
94
  cleanup();
75
95
  flushWidth();
96
+ // Remove any rogue :focus-visible outlines that appeared during the drag.
97
+ // Re-focus the grid wrapper so keyboard navigation still works.
98
+ const wrapper = thEl?.closest('[tabindex]');
99
+ if (wrapper) {
100
+ wrapper.focus({ preventScroll: true });
101
+ }
102
+ else if (document.activeElement instanceof HTMLElement) {
103
+ document.activeElement.blur();
104
+ }
76
105
  if (onColumnResizedRef.current) {
77
106
  onColumnResizedRef.current(columnId, latestWidth);
78
107
  }
@@ -1,5 +1,6 @@
1
- import { useMemo, useCallback, useState } from 'react';
1
+ import { useMemo, useCallback, useState, useLayoutEffect } from 'react';
2
2
  import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations } from '../utils';
3
+ import { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
3
4
  import { useRowSelection } from './useRowSelection';
4
5
  import { useCellEditing } from './useCellEditing';
5
6
  import { useActiveCell } from './useActiveCell';
@@ -150,11 +151,48 @@ export function useDataGridState(params) {
150
151
  pinnedColumns,
151
152
  onColumnPinned,
152
153
  });
153
- const headerMenuResult = useColumnHeaderMenuState({
154
- pinnedColumns: pinningResult.pinnedColumns,
155
- onPinColumn: pinningResult.pinColumn,
156
- onUnpinColumn: pinningResult.unpinColumn,
157
- });
154
+ // Measure actual column widths from the DOM for accurate pinning offsets.
155
+ // With table-layout: auto, rendered widths can exceed declared minimums.
156
+ const [measuredColumnWidths, setMeasuredColumnWidths] = useState({});
157
+ useLayoutEffect(() => {
158
+ const wrapper = wrapperRef.current;
159
+ if (!wrapper)
160
+ return;
161
+ const headerCells = wrapper.querySelectorAll('th[data-column-id]');
162
+ if (headerCells.length === 0)
163
+ return;
164
+ const measured = {};
165
+ headerCells.forEach((cell) => {
166
+ const colId = cell.getAttribute('data-column-id');
167
+ if (colId)
168
+ measured[colId] = cell.offsetWidth;
169
+ });
170
+ setMeasuredColumnWidths((prev) => {
171
+ // Only update if widths actually changed to avoid render loops
172
+ for (const key in measured) {
173
+ if (prev[key] !== measured[key])
174
+ return measured;
175
+ }
176
+ if (Object.keys(prev).length !== Object.keys(measured).length)
177
+ return measured;
178
+ return prev;
179
+ });
180
+ // Re-measure when columns, container size, or resize overrides change
181
+ // eslint-disable-next-line react-hooks/exhaustive-deps
182
+ }, [visibleCols, containerWidth, columnSizingOverrides]);
183
+ // Build column width map for pinning offset computation
184
+ const columnWidthMap = useMemo(() => {
185
+ const map = {};
186
+ for (const col of visibleCols) {
187
+ const override = columnSizingOverrides[col.columnId];
188
+ map[col.columnId] = override
189
+ ? override.widthPx
190
+ : (measuredColumnWidths[col.columnId] ?? col.idealWidth ?? col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
191
+ }
192
+ return map;
193
+ }, [visibleCols, columnSizingOverrides, measuredColumnWidths]);
194
+ const leftOffsets = useMemo(() => pinningResult.computeLeftOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH, hasCheckboxCol, CHECKBOX_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap, hasCheckboxCol]);
195
+ const rightOffsets = useMemo(() => pinningResult.computeRightOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap]);
158
196
  const aggregation = useMemo(() => computeAggregations(items, visibleCols, cellSelection ? selectionRange : null), [items, visibleCols, selectionRange, cellSelection]);
159
197
  const statusBarConfig = useMemo(() => {
160
198
  const base = getDataGridStatusBarConfig(statusBar, items.length, selectedRowIds.size);
@@ -172,9 +210,27 @@ export function useDataGridState(params) {
172
210
  const onFilterChangeRef = useLatestRef(onFilterChange);
173
211
  const peopleSearchRef = useLatestRef(peopleSearch);
174
212
  // Stable callback wrappers that delegate to refs
175
- const stableOnColumnSort = useCallback((columnKey) => onColumnSortRef.current?.(columnKey),
213
+ const stableOnColumnSort = useCallback((columnKey, direction) => onColumnSortRef.current?.(columnKey, direction),
176
214
  // eslint-disable-next-line react-hooks/exhaustive-deps
177
215
  []);
216
+ // Autosize callback — updates internal column sizing state + notifies external listener
217
+ const handleAutosizeColumn = useCallback((columnId, width) => {
218
+ setColumnSizingOverrides((prev) => ({ ...prev, [columnId]: { widthPx: width } }));
219
+ onColumnResized?.(columnId, width);
220
+ }, [setColumnSizingOverrides, onColumnResized]);
221
+ const headerMenuResult = useColumnHeaderMenuState({
222
+ pinnedColumns: pinningResult.pinnedColumns,
223
+ onPinColumn: pinningResult.pinColumn,
224
+ onUnpinColumn: pinningResult.unpinColumn,
225
+ sortBy,
226
+ sortDirection,
227
+ onColumnSort: stableOnColumnSort,
228
+ onColumnResized,
229
+ onAutosizeColumn: handleAutosizeColumn,
230
+ columns: flatColumns,
231
+ data: items,
232
+ getRowId: getRowId,
233
+ });
178
234
  const stableOnFilterChange = useCallback((...args) => onFilterChangeRef.current?.(...args),
179
235
  // eslint-disable-next-line react-hooks/exhaustive-deps
180
236
  []);
@@ -326,8 +382,8 @@ export function useDataGridState(params) {
326
382
  pinColumn: pinningResult.pinColumn,
327
383
  unpinColumn: pinningResult.unpinColumn,
328
384
  isPinned: pinningResult.isPinned,
329
- computeLeftOffsets: pinningResult.computeLeftOffsets,
330
- computeRightOffsets: pinningResult.computeRightOffsets,
385
+ leftOffsets,
386
+ rightOffsets,
331
387
  headerMenu: {
332
388
  isOpen: headerMenuResult.isOpen,
333
389
  openForColumn: headerMenuResult.openForColumn,
@@ -337,17 +393,28 @@ export function useDataGridState(params) {
337
393
  handlePinLeft: headerMenuResult.handlePinLeft,
338
394
  handlePinRight: headerMenuResult.handlePinRight,
339
395
  handleUnpin: headerMenuResult.handleUnpin,
396
+ handleSortAsc: headerMenuResult.handleSortAsc,
397
+ handleSortDesc: headerMenuResult.handleSortDesc,
398
+ handleClearSort: headerMenuResult.handleClearSort,
399
+ handleAutosizeThis: headerMenuResult.handleAutosizeThis,
400
+ handleAutosizeAll: headerMenuResult.handleAutosizeAll,
340
401
  canPinLeft: headerMenuResult.canPinLeft,
341
402
  canPinRight: headerMenuResult.canPinRight,
342
403
  canUnpin: headerMenuResult.canUnpin,
404
+ currentSort: headerMenuResult.currentSort,
405
+ isSortable: headerMenuResult.isSortable,
406
+ isResizable: headerMenuResult.isResizable,
343
407
  },
344
408
  }), [
345
409
  pinningResult.pinnedColumns, pinningResult.pinColumn, pinningResult.unpinColumn,
346
- pinningResult.isPinned, pinningResult.computeLeftOffsets, pinningResult.computeRightOffsets,
410
+ pinningResult.isPinned, leftOffsets, rightOffsets,
347
411
  headerMenuResult.isOpen, headerMenuResult.openForColumn, headerMenuResult.anchorElement,
348
412
  headerMenuResult.open, headerMenuResult.close, headerMenuResult.handlePinLeft,
349
413
  headerMenuResult.handlePinRight, headerMenuResult.handleUnpin,
414
+ headerMenuResult.handleSortAsc, headerMenuResult.handleSortDesc, headerMenuResult.handleClearSort,
415
+ headerMenuResult.handleAutosizeThis, headerMenuResult.handleAutosizeAll,
350
416
  headerMenuResult.canPinLeft, headerMenuResult.canPinRight, headerMenuResult.canUnpin,
417
+ headerMenuResult.currentSort, headerMenuResult.isSortable, headerMenuResult.isResizable,
351
418
  ]);
352
419
  return {
353
420
  layout: layoutState,