@alaarab/ogrid-react 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.
Files changed (66) hide show
  1. package/dist/esm/components/ColumnHeaderFilterContent.js +0 -2
  2. package/dist/esm/components/MarchingAntsOverlay.js +2 -3
  3. package/dist/esm/components/SideBar.js +8 -7
  4. package/dist/esm/components/createOGrid.js +1 -4
  5. package/dist/esm/hooks/index.js +10 -0
  6. package/dist/esm/hooks/useActiveCell.js +2 -4
  7. package/dist/esm/hooks/useCellSelection.js +85 -52
  8. package/dist/esm/hooks/useClipboard.js +15 -54
  9. package/dist/esm/hooks/useColumnChooserState.js +25 -13
  10. package/dist/esm/hooks/useColumnHeaderFilterState.js +22 -11
  11. package/dist/esm/hooks/useColumnHeaderMenuState.js +1 -1
  12. package/dist/esm/hooks/useColumnMeta.js +61 -0
  13. package/dist/esm/hooks/useColumnPinning.js +11 -12
  14. package/dist/esm/hooks/useColumnReorder.js +8 -1
  15. package/dist/esm/hooks/useColumnResize.js +6 -2
  16. package/dist/esm/hooks/useDataGridContextMenu.js +24 -0
  17. package/dist/esm/hooks/useDataGridEditing.js +56 -0
  18. package/dist/esm/hooks/useDataGridInteraction.js +109 -0
  19. package/dist/esm/hooks/useDataGridLayout.js +172 -0
  20. package/dist/esm/hooks/useDataGridState.js +83 -318
  21. package/dist/esm/hooks/useDataGridTableOrchestration.js +2 -4
  22. package/dist/esm/hooks/useFillHandle.js +60 -55
  23. package/dist/esm/hooks/useFilterOptions.js +2 -4
  24. package/dist/esm/hooks/useInlineCellEditorState.js +7 -13
  25. package/dist/esm/hooks/useKeyboardNavigation.js +19 -132
  26. package/dist/esm/hooks/useMultiSelectFilterState.js +1 -1
  27. package/dist/esm/hooks/useOGrid.js +159 -301
  28. package/dist/esm/hooks/useOGridDataFetching.js +74 -0
  29. package/dist/esm/hooks/useOGridFilters.js +59 -0
  30. package/dist/esm/hooks/useOGridPagination.js +24 -0
  31. package/dist/esm/hooks/useOGridSorting.js +24 -0
  32. package/dist/esm/hooks/usePaginationControls.js +2 -5
  33. package/dist/esm/hooks/usePeopleFilterState.js +6 -1
  34. package/dist/esm/hooks/useRichSelectState.js +7 -5
  35. package/dist/esm/hooks/useRowSelection.js +6 -26
  36. package/dist/esm/hooks/useSelectState.js +2 -5
  37. package/dist/esm/hooks/useShallowEqualMemo.js +14 -0
  38. package/dist/esm/hooks/useTableLayout.js +3 -11
  39. package/dist/esm/hooks/useUndoRedo.js +16 -10
  40. package/dist/esm/index.js +1 -1
  41. package/dist/esm/utils/index.js +1 -1
  42. package/dist/types/components/ColumnChooserProps.d.ts +2 -0
  43. package/dist/types/components/ColumnHeaderFilterContent.d.ts +0 -2
  44. package/dist/types/hooks/index.d.ts +19 -0
  45. package/dist/types/hooks/useClipboard.d.ts +0 -1
  46. package/dist/types/hooks/useColumnChooserState.d.ts +2 -0
  47. package/dist/types/hooks/useColumnHeaderFilterState.d.ts +0 -2
  48. package/dist/types/hooks/useColumnHeaderMenuState.d.ts +0 -2
  49. package/dist/types/hooks/useColumnMeta.d.ts +34 -0
  50. package/dist/types/hooks/useDataGridContextMenu.d.ts +20 -0
  51. package/dist/types/hooks/useDataGridEditing.d.ts +39 -0
  52. package/dist/types/hooks/useDataGridInteraction.d.ts +95 -0
  53. package/dist/types/hooks/useDataGridLayout.d.ts +45 -0
  54. package/dist/types/hooks/useDataGridState.d.ts +7 -1
  55. package/dist/types/hooks/useDataGridTableOrchestration.d.ts +1 -2
  56. package/dist/types/hooks/useOGrid.d.ts +4 -2
  57. package/dist/types/hooks/useOGridDataFetching.d.ts +29 -0
  58. package/dist/types/hooks/useOGridFilters.d.ts +24 -0
  59. package/dist/types/hooks/useOGridPagination.d.ts +18 -0
  60. package/dist/types/hooks/useOGridSorting.d.ts +23 -0
  61. package/dist/types/hooks/usePaginationControls.d.ts +1 -1
  62. package/dist/types/hooks/useRichSelectState.d.ts +2 -0
  63. package/dist/types/hooks/useShallowEqualMemo.d.ts +7 -0
  64. package/dist/types/index.d.ts +2 -2
  65. package/dist/types/utils/index.d.ts +2 -2
  66. package/package.json +12 -4
@@ -0,0 +1,74 @@
1
+ import { useState, useEffect, useRef, useMemo } from 'react';
2
+ import { processClientSideData } from '../utils';
3
+ /**
4
+ * Manages data fetching (server-side) and client-side filtering/sorting/pagination.
5
+ * Fires onFirstDataRendered once when items first appear.
6
+ */
7
+ export function useOGridDataFetching(params) {
8
+ const { isServerSide, dataSource, displayData, columns, stableFilters, filters, sort, page, pageSize, onError, onFirstDataRendered, } = params;
9
+ const isClientSide = !isServerSide;
10
+ // --- Client-side filtering & sorting ---
11
+ const clientItemsAndTotal = useMemo(() => {
12
+ if (!isClientSide)
13
+ return null;
14
+ const rows = processClientSideData(displayData, columns, stableFilters, sort.field, sort.direction);
15
+ const total = rows.length;
16
+ const start = (page - 1) * pageSize;
17
+ const paged = rows.slice(start, start + pageSize);
18
+ return { items: paged, totalCount: total };
19
+ }, [isClientSide, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
20
+ // --- Server-side data fetching ---
21
+ const [serverItems, setServerItems] = useState([]);
22
+ const [serverTotalCount, setServerTotalCount] = useState(0);
23
+ const [serverLoading, setServerLoading] = useState(true);
24
+ const fetchIdRef = useRef(0);
25
+ const [refreshCounter, setRefreshCounter] = useState(0);
26
+ useEffect(() => {
27
+ if (!isServerSide || !dataSource) {
28
+ if (!isServerSide)
29
+ setServerLoading(false);
30
+ return;
31
+ }
32
+ const id = ++fetchIdRef.current;
33
+ setServerLoading(true);
34
+ dataSource
35
+ .fetchPage({
36
+ page, pageSize,
37
+ sort: { field: sort.field, direction: sort.direction },
38
+ filters,
39
+ })
40
+ .then((res) => {
41
+ if (id !== fetchIdRef.current)
42
+ return;
43
+ setServerItems(res.items);
44
+ setServerTotalCount(res.totalCount);
45
+ })
46
+ .catch((err) => {
47
+ if (id !== fetchIdRef.current)
48
+ return;
49
+ onError?.(err);
50
+ setServerItems([]);
51
+ setServerTotalCount(0);
52
+ })
53
+ .finally(() => {
54
+ if (id === fetchIdRef.current)
55
+ setServerLoading(false);
56
+ });
57
+ }, [isServerSide, dataSource, page, pageSize, sort.field, sort.direction, filters, onError, refreshCounter]);
58
+ const displayItems = isClientSide && clientItemsAndTotal ? clientItemsAndTotal.items : serverItems;
59
+ const displayTotalCount = isClientSide && clientItemsAndTotal ? clientItemsAndTotal.totalCount : serverTotalCount;
60
+ // Fire onFirstDataRendered once when the grid first has data
61
+ const firstDataRenderedRef = useRef(false);
62
+ useEffect(() => {
63
+ if (!firstDataRenderedRef.current && displayItems.length > 0) {
64
+ firstDataRenderedRef.current = true;
65
+ onFirstDataRendered?.();
66
+ }
67
+ }, [displayItems.length, onFirstDataRendered]);
68
+ return {
69
+ displayItems,
70
+ displayTotalCount,
71
+ serverLoading,
72
+ refreshData: () => setRefreshCounter((prev) => prev + 1),
73
+ };
74
+ }
@@ -0,0 +1,59 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, } from '../utils';
3
+ import { useFilterOptions } from './useFilterOptions';
4
+ import { useShallowEqualMemo } from './useShallowEqualMemo';
5
+ /** Deep-equal check for filter objects (shallow key+value comparison). */
6
+ function filtersEqual(a, b) {
7
+ const aKeys = Object.keys(a);
8
+ const bKeys = Object.keys(b);
9
+ if (aKeys.length !== bKeys.length)
10
+ return false;
11
+ for (let i = 0; i < bKeys.length; i++) {
12
+ if (a[bKeys[i]] !== b[bKeys[i]])
13
+ return false;
14
+ }
15
+ return true;
16
+ }
17
+ const EMPTY_LOADING_OPTIONS = {};
18
+ const EMPTY_DATA_SOURCE = { fetchFilterOptions: undefined };
19
+ /**
20
+ * Manages filter state, filter options (client + server), and stabilized filter reference.
21
+ * Resets to page 1 on filter change.
22
+ */
23
+ export function useOGridFilters(params) {
24
+ const { controlledFilters, onFiltersChange, setPage, columns, displayData, dataSource } = params;
25
+ const [internalFilters, setInternalFilters] = useState({});
26
+ const filters = controlledFilters ?? internalFilters;
27
+ const setFilters = useCallback((f) => {
28
+ if (controlledFilters === undefined)
29
+ setInternalFilters(f);
30
+ onFiltersChange?.(f);
31
+ setPage(1);
32
+ }, [controlledFilters, onFiltersChange, setPage]);
33
+ const handleFilterChange = useCallback((key, value) => {
34
+ setFilters(mergeFilter(filters, key, value));
35
+ }, [filters, setFilters]);
36
+ // Stabilize filters via shallow comparison so processClientSideData useMemo
37
+ // doesn't re-run when the filter object reference changes but values are identical.
38
+ const stableFilters = useShallowEqualMemo(filters, (a, b) => filtersEqual(a, b));
39
+ const hasActiveFilters = useMemo(() => Object.values(filters).some((v) => v !== undefined), [filters]);
40
+ // --- Filter options (server or client-derived) ---
41
+ const multiSelectFilterFields = useMemo(() => getMultiSelectFilterFields(columns), [columns]);
42
+ const filterOptionsSource = dataSource ?? EMPTY_DATA_SOURCE;
43
+ const { filterOptions: serverFilterOptions, loadingOptions: loadingFilterOptions } = useFilterOptions(filterOptionsSource, multiSelectFilterFields);
44
+ const hasServerFilterOptions = dataSource?.fetchFilterOptions != null;
45
+ const clientFilterOptions = useMemo(() => {
46
+ if (hasServerFilterOptions)
47
+ return serverFilterOptions;
48
+ return deriveFilterOptionsFromData(displayData, columns);
49
+ }, [hasServerFilterOptions, displayData, columns, serverFilterOptions]);
50
+ return {
51
+ filters,
52
+ setFilters,
53
+ handleFilterChange,
54
+ stableFilters,
55
+ hasActiveFilters,
56
+ clientFilterOptions,
57
+ loadingFilterOptions: dataSource?.fetchFilterOptions ? loadingFilterOptions : EMPTY_LOADING_OPTIONS,
58
+ };
59
+ }
@@ -0,0 +1,24 @@
1
+ import { useState, useCallback } from 'react';
2
+ /**
3
+ * Manages pagination state with controlled/uncontrolled dual-mode support.
4
+ * Resets to page 1 when page size changes.
5
+ */
6
+ export function useOGridPagination(params) {
7
+ const { controlledPage, controlledPageSize, defaultPageSize, onPageChange, onPageSizeChange } = params;
8
+ const [internalPage, setInternalPage] = useState(1);
9
+ const [internalPageSize, setInternalPageSize] = useState(defaultPageSize);
10
+ const page = controlledPage ?? internalPage;
11
+ const pageSize = controlledPageSize ?? internalPageSize;
12
+ const setPage = useCallback((p) => {
13
+ if (controlledPage === undefined)
14
+ setInternalPage(p);
15
+ onPageChange?.(p);
16
+ }, [controlledPage, onPageChange]);
17
+ const setPageSize = useCallback((size) => {
18
+ if (controlledPageSize === undefined)
19
+ setInternalPageSize(size);
20
+ onPageSizeChange?.(size);
21
+ setPage(1);
22
+ }, [controlledPageSize, onPageSizeChange, setPage]);
23
+ return { page, pageSize, setPage, setPageSize };
24
+ }
@@ -0,0 +1,24 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { computeNextSortState } from '../utils';
3
+ /**
4
+ * Manages sort state with controlled/uncontrolled dual-mode support.
5
+ * Resets to page 1 on sort change.
6
+ */
7
+ export function useOGridSorting(params) {
8
+ const { controlledSort, defaultSortField, defaultSortDirection, onSortChange, setPage } = params;
9
+ const [internalSort, setInternalSort] = useState({
10
+ field: defaultSortField,
11
+ direction: defaultSortDirection,
12
+ });
13
+ const sort = controlledSort ?? internalSort;
14
+ const setSort = useCallback((s) => {
15
+ if (controlledSort === undefined)
16
+ setInternalSort(s);
17
+ onSortChange?.(s);
18
+ setPage(1);
19
+ }, [controlledSort, onSortChange, setPage]);
20
+ const handleSort = useCallback((columnKey, direction) => {
21
+ setSort(computeNextSortState(sort, columnKey, direction));
22
+ }, [sort, setSort]);
23
+ return { sort, setSort, handleSort, defaultSortField, defaultSortDirection };
24
+ }
@@ -1,4 +1,4 @@
1
- import { useMemo, useCallback } from 'react';
1
+ import { useMemo } from 'react';
2
2
  import { getPaginationViewModel } from '../utils';
3
3
  /**
4
4
  * Shared pagination controls logic for React UI packages.
@@ -8,12 +8,9 @@ export function usePaginationControls(props) {
8
8
  const { currentPage, pageSize, totalCount, onPageSizeChange, pageSizeOptions, entityLabelPlural } = props;
9
9
  const labelPlural = entityLabelPlural ?? 'items';
10
10
  const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined), [currentPage, pageSize, totalCount, pageSizeOptions]);
11
- const handlePageSizeChange = useCallback((value) => {
12
- onPageSizeChange(value);
13
- }, [onPageSizeChange]);
14
11
  return {
15
12
  labelPlural,
16
13
  vm,
17
- handlePageSizeChange,
14
+ handlePageSizeChange: onPageSizeChange,
18
15
  };
19
16
  }
@@ -8,6 +8,7 @@ import { PEOPLE_SEARCH_DEBOUNCE_MS } from '@alaarab/ogrid-core';
8
8
  export function usePeopleFilterState(params) {
9
9
  const { onUserChange, peopleSearch, isFilterOpen, filterType } = params;
10
10
  const peopleInputRef = useRef(null);
11
+ const focusTimeoutRef = useRef(undefined);
11
12
  const peopleSearchTimeoutRef = useRef(undefined);
12
13
  const [peopleSuggestions, setPeopleSuggestions] = useState([]);
13
14
  const [isPeopleLoading, setIsPeopleLoading] = useState(false);
@@ -18,9 +19,13 @@ export function usePeopleFilterState(params) {
18
19
  setPeopleSearchText('');
19
20
  setPeopleSuggestions([]);
20
21
  if (filterType === 'people') {
21
- setTimeout(() => peopleInputRef.current?.focus(), 50);
22
+ focusTimeoutRef.current = window.setTimeout(() => peopleInputRef.current?.focus(), 50);
22
23
  }
23
24
  }
25
+ return () => {
26
+ if (focusTimeoutRef.current)
27
+ window.clearTimeout(focusTimeoutRef.current);
28
+ };
24
29
  }, [isFilterOpen, filterType]);
25
30
  // People search with debounce
26
31
  useEffect(() => {
@@ -1,4 +1,10 @@
1
1
  import { useState, useCallback, useMemo } from 'react';
2
+ /** Shared display text formatter for select and rich-select editors. */
3
+ export function getSelectDisplayText(value, formatValue) {
4
+ if (formatValue)
5
+ return formatValue(value);
6
+ return value != null ? String(value) : '';
7
+ }
2
8
  /**
3
9
  * Manages searchable rich select editor state with keyboard navigation (arrow keys, enter, escape).
4
10
  * @param params - Values, format function, initial value, and commit/cancel callbacks.
@@ -8,11 +14,7 @@ export function useRichSelectState(params) {
8
14
  const { values, formatValue, onCommit, onCancel } = params;
9
15
  const [searchText, setSearchText] = useState('');
10
16
  const [highlightedIndex, setHighlightedIndex] = useState(0);
11
- const getDisplayText = useCallback((value) => {
12
- if (formatValue)
13
- return formatValue(value);
14
- return value != null ? String(value) : '';
15
- }, [formatValue]);
17
+ const getDisplayText = useCallback((value) => getSelectDisplayText(value, formatValue), [formatValue]);
16
18
  const filteredValues = useMemo(() => {
17
19
  if (!searchText.trim())
18
20
  return values;
@@ -1,5 +1,6 @@
1
1
  import { useState, useCallback, useRef, useMemo } from 'react';
2
2
  import { useLatestRef } from './useLatestRef';
3
+ import { applyRangeRowSelection, computeRowSelectionState } from '../utils';
3
4
  /**
4
5
  * Manages row selection state for single or multiple selection modes with shift-click range support.
5
6
  * @param params - Items, getRowId, selection mode, controlled state, and selection change callback.
@@ -33,22 +34,13 @@ export function useRowSelection(params) {
33
34
  lastClickedRowRef.current = rowIndex;
34
35
  return;
35
36
  }
36
- const next = new Set(selectedRowIdsRef.current);
37
37
  const currentItems = itemsRef.current;
38
+ let next;
38
39
  if (shiftKey && lastClickedRowRef.current >= 0 && lastClickedRowRef.current !== rowIndex) {
39
- const start = Math.min(lastClickedRowRef.current, rowIndex);
40
- const end = Math.max(lastClickedRowRef.current, rowIndex);
41
- for (let i = start; i <= end; i++) {
42
- if (i < currentItems.length) {
43
- const id = getRowId(currentItems[i]);
44
- if (checked)
45
- next.add(id);
46
- else
47
- next.delete(id);
48
- }
49
- }
40
+ next = applyRangeRowSelection(lastClickedRowRef.current, rowIndex, checked, currentItems, getRowId, selectedRowIdsRef.current);
50
41
  }
51
42
  else {
43
+ next = new Set(selectedRowIdsRef.current);
52
44
  if (checked)
53
45
  next.add(rowId);
54
46
  else
@@ -56,9 +48,7 @@ export function useRowSelection(params) {
56
48
  }
57
49
  lastClickedRowRef.current = rowIndex;
58
50
  updateSelection(next);
59
- },
60
- // eslint-disable-next-line react-hooks/exhaustive-deps -- itemsRef, selectedRowIdsRef are stable refs
61
- [rowSelection, getRowId, updateSelection]);
51
+ }, [rowSelection, getRowId, updateSelection, itemsRef, selectedRowIdsRef]);
62
52
  const handleSelectAll = useCallback((checked) => {
63
53
  if (checked) {
64
54
  updateSelection(new Set(items.map((item) => getRowId(item))));
@@ -67,17 +57,7 @@ export function useRowSelection(params) {
67
57
  updateSelection(new Set());
68
58
  }
69
59
  }, [items, getRowId, updateSelection]);
70
- const allSelected = useMemo(() => {
71
- if (selectedRowIds.size === 0 || items.length === 0)
72
- return false;
73
- return items.every((item) => selectedRowIds.has(getRowId(item)));
74
- }, [items, selectedRowIds, getRowId]);
75
- const someSelected = useMemo(() => {
76
- if (allSelected)
77
- return false;
78
- // No iteration needed — any selected row means "some" are selected
79
- return selectedRowIds.size > 0;
80
- }, [allSelected, selectedRowIds.size]);
60
+ const { allSelected, someSelected } = useMemo(() => computeRowSelectionState(selectedRowIds, items, getRowId), [items, selectedRowIds, getRowId]);
81
61
  return {
82
62
  selectedRowIds,
83
63
  updateSelection,
@@ -1,4 +1,5 @@
1
1
  import { useState, useCallback, useEffect, useRef } from 'react';
2
+ import { getSelectDisplayText } from './useRichSelectState';
2
3
  /**
3
4
  * Manages select editor state with keyboard navigation (arrow keys, enter, escape).
4
5
  * Simpler than useRichSelectState — no search, just a dropdown list.
@@ -6,11 +7,7 @@ import { useState, useCallback, useEffect, useRef } from 'react';
6
7
  export function useSelectState(params) {
7
8
  const { values, formatValue, initialValue, onCommit, onCancel } = params;
8
9
  const dropdownRef = useRef(null);
9
- const getDisplayText = useCallback((value) => {
10
- if (formatValue)
11
- return formatValue(value);
12
- return value != null ? String(value) : '';
13
- }, [formatValue]);
10
+ const getDisplayText = useCallback((value) => getSelectDisplayText(value, formatValue), [formatValue]);
14
11
  // Start highlighted on current value
15
12
  const initialIndex = values.findIndex((v) => String(v) === String(initialValue));
16
13
  const [highlightedIndex, setHighlightedIndex] = useState(Math.max(initialIndex, 0));
@@ -0,0 +1,14 @@
1
+ import { useRef } from 'react';
2
+ /**
3
+ * Returns a referentially stable value as long as the comparator considers
4
+ * the new value equal to the previous one. Unlike the broken
5
+ * `useMemo` + `useRef` + `useEffect` pattern, this works correctly because
6
+ * it compares *before* React's own reference check on the dependency array.
7
+ */
8
+ export function useShallowEqualMemo(value, isEqual) {
9
+ const ref = useRef(value);
10
+ if (!isEqual(value, ref.current)) {
11
+ ref.current = value;
12
+ }
13
+ return ref.current;
14
+ }
@@ -23,8 +23,7 @@ export function useTableLayout(params) {
23
23
  ro.observe(el);
24
24
  measure();
25
25
  return () => ro.disconnect();
26
- // eslint-disable-next-line react-hooks/exhaustive-deps
27
- }, []); // wrapperRef excluded — refs are stable
26
+ }, [wrapperRef]);
28
27
  // --- Column sizing overrides state ---
29
28
  const [columnSizingOverrides, setColumnSizingOverrides] = useState(() => {
30
29
  if (!initialColumnWidths)
@@ -44,15 +43,8 @@ export function useTableLayout(params) {
44
43
  useEffect(() => {
45
44
  const colIds = new Set(flatColumns.map((c) => c.columnId));
46
45
  setColumnSizingOverrides((prev) => {
47
- const next = { ...prev };
48
- let changed = false;
49
- for (const id of Object.keys(next)) {
50
- if (!colIds.has(id)) {
51
- delete next[id];
52
- changed = true;
53
- }
54
- }
55
- return changed ? next : prev;
46
+ const kept = Object.fromEntries(Object.entries(prev).filter(([id]) => colIds.has(id)));
47
+ return Object.keys(kept).length !== Object.keys(prev).length ? kept : prev;
56
48
  });
57
49
  }, [flatColumns]);
58
50
  // --- Desired table width calculation ---
@@ -12,30 +12,36 @@ export function useUndoRedo(params) {
12
12
  }
13
13
  const [historyLength, setHistoryLength] = useState(0);
14
14
  const [redoLength, setRedoLength] = useState(0);
15
+ const getStack = useCallback(() => {
16
+ const s = stackRef.current;
17
+ if (!s)
18
+ throw new Error('UndoRedoStack not initialized');
19
+ return s;
20
+ }, []);
15
21
  const wrapped = useCallback((event) => {
16
22
  if (!onCellValueChanged)
17
23
  return;
18
- const stack = stackRef.current;
24
+ const stack = getStack();
19
25
  stack.record(event);
20
26
  if (!stack.isBatching) {
21
27
  setHistoryLength(stack.historyLength);
22
28
  setRedoLength(stack.redoLength);
23
29
  }
24
30
  onCellValueChanged(event);
25
- }, [onCellValueChanged]);
31
+ }, [onCellValueChanged, getStack]);
26
32
  const beginBatch = useCallback(() => {
27
- stackRef.current.beginBatch();
28
- }, []);
33
+ getStack().beginBatch();
34
+ }, [getStack]);
29
35
  const endBatch = useCallback(() => {
30
- const stack = stackRef.current;
36
+ const stack = getStack();
31
37
  stack.endBatch();
32
38
  setHistoryLength(stack.historyLength);
33
39
  setRedoLength(stack.redoLength);
34
- }, []);
40
+ }, [getStack]);
35
41
  const undo = useCallback(() => {
36
42
  if (!onCellValueChanged)
37
43
  return;
38
- const stack = stackRef.current;
44
+ const stack = getStack();
39
45
  const lastBatch = stack.undo();
40
46
  if (!lastBatch)
41
47
  return;
@@ -50,11 +56,11 @@ export function useUndoRedo(params) {
50
56
  newValue: ev.oldValue,
51
57
  });
52
58
  }
53
- }, [onCellValueChanged]);
59
+ }, [onCellValueChanged, getStack]);
54
60
  const redo = useCallback(() => {
55
61
  if (!onCellValueChanged)
56
62
  return;
57
- const stack = stackRef.current;
63
+ const stack = getStack();
58
64
  const nextBatch = stack.redo();
59
65
  if (!nextBatch)
60
66
  return;
@@ -64,7 +70,7 @@ export function useUndoRedo(params) {
64
70
  for (const ev of nextBatch) {
65
71
  onCellValueChanged(ev);
66
72
  }
67
- }, [onCellValueChanged]);
73
+ }, [onCellValueChanged, getStack]);
68
74
  return {
69
75
  onCellValueChanged: onCellValueChanged ? wrapped : undefined,
70
76
  undo,
package/dist/esm/index.js CHANGED
@@ -2,7 +2,7 @@
2
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
- export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSelectState, useSideBarState, useTableLayout, useColumnReorder, useVirtualScroll, useListVirtualizer, useLatestRef, usePaginationControls, useDataGridTableOrchestration, } from './hooks';
5
+ export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSelectState, useSideBarState, useTableLayout, useColumnReorder, useVirtualScroll, useListVirtualizer, useLatestRef, usePaginationControls, useDataGridTableOrchestration, useColumnMeta, } from './hooks';
6
6
  // Constants
7
7
  export { GRID_ROOT_STYLE, CURSOR_CELL_STYLE, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, NOOP, STOP_PROPAGATION, } from './constants/domHelpers';
8
8
  // Components
@@ -1,5 +1,5 @@
1
1
  // Shared utilities re-exported from core
2
- export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, computeNextSortState, measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, UndoRedoStack, } from '@alaarab/ogrid-core';
2
+ export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, computeNextSortState, measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, applyFillValues, computeArrowNavigation, applyCellDeletion, applyRangeRowSelection, computeRowSelectionState, UndoRedoStack, } from '@alaarab/ogrid-core';
3
3
  // View model utilities (re-exported from core + React-specific getCellInteractionProps)
4
4
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './dataGridViewModel';
5
5
  export { areGridRowPropsEqual, isRowInRange } from './gridRowComparator';
@@ -8,5 +8,7 @@ export interface IColumnChooserProps {
8
8
  columns: IColumnDefinition[];
9
9
  visibleColumns: Set<string>;
10
10
  onVisibilityChange: (columnKey: string, visible: boolean) => void;
11
+ /** Optional batch setter — used by select-all / clear-all for a single state update. */
12
+ onSetVisibleColumns?: (columns: Set<string>) => void;
11
13
  className?: string;
12
14
  }
@@ -44,8 +44,6 @@ export interface DateFilterClassNames {
44
44
  export declare const DateFilterContent: React.FC<DateFilterContentProps>;
45
45
  export declare function getColumnHeaderFilterStateParams(props: IColumnHeaderFilterProps): {
46
46
  filterType: ColumnFilterType;
47
- isSorted: boolean;
48
- isSortedDescending: boolean;
49
47
  onSort: (() => void) | undefined;
50
48
  selectedValues: string[] | undefined;
51
49
  onFilterChange: ((values: string[]) => void) | undefined;
@@ -2,6 +2,14 @@ export { useFilterOptions } from './useFilterOptions';
2
2
  export type { UseFilterOptionsResult } from './useFilterOptions';
3
3
  export { useOGrid } from './useOGrid';
4
4
  export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, } from './useOGrid';
5
+ export { useOGridPagination } from './useOGridPagination';
6
+ export type { UseOGridPaginationParams, UseOGridPaginationState } from './useOGridPagination';
7
+ export { useOGridSorting } from './useOGridSorting';
8
+ export type { UseOGridSortingParams, UseOGridSortingState, SortState } from './useOGridSorting';
9
+ export { useOGridFilters as useOGridFiltersState } from './useOGridFilters';
10
+ export type { UseOGridFiltersParams, UseOGridFiltersState } from './useOGridFilters';
11
+ export { useOGridDataFetching } from './useOGridDataFetching';
12
+ export type { UseOGridDataFetchingParams, UseOGridDataFetchingState } from './useOGridDataFetching';
5
13
  export { useActiveCell } from './useActiveCell';
6
14
  export type { UseActiveCellResult } from './useActiveCell';
7
15
  export { useCellEditing } from './useCellEditing';
@@ -23,6 +31,14 @@ export { useFillHandle } from './useFillHandle';
23
31
  export type { UseFillHandleResult, UseFillHandleParams } from './useFillHandle';
24
32
  export { useDataGridState } from './useDataGridState';
25
33
  export type { UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, } from './useDataGridState';
34
+ export { useDataGridLayout } from './useDataGridLayout';
35
+ export type { UseDataGridLayoutParams, UseDataGridLayoutResult } from './useDataGridLayout';
36
+ export { useDataGridEditing } from './useDataGridEditing';
37
+ export type { UseDataGridEditingParams, UseDataGridEditingResult } from './useDataGridEditing';
38
+ export { useDataGridInteraction } from './useDataGridInteraction';
39
+ export type { UseDataGridInteractionParams, UseDataGridInteractionResult } from './useDataGridInteraction';
40
+ export { useDataGridContextMenu } from './useDataGridContextMenu';
41
+ export type { UseDataGridContextMenuParams, UseDataGridContextMenuResult } from './useDataGridContextMenu';
26
42
  export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
27
43
  export type { UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, } from './useColumnHeaderFilterState';
28
44
  export { useTextFilterState } from './useTextFilterState';
@@ -58,7 +74,10 @@ export type { IVirtualScrollConfig, UseVirtualScrollParams, UseVirtualScrollResu
58
74
  export { useListVirtualizer } from './useListVirtualizer';
59
75
  export type { UseListVirtualizerOptions, UseListVirtualizerResult, VirtualItem, } from './useListVirtualizer';
60
76
  export { useLatestRef } from './useLatestRef';
77
+ export { useShallowEqualMemo } from './useShallowEqualMemo';
61
78
  export { usePaginationControls } from './usePaginationControls';
62
79
  export type { UsePaginationControlsProps, UsePaginationControlsResult, } from './usePaginationControls';
63
80
  export { useDataGridTableOrchestration } from './useDataGridTableOrchestration';
64
81
  export type { UseDataGridTableOrchestrationParams, UseDataGridTableOrchestrationResult, } from './useDataGridTableOrchestration';
82
+ export { useColumnMeta } from './useColumnMeta';
83
+ export type { UseColumnMetaParams, ColumnMetaResult } from './useColumnMeta';
@@ -14,7 +14,6 @@ export interface UseClipboardResult {
14
14
  handleCopy: () => void;
15
15
  handleCut: () => void;
16
16
  handlePaste: () => Promise<void>;
17
- cutRangeRef: React.MutableRefObject<ISelectionRange | null>;
18
17
  /** Current cut range for UI (marching ants). Null when no cut or after paste. */
19
18
  cutRange: ISelectionRange | null;
20
19
  /** Current copy range for UI (marching ants). Null when no copy or after paste/cut. */
@@ -7,6 +7,8 @@ export interface UseColumnChooserStateParams {
7
7
  columns: IColumnDefinition[];
8
8
  visibleColumns: Set<string>;
9
9
  onVisibilityChange: (columnKey: string, visible: boolean) => void;
10
+ /** Optional batch setter for select-all / clear-all — avoids N individual callbacks. */
11
+ onSetVisibleColumns?: (columns: Set<string>) => void;
10
12
  }
11
13
  export interface UseColumnChooserStateResult {
12
14
  open: boolean;
@@ -8,8 +8,6 @@ import type { ColumnFilterType, IDateFilterValue } from '../types/columnTypes';
8
8
  import type { UserLike } from '../types/dataGridTypes';
9
9
  export interface UseColumnHeaderFilterStateParams {
10
10
  filterType: ColumnFilterType;
11
- isSorted?: boolean;
12
- isSortedDescending?: boolean;
13
11
  onSort?: () => void;
14
12
  selectedValues?: string[];
15
13
  onFilterChange?: (values: string[]) => void;
@@ -14,8 +14,6 @@ export interface UseColumnHeaderMenuStateParams {
14
14
  sortable?: boolean;
15
15
  resizable?: boolean;
16
16
  }>;
17
- data: unknown[];
18
- getRowId: (item: unknown) => string | number;
19
17
  }
20
18
  export interface UseColumnHeaderMenuStateResult {
21
19
  isOpen: boolean;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared hook that pre-computes per-column styles and class names for DataGridTable.
3
+ * Extracted from Radix/Fluent/Material DataGridTable to avoid duplication.
4
+ *
5
+ * @param params.addStickyPosition - When true, adds `position: 'sticky'` inline for pinned columns.
6
+ * This is needed by Fluent UI whose `TableCell` injects atomic `position: relative` via CSS-in-JS,
7
+ * overriding the shared `.pinnedColLeft { position: sticky }` class. Radix/Material don't need it.
8
+ */
9
+ import type { IColumnDef } from '../types';
10
+ export interface UseColumnMetaParams<T> {
11
+ visibleCols: IColumnDef<T>[];
12
+ getColumnWidth: (col: IColumnDef<T>) => number;
13
+ columnSizingOverrides: Record<string, {
14
+ widthPx: number;
15
+ }>;
16
+ measuredColumnWidths: Record<string, number>;
17
+ pinnedColumns: Record<string, 'left' | 'right'>;
18
+ leftOffsets: Record<string, number>;
19
+ rightOffsets: Record<string, number>;
20
+ pinnedColLeftClass: string;
21
+ pinnedColRightClass: string;
22
+ /** When true, adds `position: sticky` inline to pinned cells (Fluent-specific). */
23
+ addStickyPosition?: boolean;
24
+ }
25
+ export interface ColumnMetaResult {
26
+ cellStyles: Record<string, React.CSSProperties>;
27
+ cellClasses: Record<string, string>;
28
+ hdrStyles: Record<string, React.CSSProperties>;
29
+ hdrClasses: Record<string, string>;
30
+ }
31
+ /**
32
+ * Computes per-column styles and class names once per render, avoiding per-cell object creation.
33
+ */
34
+ export declare function useColumnMeta<T>(params: UseColumnMetaParams<T>): ColumnMetaResult;
@@ -0,0 +1,20 @@
1
+ import type { DataGridContextMenuState } from './useDataGridState';
2
+ export interface UseDataGridContextMenuParams {
3
+ cellSelection: boolean;
4
+ }
5
+ export interface UseDataGridContextMenuResult {
6
+ contextMenu: DataGridContextMenuState;
7
+ contextMenuPosition: {
8
+ x: number;
9
+ y: number;
10
+ } | null;
11
+ setContextMenuPosition: (pos: {
12
+ x: number;
13
+ y: number;
14
+ } | null) => void;
15
+ }
16
+ /**
17
+ * Manages context menu position and handlers.
18
+ * Extracted from useDataGridState for modularity.
19
+ */
20
+ export declare function useDataGridContextMenu(params: UseDataGridContextMenuParams): UseDataGridContextMenuResult;