@alaarab/ogrid-react 2.0.3 → 2.0.4

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.
@@ -87,7 +87,11 @@ export function BaseInlineCellEditor(props) {
87
87
  });
88
88
  React.useEffect(() => {
89
89
  const input = wrapperRef.current?.querySelector('input');
90
- input?.focus();
90
+ if (input) {
91
+ input.focus();
92
+ // Select all text for easy replacement (like Excel)
93
+ input.select();
94
+ }
91
95
  }, []);
92
96
  // Rich select (shared across all frameworks)
93
97
  if (editorType === 'richSelect') {
@@ -19,6 +19,8 @@ export { useDateFilterState } from './useDateFilterState';
19
19
  export { useColumnChooserState } from './useColumnChooserState';
20
20
  export { useInlineCellEditorState } from './useInlineCellEditorState';
21
21
  export { useColumnResize } from './useColumnResize';
22
+ export { useColumnPinning } from './useColumnPinning';
23
+ export { useColumnHeaderMenuState } from './useColumnHeaderMenuState';
22
24
  export { useRichSelectState } from './useRichSelectState';
23
25
  export { useSideBarState } from './useSideBarState';
24
26
  export { useTableLayout } from './useTableLayout';
@@ -9,8 +9,9 @@ function rangesEqual(a, b) {
9
9
  return a.startRow === b.startRow && a.endRow === b.endRow &&
10
10
  a.startCol === b.startCol && a.endCol === b.endCol;
11
11
  }
12
- /** DOM attribute name used for drag-range highlighting (bypasses React). */
12
+ /** DOM attribute names used for drag-range highlighting (bypasses React). */
13
13
  const DRAG_ATTR = 'data-drag-range';
14
+ const DRAG_ANCHOR_ATTR = 'data-drag-anchor';
14
15
  /** Auto-scroll config */
15
16
  const AUTO_SCROLL_EDGE = 40; // px from wrapper edge to trigger
16
17
  const AUTO_SCROLL_MIN_SPEED = 2;
@@ -84,6 +85,9 @@ export function useCellSelection(params) {
84
85
  // setIsDragging(true) is deferred to the first mousemove to avoid
85
86
  // a true→false toggle on simple clicks (which causes 2 extra renders).
86
87
  isDraggingRef.current = true;
88
+ // Apply drag attrs immediately for the initial cell so the anchor styling shows
89
+ // even before the first mousemove. This ensures instant visual feedback.
90
+ setTimeout(() => applyDragAttrsRef.current?.(initial), 0);
87
91
  }
88
92
  },
89
93
  // eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectionRange is a stable callback
@@ -102,12 +106,16 @@ export function useCellSelection(params) {
102
106
  }, [rowCount, visibleColCount, colOffset, setActiveCell]);
103
107
  /** Last known mouse position during drag — used by mouseUp to flush pending RAF work. */
104
108
  const lastMousePosRef = useRef(null);
109
+ // Ref to expose applyDragAttrs outside useEffect so it can be called from mouseDown
110
+ const applyDragAttrsRef = useRef(null);
105
111
  // Window mouse move/up for drag selection.
106
112
  // Performance: during drag, we update a ref + toggle DOM attributes via rAF.
107
113
  // React state is only committed on mouseup (single re-render instead of 60-120/s).
108
114
  useEffect(() => {
109
115
  const colOff = colOffset; // capture for closure
110
- /** Toggle DRAG_ATTR on cell-content divs to show the range highlight via CSS. */
116
+ /** Toggle DRAG_ATTR on cells to show the range highlight via CSS.
117
+ * Also sets edge box-shadows for a green border around the selection range,
118
+ * and marks the anchor cell with DRAG_ANCHOR_ATTR (white background). */
111
119
  const applyDragAttrs = (range) => {
112
120
  const wrapper = wrapperRef.current;
113
121
  if (!wrapper)
@@ -116,6 +124,7 @@ export function useCellSelection(params) {
116
124
  const maxR = Math.max(range.startRow, range.endRow);
117
125
  const minC = Math.min(range.startCol, range.endCol);
118
126
  const maxC = Math.max(range.startCol, range.endCol);
127
+ const anchor = dragStartRef.current;
119
128
  const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
120
129
  for (let i = 0; i < cells.length; i++) {
121
130
  const el = cells[i];
@@ -125,20 +134,51 @@ export function useCellSelection(params) {
125
134
  if (inRange) {
126
135
  if (!el.hasAttribute(DRAG_ATTR))
127
136
  el.setAttribute(DRAG_ATTR, '');
137
+ // Anchor cell gets white background instead of green
138
+ const isAnchor = anchor && r === anchor.row && c === anchor.col;
139
+ if (isAnchor) {
140
+ if (!el.hasAttribute(DRAG_ANCHOR_ATTR))
141
+ el.setAttribute(DRAG_ANCHOR_ATTR, '');
142
+ }
143
+ else {
144
+ if (el.hasAttribute(DRAG_ANCHOR_ATTR))
145
+ el.removeAttribute(DRAG_ANCHOR_ATTR);
146
+ }
147
+ // Edge borders via inset box-shadow (no layout shift)
148
+ const shadows = [];
149
+ if (r === minR)
150
+ shadows.push('inset 0 2px 0 0 var(--ogrid-selection, #217346)');
151
+ if (r === maxR)
152
+ shadows.push('inset 0 -2px 0 0 var(--ogrid-selection, #217346)');
153
+ if (c === minC)
154
+ shadows.push('inset 2px 0 0 0 var(--ogrid-selection, #217346)');
155
+ if (c === maxC)
156
+ shadows.push('inset -2px 0 0 0 var(--ogrid-selection, #217346)');
157
+ el.style.boxShadow = shadows.length > 0 ? shadows.join(', ') : '';
128
158
  }
129
159
  else {
130
160
  if (el.hasAttribute(DRAG_ATTR))
131
161
  el.removeAttribute(DRAG_ATTR);
162
+ if (el.hasAttribute(DRAG_ANCHOR_ATTR))
163
+ el.removeAttribute(DRAG_ANCHOR_ATTR);
164
+ if (el.style.boxShadow)
165
+ el.style.boxShadow = '';
132
166
  }
133
167
  }
134
168
  };
169
+ // Expose applyDragAttrs via ref so mouseDown can access it
170
+ applyDragAttrsRef.current = applyDragAttrs;
135
171
  const clearDragAttrs = () => {
136
172
  const wrapper = wrapperRef.current;
137
173
  if (!wrapper)
138
174
  return;
139
175
  const marked = wrapper.querySelectorAll(`[${DRAG_ATTR}]`);
140
- for (let i = 0; i < marked.length; i++)
141
- marked[i].removeAttribute(DRAG_ATTR);
176
+ for (let i = 0; i < marked.length; i++) {
177
+ const el = marked[i];
178
+ el.removeAttribute(DRAG_ATTR);
179
+ el.removeAttribute(DRAG_ANCHOR_ATTR);
180
+ el.style.boxShadow = '';
181
+ }
142
182
  };
143
183
  /** Resolve mouse coordinates to a cell range (shared by RAF callback and mouseUp flush). */
144
184
  const resolveRange = (cx, cy) => {
@@ -0,0 +1,56 @@
1
+ import { useState, useCallback } from 'react';
2
+ /**
3
+ * Manages state for the column header menu (pin left/right/unpin actions).
4
+ * Tracks which column's menu is open, anchor element, and action handlers.
5
+ */
6
+ export function useColumnHeaderMenuState(params) {
7
+ const { pinnedColumns, onPinColumn, onUnpinColumn } = params;
8
+ const [isOpen, setIsOpen] = useState(false);
9
+ const [openForColumn, setOpenForColumn] = useState(null);
10
+ const [anchorElement, setAnchorElement] = useState(null);
11
+ const open = useCallback((columnId, anchorEl) => {
12
+ setOpenForColumn(columnId);
13
+ setAnchorElement(anchorEl);
14
+ setIsOpen(true);
15
+ }, []);
16
+ const close = useCallback(() => {
17
+ setIsOpen(false);
18
+ setOpenForColumn(null);
19
+ setAnchorElement(null);
20
+ }, []);
21
+ const currentPinState = openForColumn ? pinnedColumns[openForColumn] : undefined;
22
+ const canPinLeft = currentPinState !== 'left';
23
+ const canPinRight = currentPinState !== 'right';
24
+ const canUnpin = !!currentPinState;
25
+ const handlePinLeft = useCallback(() => {
26
+ if (openForColumn && canPinLeft) {
27
+ onPinColumn(openForColumn, 'left');
28
+ close();
29
+ }
30
+ }, [openForColumn, canPinLeft, onPinColumn, close]);
31
+ const handlePinRight = useCallback(() => {
32
+ if (openForColumn && canPinRight) {
33
+ onPinColumn(openForColumn, 'right');
34
+ close();
35
+ }
36
+ }, [openForColumn, canPinRight, onPinColumn, close]);
37
+ const handleUnpin = useCallback(() => {
38
+ if (openForColumn && canUnpin) {
39
+ onUnpinColumn(openForColumn);
40
+ close();
41
+ }
42
+ }, [openForColumn, canUnpin, onUnpinColumn, close]);
43
+ return {
44
+ isOpen,
45
+ openForColumn,
46
+ anchorElement,
47
+ open,
48
+ close,
49
+ handlePinLeft,
50
+ handlePinRight,
51
+ handleUnpin,
52
+ canPinLeft,
53
+ canPinRight,
54
+ canUnpin,
55
+ };
56
+ }
@@ -0,0 +1,67 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ /**
3
+ * Manages column pinning state (left/right sticky positioning).
4
+ * Supports controlled and uncontrolled modes.
5
+ * Initializes from column.pinned definitions and pinnedColumns prop.
6
+ */
7
+ export function useColumnPinning(params) {
8
+ const { columns, pinnedColumns: controlledPinnedColumns, onColumnPinned } = params;
9
+ // Initialize internal state from column.pinned definitions
10
+ const initialPinnedColumns = useMemo(() => {
11
+ const initial = {};
12
+ for (const col of columns) {
13
+ if (col.pinned) {
14
+ initial[col.columnId] = col.pinned;
15
+ }
16
+ }
17
+ return initial;
18
+ }, []); // Only on mount
19
+ const [internalPinnedColumns, setInternalPinnedColumns] = useState(initialPinnedColumns);
20
+ // Use controlled state if provided, otherwise internal
21
+ const pinnedColumns = controlledPinnedColumns ?? internalPinnedColumns;
22
+ const pinColumn = useCallback((columnId, side) => {
23
+ const next = { ...pinnedColumns, [columnId]: side };
24
+ setInternalPinnedColumns(next);
25
+ onColumnPinned?.(columnId, side);
26
+ }, [pinnedColumns, onColumnPinned]);
27
+ const unpinColumn = useCallback((columnId) => {
28
+ const next = { ...pinnedColumns };
29
+ delete next[columnId];
30
+ setInternalPinnedColumns(next);
31
+ onColumnPinned?.(columnId, null);
32
+ }, [pinnedColumns, onColumnPinned]);
33
+ const isPinned = useCallback((columnId) => {
34
+ return pinnedColumns[columnId];
35
+ }, [pinnedColumns]);
36
+ const computeLeftOffsets = useCallback((visibleCols, columnWidths, defaultWidth, hasCheckboxColumn, checkboxColumnWidth) => {
37
+ const offsets = {};
38
+ let left = hasCheckboxColumn ? checkboxColumnWidth : 0;
39
+ for (const col of visibleCols) {
40
+ if (pinnedColumns[col.columnId] === 'left') {
41
+ offsets[col.columnId] = left;
42
+ left += columnWidths[col.columnId] ?? defaultWidth;
43
+ }
44
+ }
45
+ return offsets;
46
+ }, [pinnedColumns]);
47
+ const computeRightOffsets = useCallback((visibleCols, columnWidths, defaultWidth) => {
48
+ const offsets = {};
49
+ let right = 0;
50
+ for (let i = visibleCols.length - 1; i >= 0; i--) {
51
+ const col = visibleCols[i];
52
+ if (pinnedColumns[col.columnId] === 'right') {
53
+ offsets[col.columnId] = right;
54
+ right += columnWidths[col.columnId] ?? defaultWidth;
55
+ }
56
+ }
57
+ return offsets;
58
+ }, [pinnedColumns]);
59
+ return {
60
+ pinnedColumns,
61
+ pinColumn,
62
+ unpinColumn,
63
+ isPinned,
64
+ computeLeftOffsets,
65
+ computeRightOffsets,
66
+ };
67
+ }
@@ -11,6 +11,8 @@ import { useFillHandle } from './useFillHandle';
11
11
  import { useUndoRedo } from './useUndoRedo';
12
12
  import { useLatestRef } from './useLatestRef';
13
13
  import { useTableLayout } from './useTableLayout';
14
+ import { useColumnPinning } from './useColumnPinning';
15
+ import { useColumnHeaderMenuState } from './useColumnHeaderMenuState';
14
16
  // Stable no-op handlers used when cellSelection is disabled (module-scope = no re-renders)
15
17
  const NOOP = () => { };
16
18
  const NOOP_ASYNC = async () => { };
@@ -23,7 +25,7 @@ const NOOP_CTX = (_e) => { };
23
25
  */
24
26
  export function useDataGridState(params) {
25
27
  const { props, wrapperRef } = params;
26
- const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, initialColumnWidths, onColumnResized, pinnedColumns, onCellError, } = props;
28
+ const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, showRowNumbers, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, initialColumnWidths, onColumnResized, pinnedColumns, onColumnPinned, onCellError, } = props;
27
29
  const cellSelection = cellSelectionProp !== false;
28
30
  // Wrap onCellValueChanged with undo/redo tracking — all edits are recorded automatically
29
31
  const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp });
@@ -63,8 +65,10 @@ export function useDataGridState(params) {
63
65
  }, [flatColumns, visibleColumns, columnOrder]);
64
66
  const visibleColumnCount = visibleCols.length;
65
67
  const hasCheckboxCol = rowSelection === 'multiple';
66
- const totalColCount = visibleColumnCount + (hasCheckboxCol ? 1 : 0);
67
- const colOffset = hasCheckboxCol ? 1 : 0;
68
+ const hasRowNumbersCol = !!showRowNumbers;
69
+ const specialColsCount = (hasCheckboxCol ? 1 : 0) + (hasRowNumbersCol ? 1 : 0);
70
+ const totalColCount = visibleColumnCount + specialColsCount;
71
+ const colOffset = specialColsCount;
68
72
  const rowIndexByRowId = useMemo(() => {
69
73
  const m = new Map();
70
74
  items.forEach((item, idx) => m.set(getRowId(item), idx));
@@ -136,6 +140,16 @@ export function useDataGridState(params) {
136
140
  initialColumnWidths,
137
141
  onColumnResized,
138
142
  });
143
+ const pinningResult = useColumnPinning({
144
+ columns: flatColumns,
145
+ pinnedColumns,
146
+ onColumnPinned,
147
+ });
148
+ const headerMenuResult = useColumnHeaderMenuState({
149
+ pinnedColumns: pinningResult.pinnedColumns,
150
+ onPinColumn: pinningResult.pinColumn,
151
+ onUnpinColumn: pinningResult.unpinColumn,
152
+ });
139
153
  const aggregation = useMemo(() => computeAggregations(items, visibleCols, cellSelection ? selectionRange : null), [items, visibleCols, selectionRange, cellSelection]);
140
154
  const statusBarConfig = useMemo(() => {
141
155
  const base = getDataGridStatusBarConfig(statusBar, items.length, selectedRowIds.size);
@@ -250,11 +264,11 @@ export function useDataGridState(params) {
250
264
  // --- Memoize each sub-object so downstream consumers only re-render when their slice changes ---
251
265
  const layoutState = useMemo(() => ({
252
266
  flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
253
- hasCheckboxCol, rowIndexByRowId, containerWidth, minTableWidth,
267
+ hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
254
268
  desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
255
269
  }), [
256
270
  flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
257
- hasCheckboxCol, rowIndexByRowId, containerWidth, minTableWidth,
271
+ hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
258
272
  desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
259
273
  ]);
260
274
  const rowSelectionState = useMemo(() => ({
@@ -302,6 +316,27 @@ export function useDataGridState(params) {
302
316
  const viewModelsState = useMemo(() => ({
303
317
  headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError,
304
318
  }), [headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError]);
319
+ const pinningState = useMemo(() => ({
320
+ pinnedColumns: pinningResult.pinnedColumns,
321
+ pinColumn: pinningResult.pinColumn,
322
+ unpinColumn: pinningResult.unpinColumn,
323
+ isPinned: pinningResult.isPinned,
324
+ computeLeftOffsets: pinningResult.computeLeftOffsets,
325
+ computeRightOffsets: pinningResult.computeRightOffsets,
326
+ headerMenu: {
327
+ isOpen: headerMenuResult.isOpen,
328
+ openForColumn: headerMenuResult.openForColumn,
329
+ anchorElement: headerMenuResult.anchorElement,
330
+ open: headerMenuResult.open,
331
+ close: headerMenuResult.close,
332
+ handlePinLeft: headerMenuResult.handlePinLeft,
333
+ handlePinRight: headerMenuResult.handlePinRight,
334
+ handleUnpin: headerMenuResult.handleUnpin,
335
+ canPinLeft: headerMenuResult.canPinLeft,
336
+ canPinRight: headerMenuResult.canPinRight,
337
+ canUnpin: headerMenuResult.canUnpin,
338
+ },
339
+ }), [pinningResult, headerMenuResult]);
305
340
  return {
306
341
  layout: layoutState,
307
342
  rowSelection: rowSelectionState,
@@ -309,5 +344,6 @@ export function useDataGridState(params) {
309
344
  interaction: interactionState,
310
345
  contextMenu: contextMenuState,
311
346
  viewModels: viewModelsState,
347
+ pinning: pinningState,
312
348
  };
313
349
  }
@@ -11,7 +11,7 @@ const EMPTY_LOADING_OPTIONS = {};
11
11
  * @returns Grouped props for DataGridTable, pagination controls, column chooser, layout, and filters.
12
12
  */
13
13
  export function useOGrid(props, ref) {
14
- const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, freezeRows, freezeCols, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, columnReorder, virtualScroll, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
14
+ const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, freezeRows, freezeCols, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, showRowNumbers, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, columnReorder, virtualScroll, density = 'normal', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
15
15
  // Resolve column chooser placement
16
16
  const columnChooserPlacement = columnChooserProp === false ? 'none'
17
17
  : columnChooserProp === 'sidebar' ? 'sidebar'
@@ -409,6 +409,9 @@ export function useOGrid(props, ref) {
409
409
  rowSelection,
410
410
  selectedRows: effectiveSelectedRows,
411
411
  onSelectionChange: handleSelectionChange,
412
+ showRowNumbers,
413
+ currentPage: page,
414
+ pageSize,
412
415
  statusBar: statusBarConfig,
413
416
  isLoading: isLoadingResolved,
414
417
  filters,
@@ -421,6 +424,7 @@ export function useOGrid(props, ref) {
421
424
  suppressHorizontalScroll,
422
425
  columnReorder,
423
426
  virtualScroll,
427
+ density,
424
428
  'aria-label': ariaLabel,
425
429
  'aria-labelledby': ariaLabelledBy,
426
430
  emptyState: {
@@ -434,10 +438,10 @@ export function useOGrid(props, ref) {
434
438
  visibleColumns, columnOrder, onColumnOrderChange, handleColumnResized,
435
439
  handleColumnPinned, pinnedOverrides, columnWidthOverrides, freezeRows, freezeCols,
436
440
  editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo,
437
- rowSelection, effectiveSelectedRows, handleSelectionChange, statusBarConfig,
441
+ rowSelection, effectiveSelectedRows, handleSelectionChange, showRowNumbers, page, pageSize, statusBarConfig,
438
442
  isLoadingResolved, filters, handleFilterChange, clientFilterOptions, dataSource,
439
443
  loadingFilterOptions, layoutMode, suppressHorizontalScroll, columnReorder, virtualScroll,
440
- ariaLabel, ariaLabelledBy,
444
+ density, ariaLabel, ariaLabelledBy,
441
445
  hasActiveFilters, clearAllFilters, emptyState,
442
446
  ]);
443
447
  const pagination = useMemo(() => ({
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Constants (re-exported from core)
2
- export { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
2
+ export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
4
4
  // Hooks
5
5
  export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, useTableLayout, useColumnReorder, useVirtualScroll, useLatestRef, } from './hooks';
@@ -39,6 +39,10 @@ export { useInlineCellEditorState } from './useInlineCellEditorState';
39
39
  export type { UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, InlineCellEditorType, } from './useInlineCellEditorState';
40
40
  export { useColumnResize } from './useColumnResize';
41
41
  export type { UseColumnResizeParams, UseColumnResizeResult, } from './useColumnResize';
42
+ export { useColumnPinning } from './useColumnPinning';
43
+ export type { UseColumnPinningParams, UseColumnPinningResult, } from './useColumnPinning';
44
+ export { useColumnHeaderMenuState } from './useColumnHeaderMenuState';
45
+ export type { UseColumnHeaderMenuStateParams, UseColumnHeaderMenuStateResult, } from './useColumnHeaderMenuState';
42
46
  export { useRichSelectState } from './useRichSelectState';
43
47
  export type { UseRichSelectStateParams, UseRichSelectStateResult } from './useRichSelectState';
44
48
  export { useSideBarState } from './useSideBarState';
@@ -0,0 +1,23 @@
1
+ export interface UseColumnHeaderMenuStateParams {
2
+ pinnedColumns: Record<string, 'left' | 'right'>;
3
+ onPinColumn: (columnId: string, side: 'left' | 'right') => void;
4
+ onUnpinColumn: (columnId: string) => void;
5
+ }
6
+ export interface UseColumnHeaderMenuStateResult {
7
+ isOpen: boolean;
8
+ openForColumn: string | null;
9
+ anchorElement: HTMLElement | null;
10
+ open: (columnId: string, anchorEl: HTMLElement) => void;
11
+ close: () => void;
12
+ handlePinLeft: () => void;
13
+ handlePinRight: () => void;
14
+ handleUnpin: () => void;
15
+ canPinLeft: boolean;
16
+ canPinRight: boolean;
17
+ canUnpin: boolean;
18
+ }
19
+ /**
20
+ * Manages state for the column header menu (pin left/right/unpin actions).
21
+ * Tracks which column's menu is open, anchor element, and action handlers.
22
+ */
23
+ export declare function useColumnHeaderMenuState(params: UseColumnHeaderMenuStateParams): UseColumnHeaderMenuStateResult;
@@ -0,0 +1,32 @@
1
+ import type { IColumnDef } from '@alaarab/ogrid-core';
2
+ export interface UseColumnPinningParams<T = unknown> {
3
+ columns: IColumnDef<T>[];
4
+ /** Controlled pinned columns state. If provided, component is controlled. */
5
+ pinnedColumns?: Record<string, 'left' | 'right'>;
6
+ /** Called when user pins/unpins a column via UI. */
7
+ onColumnPinned?: (columnId: string, pinned: 'left' | 'right' | null) => void;
8
+ }
9
+ export interface UseColumnPinningResult {
10
+ /** Current pinned columns (controlled or internal). */
11
+ pinnedColumns: Record<string, 'left' | 'right'>;
12
+ /** Pin a column to left or right. */
13
+ pinColumn: (columnId: string, side: 'left' | 'right') => void;
14
+ /** Unpin a column. */
15
+ unpinColumn: (columnId: string) => void;
16
+ /** Check if a column is pinned and which side. */
17
+ isPinned: (columnId: string) => 'left' | 'right' | undefined;
18
+ /** Compute sticky left offsets for pinned columns. */
19
+ computeLeftOffsets: (visibleCols: {
20
+ columnId: string;
21
+ }[], columnWidths: Record<string, number>, defaultWidth: number, hasCheckboxColumn: boolean, checkboxColumnWidth: number) => Record<string, number>;
22
+ /** Compute sticky right offsets for pinned columns. */
23
+ computeRightOffsets: (visibleCols: {
24
+ columnId: string;
25
+ }[], columnWidths: Record<string, number>, defaultWidth: number) => Record<string, number>;
26
+ }
27
+ /**
28
+ * Manages column pinning state (left/right sticky positioning).
29
+ * Supports controlled and uncontrolled modes.
30
+ * Initializes from column.pinned definitions and pinnedColumns prop.
31
+ */
32
+ export declare function useColumnPinning<T = unknown>(params: UseColumnPinningParams<T>): UseColumnPinningResult;
@@ -13,6 +13,7 @@ export interface DataGridLayoutState<T> {
13
13
  totalColCount: number;
14
14
  colOffset: number;
15
15
  hasCheckboxCol: boolean;
16
+ hasRowNumbersCol: boolean;
16
17
  rowIndexByRowId: Map<RowId, number>;
17
18
  containerWidth: number;
18
19
  minTableWidth: number;
@@ -121,6 +122,32 @@ export interface DataGridViewModelState<T> {
121
122
  showEmptyInGrid: boolean;
122
123
  onCellError?: (error: Error, errorInfo: React.ErrorInfo) => void;
123
124
  }
125
+ /** Column pinning state and column header menu. */
126
+ export interface DataGridPinningState {
127
+ pinnedColumns: Record<string, 'left' | 'right'>;
128
+ pinColumn: (columnId: string, side: 'left' | 'right') => void;
129
+ unpinColumn: (columnId: string) => void;
130
+ isPinned: (columnId: string) => 'left' | 'right' | undefined;
131
+ computeLeftOffsets: (visibleCols: {
132
+ columnId: string;
133
+ }[], columnWidths: Record<string, number>, defaultWidth: number, hasCheckboxColumn: boolean, checkboxColumnWidth: number) => Record<string, number>;
134
+ computeRightOffsets: (visibleCols: {
135
+ columnId: string;
136
+ }[], columnWidths: Record<string, number>, defaultWidth: number) => Record<string, number>;
137
+ headerMenu: {
138
+ isOpen: boolean;
139
+ openForColumn: string | null;
140
+ anchorElement: HTMLElement | null;
141
+ open: (columnId: string, anchorEl: HTMLElement) => void;
142
+ close: () => void;
143
+ handlePinLeft: () => void;
144
+ handlePinRight: () => void;
145
+ handleUnpin: () => void;
146
+ canPinLeft: boolean;
147
+ canPinRight: boolean;
148
+ canUnpin: boolean;
149
+ };
150
+ }
124
151
  /** Grouped result from useDataGridState. */
125
152
  export interface UseDataGridStateResult<T> {
126
153
  layout: DataGridLayoutState<T>;
@@ -129,6 +156,7 @@ export interface UseDataGridStateResult<T> {
129
156
  interaction: DataGridCellInteractionState;
130
157
  contextMenu: DataGridContextMenuState;
131
158
  viewModels: DataGridViewModelState<T>;
159
+ pinning: DataGridPinningState;
132
160
  }
133
161
  /**
134
162
  * Single orchestration hook for DataGridTable. Takes grid props and wrapper ref,
@@ -1,4 +1,4 @@
1
- export { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
1
+ export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
2
2
  export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, RowId, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, HeaderCell, HeaderRow, SideBarPanelId, ISideBarDef, IDateFilterValue, IVirtualScrollConfig, IColumnReorderConfig, } from './types';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
4
4
  export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, useTableLayout, useColumnReorder, useVirtualScroll, useLatestRef, } from './hooks';
@@ -43,6 +43,8 @@ interface IOGridBaseProps<T> {
43
43
  rowSelection?: RowSelectionMode;
44
44
  selectedRows?: Set<RowId>;
45
45
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
46
+ /** Show Excel-style row numbers column at the start of the grid (1, 2, 3...). Default: false. */
47
+ showRowNumbers?: boolean;
46
48
  statusBar?: boolean | IStatusBarProps;
47
49
  defaultPageSize?: number;
48
50
  defaultSortBy?: string;
@@ -72,6 +74,8 @@ interface IOGridBaseProps<T> {
72
74
  columnReorder?: boolean;
73
75
  /** Virtual scrolling configuration. When provided, only visible rows are rendered for large datasets. */
74
76
  virtualScroll?: IVirtualScrollConfig;
77
+ /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
78
+ density?: 'compact' | 'normal' | 'comfortable';
75
79
  /** Fires once when the grid first renders with data (useful for restoring column state). */
76
80
  onFirstDataRendered?: () => void;
77
81
  /** Called when server-side fetchPage fails. */
@@ -134,6 +138,12 @@ export interface IOGridDataGridProps<T> {
134
138
  rowSelection?: RowSelectionMode;
135
139
  selectedRows?: Set<RowId>;
136
140
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
141
+ /** Show Excel-style row numbers column. */
142
+ showRowNumbers?: boolean;
143
+ /** Current page number (1-based) for row number calculation. */
144
+ currentPage?: number;
145
+ /** Page size for row number calculation. */
146
+ pageSize?: number;
137
147
  statusBar?: IStatusBarProps;
138
148
  /** Unified filter model (discriminated union values). */
139
149
  filters: IFilters;
@@ -153,6 +163,8 @@ export interface IOGridDataGridProps<T> {
153
163
  columnReorder?: boolean;
154
164
  /** Virtual scrolling configuration. When provided, only visible rows are rendered for large datasets. */
155
165
  virtualScroll?: IVirtualScrollConfig;
166
+ /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
167
+ density?: 'compact' | 'normal' | 'comfortable';
156
168
  /** Called when a cell renderer or custom editor throws an error. */
157
169
  onCellError?: (error: Error, errorInfo: React.ErrorInfo) => void;
158
170
  'aria-label'?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "OGrid React – React hooks, headless components, and utilities for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -35,7 +35,7 @@
35
35
  "node": ">=18"
36
36
  },
37
37
  "dependencies": {
38
- "@alaarab/ogrid-core": "2.0.3",
38
+ "@alaarab/ogrid-core": "2.0.4",
39
39
  "@tanstack/react-virtual": "^3.11.0"
40
40
  },
41
41
  "peerDependencies": {