@alaarab/ogrid-react 2.1.15 → 2.2.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.
package/dist/esm/index.js CHANGED
@@ -715,6 +715,285 @@ function calculateDropTarget(params) {
715
715
  }
716
716
  return { targetIndex, indicatorX };
717
717
  }
718
+ function computeVisibleColumnRange(scrollLeft, columnWidths, containerWidth, overscan = 2) {
719
+ if (columnWidths.length === 0 || containerWidth <= 0) {
720
+ return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
721
+ }
722
+ let cumWidth = 0;
723
+ let rawStart = columnWidths.length;
724
+ let rawEnd = -1;
725
+ for (let i = 0; i < columnWidths.length; i++) {
726
+ const colStart = cumWidth;
727
+ cumWidth += columnWidths[i];
728
+ if (cumWidth > scrollLeft && rawStart === columnWidths.length) {
729
+ rawStart = i;
730
+ }
731
+ if (colStart < scrollLeft + containerWidth) {
732
+ rawEnd = i;
733
+ }
734
+ }
735
+ if (rawStart > rawEnd) {
736
+ return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
737
+ }
738
+ const startIndex = Math.max(0, rawStart - overscan);
739
+ const endIndex = Math.min(columnWidths.length - 1, rawEnd + overscan);
740
+ let leftOffset = 0;
741
+ for (let i = 0; i < startIndex; i++) {
742
+ leftOffset += columnWidths[i];
743
+ }
744
+ let rightOffset = 0;
745
+ for (let i = endIndex + 1; i < columnWidths.length; i++) {
746
+ rightOffset += columnWidths[i];
747
+ }
748
+ return { startIndex, endIndex, leftOffset, rightOffset };
749
+ }
750
+ function partitionColumnsForVirtualization(visibleCols, columnRange, pinnedColumns) {
751
+ const pinnedLeft = [];
752
+ const pinnedRight = [];
753
+ const unpinned = [];
754
+ for (const col of visibleCols) {
755
+ const pin = pinnedColumns?.[col.columnId];
756
+ if (pin === "left") pinnedLeft.push(col);
757
+ else if (pin === "right") pinnedRight.push(col);
758
+ else unpinned.push(col);
759
+ }
760
+ if (!columnRange || columnRange.endIndex < 0) {
761
+ return {
762
+ pinnedLeft,
763
+ virtualizedUnpinned: unpinned,
764
+ pinnedRight,
765
+ leftSpacerWidth: 0,
766
+ rightSpacerWidth: 0
767
+ };
768
+ }
769
+ const virtualizedUnpinned = unpinned.slice(columnRange.startIndex, columnRange.endIndex + 1);
770
+ return {
771
+ pinnedLeft,
772
+ virtualizedUnpinned,
773
+ pinnedRight,
774
+ leftSpacerWidth: columnRange.leftOffset,
775
+ rightSpacerWidth: columnRange.rightOffset
776
+ };
777
+ }
778
+ function workerBody() {
779
+ const ctx = self;
780
+ ctx.onmessage = (e) => {
781
+ const msg = e.data;
782
+ if (msg.type !== "sort-filter") return;
783
+ const { requestId, values, filters, sort } = msg;
784
+ const rowCount = values.length;
785
+ let indices = [];
786
+ const filterEntries = Object.entries(filters);
787
+ if (filterEntries.length === 0) {
788
+ indices = new Array(rowCount);
789
+ for (let i = 0; i < rowCount; i++) indices[i] = i;
790
+ } else {
791
+ for (let r = 0; r < rowCount; r++) {
792
+ let pass = true;
793
+ for (let f = 0; f < filterEntries.length; f++) {
794
+ const colIdx = Number(filterEntries[f][0]);
795
+ const filter = filterEntries[f][1];
796
+ const cellVal = values[r][colIdx];
797
+ switch (filter.type) {
798
+ case "text": {
799
+ const trimmed = filter.value.trim().toLowerCase();
800
+ if (trimmed && !String(cellVal ?? "").toLowerCase().includes(trimmed)) {
801
+ pass = false;
802
+ }
803
+ break;
804
+ }
805
+ case "multiSelect": {
806
+ if (filter.value.length > 0) {
807
+ const set = new Set(filter.value);
808
+ if (!set.has(String(cellVal ?? ""))) {
809
+ pass = false;
810
+ }
811
+ }
812
+ break;
813
+ }
814
+ case "date": {
815
+ if (cellVal == null) {
816
+ pass = false;
817
+ break;
818
+ }
819
+ const ts = new Date(String(cellVal)).getTime();
820
+ if (isNaN(ts)) {
821
+ pass = false;
822
+ break;
823
+ }
824
+ if (filter.value.from) {
825
+ const fromTs = (/* @__PURE__ */ new Date(filter.value.from + "T00:00:00")).getTime();
826
+ if (ts < fromTs) {
827
+ pass = false;
828
+ break;
829
+ }
830
+ }
831
+ if (filter.value.to) {
832
+ const toTs = (/* @__PURE__ */ new Date(filter.value.to + "T23:59:59.999")).getTime();
833
+ if (ts > toTs) {
834
+ pass = false;
835
+ break;
836
+ }
837
+ }
838
+ break;
839
+ }
840
+ }
841
+ if (!pass) break;
842
+ }
843
+ if (pass) indices.push(r);
844
+ }
845
+ }
846
+ if (sort) {
847
+ const { columnIndex, direction } = sort;
848
+ const dir = direction === "asc" ? 1 : -1;
849
+ indices.sort((a, b) => {
850
+ const av = values[a][columnIndex];
851
+ const bv = values[b][columnIndex];
852
+ if (av == null && bv == null) return 0;
853
+ if (av == null) return -1 * dir;
854
+ if (bv == null) return 1 * dir;
855
+ if (typeof av === "number" && typeof bv === "number") {
856
+ return av === bv ? 0 : av > bv ? dir : -dir;
857
+ }
858
+ const sa = String(av).toLowerCase();
859
+ const sb = String(bv).toLowerCase();
860
+ return sa === sb ? 0 : sa > sb ? dir : -dir;
861
+ });
862
+ }
863
+ const response = {
864
+ type: "sort-filter-result",
865
+ requestId,
866
+ indices
867
+ };
868
+ ctx.postMessage(response);
869
+ };
870
+ }
871
+ var workerInstance = null;
872
+ var requestCounter = 0;
873
+ var pendingRequests = /* @__PURE__ */ new Map();
874
+ function createSortFilterWorker() {
875
+ if (workerInstance) return workerInstance;
876
+ if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") {
877
+ return null;
878
+ }
879
+ try {
880
+ const fnStr = workerBody.toString();
881
+ const blob = new Blob(
882
+ [`(${fnStr})()`],
883
+ { type: "application/javascript" }
884
+ );
885
+ const url = URL.createObjectURL(blob);
886
+ workerInstance = new Worker(url);
887
+ URL.revokeObjectURL(url);
888
+ workerInstance.onmessage = (e) => {
889
+ const { requestId, indices } = e.data;
890
+ const pending = pendingRequests.get(requestId);
891
+ if (pending) {
892
+ pendingRequests.delete(requestId);
893
+ pending.resolve(indices);
894
+ }
895
+ };
896
+ workerInstance.onerror = (err) => {
897
+ for (const [id, pending] of pendingRequests) {
898
+ pending.reject(new Error(err.message || "Worker error"));
899
+ pendingRequests.delete(id);
900
+ }
901
+ };
902
+ return workerInstance;
903
+ } catch {
904
+ return null;
905
+ }
906
+ }
907
+ function extractValueMatrix(data, columns) {
908
+ const matrix = new Array(data.length);
909
+ for (let r = 0; r < data.length; r++) {
910
+ const row = new Array(columns.length);
911
+ for (let c = 0; c < columns.length; c++) {
912
+ const val = getCellValue(data[r], columns[c]);
913
+ if (val == null) {
914
+ row[c] = null;
915
+ } else if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
916
+ row[c] = val;
917
+ } else {
918
+ row[c] = String(val);
919
+ }
920
+ }
921
+ matrix[r] = row;
922
+ }
923
+ return matrix;
924
+ }
925
+ function processClientSideDataAsync(data, columns, filters, sortBy, sortDirection) {
926
+ if (sortBy) {
927
+ const sortCol = columns.find((c) => c.columnId === sortBy);
928
+ if (sortCol?.compare) {
929
+ return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
930
+ }
931
+ }
932
+ const worker = createSortFilterWorker();
933
+ if (!worker) {
934
+ return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
935
+ }
936
+ const columnIndexMap = /* @__PURE__ */ new Map();
937
+ for (let i = 0; i < columns.length; i++) {
938
+ columnIndexMap.set(columns[i].columnId, i);
939
+ }
940
+ const values = extractValueMatrix(data, columns);
941
+ const columnMeta = columns.map((col, idx) => ({
942
+ type: col.type ?? "text",
943
+ index: idx
944
+ }));
945
+ const workerFilters = {};
946
+ for (const col of columns) {
947
+ const filterKey = getFilterField(col);
948
+ const val = filters[filterKey];
949
+ if (!val) continue;
950
+ const colIdx = columnIndexMap.get(col.columnId);
951
+ if (colIdx === void 0) continue;
952
+ switch (val.type) {
953
+ case "text":
954
+ workerFilters[colIdx] = { type: "text", value: val.value };
955
+ break;
956
+ case "multiSelect":
957
+ workerFilters[colIdx] = { type: "multiSelect", value: val.value };
958
+ break;
959
+ case "date":
960
+ workerFilters[colIdx] = { type: "date", value: { from: val.value.from, to: val.value.to } };
961
+ break;
962
+ // 'people' filter has a UserLike object — fall back to sync
963
+ case "people":
964
+ return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
965
+ }
966
+ }
967
+ let sort;
968
+ if (sortBy) {
969
+ const sortColIdx = columnIndexMap.get(sortBy);
970
+ if (sortColIdx !== void 0) {
971
+ sort = { columnIndex: sortColIdx, direction: sortDirection ?? "asc" };
972
+ }
973
+ }
974
+ const requestId = ++requestCounter;
975
+ return new Promise((resolve, reject) => {
976
+ pendingRequests.set(requestId, {
977
+ resolve: (indices) => {
978
+ const result = new Array(indices.length);
979
+ for (let i = 0; i < indices.length; i++) {
980
+ result[i] = data[indices[i]];
981
+ }
982
+ resolve(result);
983
+ },
984
+ reject
985
+ });
986
+ const request = {
987
+ type: "sort-filter",
988
+ requestId,
989
+ values,
990
+ columnMeta,
991
+ filters: workerFilters,
992
+ sort
993
+ };
994
+ worker.postMessage(request);
995
+ });
996
+ }
718
997
  function getHeaderFilterConfig(col, input) {
719
998
  const filterable = isFilterConfig(col.filterable) ? col.filterable : null;
720
999
  const filterType = filterable?.type ?? "none";
@@ -1719,11 +1998,13 @@ function useOGridDataFetching(params) {
1719
1998
  page,
1720
1999
  pageSize,
1721
2000
  onError,
1722
- onFirstDataRendered
2001
+ onFirstDataRendered,
2002
+ workerSort
1723
2003
  } = params;
1724
2004
  const isClientSide = !isServerSide;
2005
+ const useWorker = workerSort === true || workerSort === "auto" && displayData.length > 5e3;
1725
2006
  const clientItemsAndTotal = useMemo(() => {
1726
- if (!isClientSide) return null;
2007
+ if (!isClientSide || useWorker) return null;
1727
2008
  const rows = processClientSideData(
1728
2009
  displayData,
1729
2010
  columns,
@@ -1735,7 +2016,29 @@ function useOGridDataFetching(params) {
1735
2016
  const start = (page - 1) * pageSize;
1736
2017
  const paged = rows.slice(start, start + pageSize);
1737
2018
  return { items: paged, totalCount: total };
1738
- }, [isClientSide, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
2019
+ }, [isClientSide, useWorker, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
2020
+ const [asyncItems, setAsyncItems] = useState(null);
2021
+ const asyncIdRef = useRef(0);
2022
+ useEffect(() => {
2023
+ if (!isClientSide || !useWorker) {
2024
+ setAsyncItems(null);
2025
+ return;
2026
+ }
2027
+ const id = ++asyncIdRef.current;
2028
+ processClientSideDataAsync(
2029
+ displayData,
2030
+ columns,
2031
+ stableFilters,
2032
+ sort.field,
2033
+ sort.direction
2034
+ ).then((rows) => {
2035
+ if (id !== asyncIdRef.current) return;
2036
+ const total = rows.length;
2037
+ const start = (page - 1) * pageSize;
2038
+ const paged = rows.slice(start, start + pageSize);
2039
+ setAsyncItems({ items: paged, totalCount: total });
2040
+ });
2041
+ }, [isClientSide, useWorker, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
1739
2042
  const [serverItems, setServerItems] = useState([]);
1740
2043
  const [serverTotalCount, setServerTotalCount] = useState(0);
1741
2044
  const [serverLoading, setServerLoading] = useState(true);
@@ -1769,8 +2072,9 @@ function useOGridDataFetching(params) {
1769
2072
  if (id === fetchIdRef.current) setServerLoading(false);
1770
2073
  });
1771
2074
  }, [isServerSide, page, pageSize, sort.field, sort.direction, stableFilters, refreshCounter, dataSourceRef, onErrorRef]);
1772
- const displayItems = isClientSide && clientItemsAndTotal ? clientItemsAndTotal.items : serverItems;
1773
- const displayTotalCount = isClientSide && clientItemsAndTotal ? clientItemsAndTotal.totalCount : serverTotalCount;
2075
+ const clientResult = clientItemsAndTotal ?? asyncItems;
2076
+ const displayItems = isClientSide && clientResult ? clientResult.items : serverItems;
2077
+ const displayTotalCount = isClientSide && clientResult ? clientResult.totalCount : serverTotalCount;
1774
2078
  const onFirstDataRenderedRef = useLatestRef(onFirstDataRendered);
1775
2079
  const firstDataRenderedRef = useRef(false);
1776
2080
  useEffect(() => {
@@ -1877,6 +2181,7 @@ function useOGrid(props, ref) {
1877
2181
  virtualScroll,
1878
2182
  rowHeight,
1879
2183
  density = "normal",
2184
+ workerSort,
1880
2185
  "aria-label": ariaLabel,
1881
2186
  "aria-labelledby": ariaLabelledBy
1882
2187
  } = props;
@@ -1950,7 +2255,8 @@ function useOGrid(props, ref) {
1950
2255
  page: paginationState.page,
1951
2256
  pageSize: paginationState.pageSize,
1952
2257
  onError,
1953
- onFirstDataRendered
2258
+ onFirstDataRendered,
2259
+ workerSort
1954
2260
  });
1955
2261
  useEffect(() => {
1956
2262
  const items = dataFetchingState.displayItems;
@@ -2008,7 +2314,13 @@ function useOGrid(props, ref) {
2008
2314
  [selectedRows, onSelectionChange]
2009
2315
  );
2010
2316
  const [columnWidthOverrides, setColumnWidthOverrides] = useState({});
2011
- const [pinnedOverrides, setPinnedOverrides] = useState({});
2317
+ const [pinnedOverrides, setPinnedOverrides] = useState(() => {
2318
+ const initial = {};
2319
+ for (const col of flattenColumns(columnsProp)) {
2320
+ if (col.pinned) initial[col.columnId] = col.pinned;
2321
+ }
2322
+ return initial;
2323
+ });
2012
2324
  const handleColumnResized = useCallback(
2013
2325
  (columnId, width) => {
2014
2326
  setColumnWidthOverrides((prev) => ({ ...prev, [columnId]: width }));
@@ -6023,7 +6335,10 @@ function useVirtualScroll(params) {
6023
6335
  enabled,
6024
6336
  overscan = 5,
6025
6337
  threshold = DEFAULT_PASSTHROUGH_THRESHOLD,
6026
- containerRef
6338
+ containerRef,
6339
+ columnVirtualization = false,
6340
+ columnWidths,
6341
+ columnOverscan = 2
6027
6342
  } = params;
6028
6343
  useEffect(() => {
6029
6344
  validateVirtualScrollConfig({ enabled, rowHeight });
@@ -6082,11 +6397,51 @@ function useVirtualScroll(params) {
6082
6397
  },
6083
6398
  [isActive, containerRef, rowHeight]
6084
6399
  );
6400
+ const [scrollLeft, setScrollLeft] = useState(0);
6401
+ const scrollLeftRaf = useRef(0);
6402
+ const onHorizontalScroll = useCallback(
6403
+ (sl) => {
6404
+ if (scrollLeftRaf.current) cancelAnimationFrame(scrollLeftRaf.current);
6405
+ scrollLeftRaf.current = requestAnimationFrame(() => {
6406
+ scrollLeftRaf.current = 0;
6407
+ setScrollLeft(sl);
6408
+ });
6409
+ },
6410
+ []
6411
+ );
6412
+ useEffect(() => {
6413
+ return () => {
6414
+ if (scrollLeftRaf.current) cancelAnimationFrame(scrollLeftRaf.current);
6415
+ };
6416
+ }, []);
6417
+ const [containerWidth, setContainerWidth] = useState(0);
6418
+ useEffect(() => {
6419
+ if (!columnVirtualization) return;
6420
+ const el = containerRef.current;
6421
+ if (!el) return;
6422
+ setContainerWidth(el.clientWidth);
6423
+ if (typeof ResizeObserver === "undefined") return;
6424
+ const ro = new ResizeObserver((entries) => {
6425
+ if (entries.length > 0) {
6426
+ setContainerWidth(entries[0].contentRect.width);
6427
+ }
6428
+ });
6429
+ ro.observe(el);
6430
+ return () => ro.disconnect();
6431
+ }, [columnVirtualization, containerRef]);
6432
+ const columnRange = useMemo(() => {
6433
+ if (!columnVirtualization || !columnWidths || columnWidths.length === 0 || containerWidth <= 0) {
6434
+ return null;
6435
+ }
6436
+ return computeVisibleColumnRange(scrollLeft, columnWidths, containerWidth, columnOverscan);
6437
+ }, [columnVirtualization, columnWidths, containerWidth, scrollLeft, columnOverscan]);
6085
6438
  return {
6086
6439
  virtualizer: isActive ? virtualizer : null,
6087
6440
  totalHeight,
6088
6441
  visibleRange: activeRange,
6089
- scrollToIndex
6442
+ scrollToIndex,
6443
+ columnRange,
6444
+ onHorizontalScroll: columnVirtualization ? onHorizontalScroll : void 0
6090
6445
  };
6091
6446
  }
6092
6447
  function useListVirtualizer(opts) {
@@ -6197,13 +6552,28 @@ function useDataGridTableOrchestration(params) {
6197
6552
  });
6198
6553
  const virtualScrollEnabled = virtualScroll?.enabled === true;
6199
6554
  const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
6200
- const { visibleRange } = useVirtualScroll({
6555
+ const columnVirtualization = virtualScroll?.columns === true;
6556
+ const unpinnedColumnWidths = useMemo(() => {
6557
+ if (!columnVirtualization) return void 0;
6558
+ const widths = [];
6559
+ for (const col of visibleCols) {
6560
+ const pin = pinnedColumns?.[col.columnId];
6561
+ if (!pin) {
6562
+ widths.push(getColumnWidth(col));
6563
+ }
6564
+ }
6565
+ return widths;
6566
+ }, [columnVirtualization, visibleCols, pinnedColumns, getColumnWidth]);
6567
+ const { visibleRange, columnRange, onHorizontalScroll } = useVirtualScroll({
6201
6568
  totalRows: items.length,
6202
6569
  rowHeight: virtualRowHeight,
6203
6570
  enabled: virtualScrollEnabled,
6204
6571
  overscan: virtualScroll?.overscan,
6205
6572
  threshold: virtualScroll?.threshold,
6206
- containerRef: wrapperRef
6573
+ containerRef: wrapperRef,
6574
+ columnVirtualization,
6575
+ columnWidths: unpinnedColumnWidths,
6576
+ columnOverscan: virtualScroll?.columnOverscan
6207
6577
  });
6208
6578
  const editCallbacks = useMemo(
6209
6579
  () => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }),
@@ -6259,6 +6629,8 @@ function useDataGridTableOrchestration(params) {
6259
6629
  virtualScrollEnabled,
6260
6630
  virtualRowHeight,
6261
6631
  visibleRange,
6632
+ columnRange,
6633
+ onHorizontalScroll,
6262
6634
  // Derived from props
6263
6635
  items,
6264
6636
  columns,
@@ -7561,4 +7933,4 @@ function renderFilterContent(filterType, state, options, isLoadingOptions, selec
7561
7933
  return null;
7562
7934
  }
7563
7935
 
7564
- export { BaseColumnHeaderMenu, BaseDropIndicator, BaseEmptyState, BaseInlineCellEditor, BaseLoadingOverlay, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CURSOR_CELL_STYLE, CellDescriptorCache, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, DateFilterContent, EmptyState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GRID_ROOT_STYLE, GridContextMenu, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NOOP3 as NOOP, OGridLayout, PAGE_SIZE_OPTIONS, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, ROW_NUMBER_COLUMN_WIDTH, STOP_PROPAGATION, SideBar, StatusBar, UndoRedoStack, areGridRowPropsEqual, booleanParser, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps2 as buildInlineEditorProps, buildPopoverEditorProps2 as buildPopoverEditorProps, clampSelectionToBounds, computeAggregations, computeAutoScrollSpeed, computeTabNavigation, createOGrid, currencyParser, dateParser, deriveFilterOptionsFromData, editorInputStyle, editorWrapperStyle, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellInteractionProps, getCellRenderDescriptor, getCellValue, getColumnHeaderFilterStateParams, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getDateFilterContentProps, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getStatusBarParts, isInSelectionRange, isRowInRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, processClientSideData, rangesEqual, renderFilterContent, resolveCellDisplayContent2 as resolveCellDisplayContent, resolveCellStyle2 as resolveCellStyle, richSelectDropdownStyle, richSelectNoMatchesStyle, richSelectOptionHighlightedStyle, richSelectOptionStyle, richSelectWrapperStyle, selectChevronStyle, selectDisplayStyle, selectEditorStyle, toUserLike, triggerCsvDownload, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnMeta, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableOrchestration, useDateFilterState, useDebounce, useFillHandle, useFilterOptions, useInlineCellEditorState, useKeyboardNavigation, useLatestRef, useListVirtualizer, useMultiSelectFilterState, useOGrid, usePaginationControls, usePeopleFilterState, useRichSelectState, useRowSelection, useSelectState, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
7936
+ export { BaseColumnHeaderMenu, BaseDropIndicator, BaseEmptyState, BaseInlineCellEditor, BaseLoadingOverlay, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CURSOR_CELL_STYLE, CellDescriptorCache, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, DateFilterContent, EmptyState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GRID_ROOT_STYLE, GridContextMenu, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NOOP3 as NOOP, OGridLayout, PAGE_SIZE_OPTIONS, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, ROW_NUMBER_COLUMN_WIDTH, STOP_PROPAGATION, SideBar, StatusBar, UndoRedoStack, areGridRowPropsEqual, booleanParser, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps2 as buildInlineEditorProps, buildPopoverEditorProps2 as buildPopoverEditorProps, clampSelectionToBounds, computeAggregations, computeAutoScrollSpeed, computeTabNavigation, createOGrid, currencyParser, dateParser, deriveFilterOptionsFromData, editorInputStyle, editorWrapperStyle, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellInteractionProps, getCellRenderDescriptor, getCellValue, getColumnHeaderFilterStateParams, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getDateFilterContentProps, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getStatusBarParts, isInSelectionRange, isRowInRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, rangesEqual, renderFilterContent, resolveCellDisplayContent2 as resolveCellDisplayContent, resolveCellStyle2 as resolveCellStyle, richSelectDropdownStyle, richSelectNoMatchesStyle, richSelectOptionHighlightedStyle, richSelectOptionStyle, richSelectWrapperStyle, selectChevronStyle, selectDisplayStyle, selectEditorStyle, toUserLike, triggerCsvDownload, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnMeta, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableOrchestration, useDateFilterState, useDebounce, useFillHandle, useFilterOptions, useInlineCellEditorState, useKeyboardNavigation, useLatestRef, useListVirtualizer, useMultiSelectFilterState, useOGrid, usePaginationControls, usePeopleFilterState, useRichSelectState, useRowSelection, useSelectState, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
@@ -4,6 +4,7 @@ import type { DataGridLayoutState, DataGridRowSelectionState, DataGridEditingSta
4
4
  import type { UseColumnResizeResult } from './useColumnResize';
5
5
  import type { UseColumnReorderResult } from './useColumnReorder';
6
6
  import type { UseVirtualScrollResult } from './useVirtualScroll';
7
+ import type { IVisibleColumnRange } from '@alaarab/ogrid-core';
7
8
  import type { HeaderFilterConfigInput, CellRenderDescriptorInput } from '../utils';
8
9
  import type { IStatusBarProps, RowId, HeaderRow } from '../types';
9
10
  import { CellDescriptorCache } from '@alaarab/ogrid-core';
@@ -32,6 +33,10 @@ export interface UseDataGridTableOrchestrationResult<T> {
32
33
  virtualScrollEnabled: boolean;
33
34
  virtualRowHeight: number;
34
35
  visibleRange: UseVirtualScrollResult['visibleRange'];
36
+ /** Visible column range for horizontal virtualization (null when disabled). */
37
+ columnRange: IVisibleColumnRange | null;
38
+ /** Callback for horizontal scroll events (column virtualization). */
39
+ onHorizontalScroll?: (scrollLeft: number) => void;
35
40
  items: T[];
36
41
  columns: IOGridDataGridProps<T>['columns'];
37
42
  getRowId: IOGridDataGridProps<T>['getRowId'];
@@ -14,6 +14,8 @@ export interface UseOGridDataFetchingParams<T> {
14
14
  pageSize: number;
15
15
  onError?: (err: unknown) => void;
16
16
  onFirstDataRendered?: () => void;
17
+ /** Worker sort mode: true=always, 'auto'=when data > 5000 rows, false=sync. */
18
+ workerSort?: boolean | 'auto';
17
19
  }
18
20
  export interface UseOGridDataFetchingState<T> {
19
21
  displayItems: T[];
@@ -1,6 +1,6 @@
1
1
  import type { Virtualizer } from '@tanstack/react-virtual';
2
2
  import type { RefObject } from 'react';
3
- import type { IVisibleRange } from '@alaarab/ogrid-core';
3
+ import type { IVisibleRange, IVisibleColumnRange } from '@alaarab/ogrid-core';
4
4
  export type { IVirtualScrollConfig } from '@alaarab/ogrid-core';
5
5
  export interface UseVirtualScrollParams {
6
6
  /** Total number of rows in the data set. */
@@ -18,6 +18,12 @@ export interface UseVirtualScrollParams {
18
18
  threshold?: number;
19
19
  /** Ref to the scrollable container element. */
20
20
  containerRef: RefObject<HTMLElement | null>;
21
+ /** Enable column virtualization (only render visible columns). */
22
+ columnVirtualization?: boolean;
23
+ /** Column widths for horizontal virtualization (unpinned columns only). */
24
+ columnWidths?: number[];
25
+ /** Column overscan count. Default: 2. */
26
+ columnOverscan?: number;
21
27
  }
22
28
  export interface UseVirtualScrollResult {
23
29
  /** The TanStack virtualizer instance (null when disabled). */
@@ -28,11 +34,15 @@ export interface UseVirtualScrollResult {
28
34
  visibleRange: IVisibleRange;
29
35
  /** Scroll to a specific row index. */
30
36
  scrollToIndex: (index: number) => void;
37
+ /** Visible column range for horizontal virtualization (null when column virtualization disabled). */
38
+ columnRange: IVisibleColumnRange | null;
39
+ /** Callback to attach to scroll container's onScroll for horizontal tracking. */
40
+ onHorizontalScroll?: (scrollLeft: number) => void;
31
41
  }
32
42
  /**
33
- * Wraps TanStack Virtual for row virtualization.
43
+ * Wraps TanStack Virtual for row virtualization, with optional column virtualization.
34
44
  * When disabled or when totalRows < threshold, returns a pass-through (all rows visible).
35
- * @param params - Total rows, row height, enabled flag, overscan, threshold, and container ref.
36
- * @returns Virtualizer instance, total height, visible range, and scrollToIndex helper.
45
+ * @param params - Total rows, row height, enabled flag, overscan, threshold, container ref, and column virtualization params.
46
+ * @returns Virtualizer instance, total height, visible range, scrollToIndex, columnRange, and onHorizontalScroll.
37
47
  */
38
48
  export declare function useVirtualScroll(params: UseVirtualScrollParams): UseVirtualScrollResult;
@@ -32,7 +32,7 @@ export { BaseDropIndicator } from './components/BaseDropIndicator';
32
32
  export type { BaseDropIndicatorProps } from './components/BaseDropIndicator';
33
33
  export { DateFilterContent, getColumnHeaderFilterStateParams, getDateFilterContentProps, } from './components/ColumnHeaderFilterContent';
34
34
  export type { IColumnHeaderFilterProps, DateFilterContentProps, DateFilterClassNames, } from './components/ColumnHeaderFilterContent';
35
- export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, CellDescriptorCache, isRowInRange, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, areGridRowPropsEqual, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, UndoRedoStack, } from './utils';
35
+ export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, CellDescriptorCache, isRowInRange, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, partitionColumnsForVirtualization, areGridRowPropsEqual, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, UndoRedoStack, } from './utils';
36
36
  export type { CsvColumn, StatusBarPart, StatusBarPartsInput, GridContextMenuItem, GridContextMenuHandlerProps, PaginationViewModel, HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, ParseValueResult, AggregationResult, GridRowComparatorProps, IColumnHeaderMenuItem, ColumnHeaderMenuInput, ColumnHeaderMenuHandlers, } from './utils';
37
37
  export { renderFilterContent } from './components/ColumnHeaderFilterRenderers';
38
38
  export type { FilterContentRenderers, MultiSelectRendererProps, TextRendererProps, PeopleRendererProps, DateRendererProps, } from './components/ColumnHeaderFilterRenderers';
@@ -80,6 +80,14 @@ interface IOGridBaseProps<T> {
80
80
  rowHeight?: number;
81
81
  /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
82
82
  density?: 'compact' | 'normal' | 'comfortable';
83
+ /**
84
+ * Offload sorting to a Web Worker to avoid blocking the main thread.
85
+ * - `true`: always use worker sort
86
+ * - `'auto'`: use worker sort when data.length > 5000
87
+ * - `false` (default): use synchronous sort
88
+ * Columns with custom `compare` functions fall back to synchronous sort.
89
+ */
90
+ workerSort?: boolean | 'auto';
83
91
  /** Fires once when the grid first renders with data (useful for restoring column state). */
84
92
  onFirstDataRendered?: () => void;
85
93
  /** Called when server-side fetchPage fails. */
@@ -1,4 +1,4 @@
1
- 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, buildCellIndex, } from '@alaarab/ogrid-core';
1
+ 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, partitionColumnsForVirtualization, computeNextSortState, measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, applyFillValues, computeArrowNavigation, applyCellDeletion, applyRangeRowSelection, computeRowSelectionState, UndoRedoStack, buildCellIndex, } from '@alaarab/ogrid-core';
2
2
  export type { CsvColumn, StatusBarPart, StatusBarPartsInput, GridContextMenuItem, GridContextMenuHandlerProps, PaginationViewModel, ParseValueResult, AggregationResult, IColumnHeaderMenuItem, ColumnHeaderMenuInput, ColumnHeaderMenuHandlers, ArrowNavigationContext, ArrowNavigationResult, } from '@alaarab/ogrid-core';
3
3
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './dataGridViewModel';
4
4
  export { CellDescriptorCache } from '@alaarab/ogrid-core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react",
3
- "version": "2.1.15",
3
+ "version": "2.2.0",
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",
@@ -39,7 +39,7 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
- "@alaarab/ogrid-core": "2.1.15",
42
+ "@alaarab/ogrid-core": "2.2.0",
43
43
  "@tanstack/react-virtual": "^3.0.0"
44
44
  },
45
45
  "peerDependencies": {