@opencorestack/opengridx 2.1.0 → 3.0.1
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/CHANGELOG.md +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -166
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +6 -7
- package/docs/components/datagrid.md +15 -13
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +90 -38
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +126 -39
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +19 -4
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +279 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +104 -5
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +17 -5
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +82 -51
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +55 -5
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import type { GridRowId, GridRowMeta, GridRowModel } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface UseGridLiveRowSelectionParams {
|
|
5
|
+
/** The selection model (controlled prop or internal state). */
|
|
6
|
+
selectedRowIds: Set<GridRowId>;
|
|
7
|
+
/** The row store: ids of the rows the grid holds. */
|
|
8
|
+
rowsLookup: ReadonlyMap<GridRowId, GridRowModel>;
|
|
9
|
+
/** Hierarchy rows the grid made (group rows, generated tree parents); their ids are kept. */
|
|
10
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
11
|
+
/**
|
|
12
|
+
* Prune only when the grid holds every row (client rows, not a server page or a pivot of
|
|
13
|
+
* them); otherwise ids of rows that are not loaded are kept.
|
|
14
|
+
*/
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
/** Applies a model: internal state when uncontrolled, plus onRowSelectionModelChange. */
|
|
17
|
+
onSelectionModelChange: (model: GridRowId[]) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Content key of a set of ids; the type is part of it, so 1 and '1' stay apart.
|
|
21
|
+
const idsKey = (ids: Iterable<GridRowId>): string =>
|
|
22
|
+
JSON.stringify(Array.from(ids, id => (typeof id === 'number' ? ['n', id] : ['s', id])));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The selection the grid acts on: the model without the ids of rows that no longer exist.
|
|
26
|
+
* Everything that reads or builds on the selection (row highlighting, the header checkbox,
|
|
27
|
+
* `getSelectedRows`, the next model a click produces) uses this set. When rows are removed,
|
|
28
|
+
* the pruned model is reported once through `onSelectionModelChange`, so an uncontrolled
|
|
29
|
+
* selection forgets the ids (a row that comes back is not selected again) and a controlled
|
|
30
|
+
* consumer can follow. A pruned model is reported once per distinct (model, pruned model) pair:
|
|
31
|
+
* a controlled parent that keeps passing the stale ids (a fresh inline array each render, or its
|
|
32
|
+
* own state that re-adds them) is not notified again and cannot loop, and StrictMode's re-run of
|
|
33
|
+
* the effect does not report twice.
|
|
34
|
+
*/
|
|
35
|
+
export function useGridLiveRowSelection(params: UseGridLiveRowSelectionParams): Set<GridRowId> {
|
|
36
|
+
const { selectedRowIds, rowsLookup, rowMetaMap, enabled, onSelectionModelChange } = params;
|
|
37
|
+
|
|
38
|
+
const liveRowIds = useMemo(() => {
|
|
39
|
+
if (!enabled || selectedRowIds.size === 0) return selectedRowIds;
|
|
40
|
+
let stale = false;
|
|
41
|
+
const live = new Set<GridRowId>();
|
|
42
|
+
for (const id of selectedRowIds) {
|
|
43
|
+
if (rowsLookup.has(id) || rowMetaMap.has(id)) live.add(id);
|
|
44
|
+
else stale = true;
|
|
45
|
+
}
|
|
46
|
+
return stale ? live : selectedRowIds;
|
|
47
|
+
}, [enabled, selectedRowIds, rowsLookup, rowMetaMap]);
|
|
48
|
+
|
|
49
|
+
// Latest callback, so an inline onRowSelectionModelChange does not report again on every render.
|
|
50
|
+
const onChangeRef = useRef(onSelectionModelChange);
|
|
51
|
+
useLayoutEffect(() => { onChangeRef.current = onSelectionModelChange; });
|
|
52
|
+
|
|
53
|
+
// Survives StrictMode's effect re-run and re-renders; cleared once nothing is stale.
|
|
54
|
+
const reportedRef = useRef<string | null>(null);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (liveRowIds === selectedRowIds) {
|
|
57
|
+
reportedRef.current = null;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const key = `${idsKey(selectedRowIds)}|${idsKey(liveRowIds)}`;
|
|
61
|
+
if (reportedRef.current === key) return;
|
|
62
|
+
reportedRef.current = key;
|
|
63
|
+
onChangeRef.current(Array.from(liveRowIds));
|
|
64
|
+
}, [liveRowIds, selectedRowIds]);
|
|
65
|
+
|
|
66
|
+
return liveRowIds;
|
|
67
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import type { GridPaginationModel } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface UseGridPageCorrectionParams {
|
|
5
|
+
/** The pagination model the grid was given (controlled) or holds (uncontrolled). */
|
|
6
|
+
paginationModel: GridPaginationModel;
|
|
7
|
+
/** The page the row pipeline actually shows: the requested page clamped to the last page. */
|
|
8
|
+
currentPage: number;
|
|
9
|
+
/**
|
|
10
|
+
* Whether the row count is settled. While data is loading, or while there are no rows at all
|
|
11
|
+
* (rows not fetched yet), a clamp would throw away a restored page such as
|
|
12
|
+
* `initialState.pagination`, so the model is only corrected once rows are present.
|
|
13
|
+
*/
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
/** Stores the model when uncontrolled and calls `onPaginationModelChange`. */
|
|
16
|
+
onPaginationModelChange: (model: GridPaginationModel) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* When the rows shrink (new `rows`, a filter, collapsed tree nodes) the requested page can end up
|
|
21
|
+
* past the last page. The row pipeline already renders the clamped page during render; this hook
|
|
22
|
+
* reports the corrected model so the stored model, the consumer's state and later Previous/Next
|
|
23
|
+
* navigation agree with what is on screen. Reporting a model is a side effect, so it runs in an
|
|
24
|
+
* effect, keyed on the page numbers only: a consumer that passes a fresh model object every render
|
|
25
|
+
* or ignores the callback is told once per change, not on every render, and StrictMode's re-run
|
|
26
|
+
* of the effect does not report it twice.
|
|
27
|
+
*/
|
|
28
|
+
export function useGridPageCorrection({
|
|
29
|
+
paginationModel,
|
|
30
|
+
currentPage,
|
|
31
|
+
enabled,
|
|
32
|
+
onPaginationModelChange,
|
|
33
|
+
}: UseGridPageCorrectionParams): void {
|
|
34
|
+
const latestRef = useRef({ paginationModel, onPaginationModelChange });
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
latestRef.current = { paginationModel, onPaginationModelChange };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const requestedPage = paginationModel.page;
|
|
40
|
+
|
|
41
|
+
// The correction last reported; cleared once the requested page is the shown one again.
|
|
42
|
+
const reportedRef = useRef<string | null>(null);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!enabled) return;
|
|
45
|
+
if (currentPage === requestedPage) {
|
|
46
|
+
reportedRef.current = null;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const key = `${requestedPage}>${currentPage}`;
|
|
50
|
+
if (reportedRef.current === key) return;
|
|
51
|
+
reportedRef.current = key;
|
|
52
|
+
const { paginationModel: model, onPaginationModelChange: notify } = latestRef.current;
|
|
53
|
+
notify({ ...model, page: currentPage });
|
|
54
|
+
}, [enabled, currentPage, requestedPage]);
|
|
55
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import type { GridRowId, GridRowModel } from '../../types';
|
|
3
|
+
|
|
4
|
+
/** The default `getRowId`: the row's own `id` field. */
|
|
5
|
+
export function getDefaultRowId(row: GridRowModel): GridRowId {
|
|
6
|
+
return row.id;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseGridRowIdOfResult {
|
|
10
|
+
/**
|
|
11
|
+
* The key of any row the grid renders: stored rows resolve through the store, and grid-made
|
|
12
|
+
* rows (group rows, skeletons, generated tree parents) through their own `id`.
|
|
13
|
+
*/
|
|
14
|
+
getRowIdOf: (row: GridRowModel) => GridRowId;
|
|
15
|
+
/**
|
|
16
|
+
* Same resolver with a stable identity, for event handlers only (it reads the last committed
|
|
17
|
+
* store), so column definitions are not rebuilt on every rows change.
|
|
18
|
+
*/
|
|
19
|
+
getRowIdOfInEvents: (row: GridRowModel) => GridRowId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Row-id resolvers over the row store's `idByRow` map. */
|
|
23
|
+
export function useGridRowIdOf(idByRow: Map<GridRowModel, GridRowId>): UseGridRowIdOfResult {
|
|
24
|
+
const getRowIdOf = useCallback(
|
|
25
|
+
(row: GridRowModel): GridRowId => idByRow.get(row) ?? row.id,
|
|
26
|
+
[idByRow]
|
|
27
|
+
);
|
|
28
|
+
const getRowIdOfRef = useRef(getRowIdOf);
|
|
29
|
+
useLayoutEffect(() => { getRowIdOfRef.current = getRowIdOf; });
|
|
30
|
+
const getRowIdOfInEvents = useCallback((row: GridRowModel) => getRowIdOfRef.current(row), []);
|
|
31
|
+
return { getRowIdOf, getRowIdOfInEvents };
|
|
32
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { useGridRowSelection, type UseGridRowSelectionReturn } from './useGridRowSelection';
|
|
3
|
+
import { PIVOT_GRAND_TOTAL_ID } from '../../utils/pivot';
|
|
4
|
+
import { isSyntheticRowId } from '../../utils/syntheticRows';
|
|
5
|
+
import type { GridRowId, GridRowMeta, GridRowModel, GridRowParams } from '../../types';
|
|
6
|
+
|
|
7
|
+
export interface UseGridRowInteractionsParams<R extends GridRowModel> {
|
|
8
|
+
/** Data rows that pass the filter (pinned included, synthetic group rows excluded). */
|
|
9
|
+
dataRows: R[];
|
|
10
|
+
getRowId: (row: GridRowModel) => GridRowId;
|
|
11
|
+
isPivotActive: boolean;
|
|
12
|
+
selectedRowIds: Set<GridRowId>;
|
|
13
|
+
onSelectionModelChange: (model: GridRowId[]) => void;
|
|
14
|
+
disableMultipleRowSelection: boolean;
|
|
15
|
+
disableRowSelectionOnClick: boolean;
|
|
16
|
+
isHierarchyEnabled: boolean;
|
|
17
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
18
|
+
/** The active tree-data / row-grouping handlers, if any. */
|
|
19
|
+
activeHierarchyHandlers: { toggleExpansion: (id: GridRowId) => void } | null;
|
|
20
|
+
onRowClick: ((params: GridRowParams<R>) => void) | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseGridRowInteractionsResult<R extends GridRowModel> {
|
|
24
|
+
rowSelection: UseGridRowSelectionReturn;
|
|
25
|
+
/** Row click (and keyboard row activation). */
|
|
26
|
+
handleRowClick: (params: GridRowParams<R>) => void;
|
|
27
|
+
/** Row checkbox / Space key. */
|
|
28
|
+
handleSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Row selection and the row-level click / checkbox handlers. */
|
|
32
|
+
export function useGridRowInteractions<R extends GridRowModel>(
|
|
33
|
+
params: UseGridRowInteractionsParams<R>
|
|
34
|
+
): UseGridRowInteractionsResult<R> {
|
|
35
|
+
const {
|
|
36
|
+
dataRows, getRowId, isPivotActive, selectedRowIds, onSelectionModelChange, disableMultipleRowSelection,
|
|
37
|
+
disableRowSelectionOnClick, isHierarchyEnabled, rowMetaMap, activeHierarchyHandlers, onRowClick,
|
|
38
|
+
} = params;
|
|
39
|
+
|
|
40
|
+
// Select-all acts on, and the header checkbox reflects, the data rows that pass the
|
|
41
|
+
// filter: never rows hidden by it, synthetic group rows, the pivot grand total, or stale
|
|
42
|
+
// ids in the selection.
|
|
43
|
+
const selectableRowIds = useMemo(() => {
|
|
44
|
+
const ids = dataRows.map(getRowId);
|
|
45
|
+
return isPivotActive ? ids.filter(id => id !== PIVOT_GRAND_TOTAL_ID) : ids;
|
|
46
|
+
}, [dataRows, getRowId, isPivotActive]);
|
|
47
|
+
const rowSelection = useGridRowSelection({
|
|
48
|
+
selectedRowIds,
|
|
49
|
+
onSelectionModelChange,
|
|
50
|
+
disableMultipleRowSelection,
|
|
51
|
+
selectableRowIds,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const handleRowClick = useCallback((rowParams: GridRowParams<R>) => {
|
|
55
|
+
const { id } = rowParams;
|
|
56
|
+
|
|
57
|
+
// Synthetic group rows toggle on click and are never selected. Tree-data parents are real rows:
|
|
58
|
+
// they fire onRowClick and select like any row, and expand through their chevron.
|
|
59
|
+
const rowMeta = isHierarchyEnabled ? rowMetaMap.get(id) : undefined;
|
|
60
|
+
if (rowMeta?.isGroupRow) {
|
|
61
|
+
if (rowMeta.hasChildren) activeHierarchyHandlers?.toggleExpansion(id);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onRowClick?.(rowParams);
|
|
66
|
+
|
|
67
|
+
// The pivot Grand Total is clickable (onRowClick) but, like any synthetic row, not selectable.
|
|
68
|
+
if (!disableRowSelectionOnClick && !isSyntheticRowId(id, rowMetaMap)) {
|
|
69
|
+
rowSelection.clickRow(id);
|
|
70
|
+
}
|
|
71
|
+
}, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick, rowMetaMap, disableRowSelectionOnClick, rowSelection]);
|
|
72
|
+
|
|
73
|
+
// Honors disableMultipleRowSelection like a row click does. Synthetic group / subtotal rows
|
|
74
|
+
// and the pivot Grand Total are not data: their ids never enter the selection model.
|
|
75
|
+
const handleSelectionChange = useCallback((rowId: GridRowId, isSelected: boolean) => {
|
|
76
|
+
if (isSyntheticRowId(rowId, rowMetaMap)) return;
|
|
77
|
+
rowSelection.toggleRow(rowId, isSelected);
|
|
78
|
+
}, [rowMetaMap, rowSelection]);
|
|
79
|
+
|
|
80
|
+
return { rowSelection, handleRowClick, handleSelectionChange };
|
|
81
|
+
}
|
|
@@ -2,10 +2,13 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import { filterRows } from '../../utils/filtering';
|
|
3
3
|
import { sortRows } from '../../utils/sorting';
|
|
4
4
|
import { getPinnedRowGroups } from '../../utils/pinning';
|
|
5
|
-
import
|
|
5
|
+
import { normalizePageSize, getPageCount, clampPage } from '../../utils/pagination';
|
|
6
|
+
import type { GridColumnLookup } from '../../utils/columnLookup';
|
|
7
|
+
import type { GridRowModel, GridRowId, GridFilterModel, GridSortItem, GridPaginationModel, GridRowPinning } from '../../types';
|
|
6
8
|
|
|
7
9
|
interface HierarchyHandlers<R extends GridRowModel> {
|
|
8
|
-
|
|
10
|
+
/** `labelField` is the column showing the hierarchy, so sorting by it can order group rows by their labels. */
|
|
11
|
+
getVisibleRows: (options?: { labelField?: string; expandAll?: boolean }) => R[] | null;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export interface UseGridRowPipelineParams<R extends GridRowModel> {
|
|
@@ -13,24 +16,49 @@ export interface UseGridRowPipelineParams<R extends GridRowModel> {
|
|
|
13
16
|
activeHierarchyHandlers: HierarchyHandlers<R> | null;
|
|
14
17
|
filterMode: string;
|
|
15
18
|
filterModel: GridFilterModel;
|
|
16
|
-
dataSource?: GridDataSource<R>;
|
|
17
19
|
sortModel: GridSortItem[];
|
|
18
20
|
sortingMode: string;
|
|
19
21
|
pagination: boolean;
|
|
20
22
|
paginationMode: string;
|
|
21
23
|
effectivePaginationModel: GridPaginationModel;
|
|
22
24
|
pinnedRows?: GridRowPinning;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
/** Resolves a row's id for pinnedRows lookups; defaults to `row.id`. */
|
|
26
|
+
getRowId?: (row: R) => GridRowId;
|
|
27
|
+
/**
|
|
28
|
+
* Column definitions for client-side filtering and sorting: cells are read through
|
|
29
|
+
* `valueGetter`, `type` drives numeric/date comparison, and the quick filter searches only
|
|
30
|
+
* the visible, filterable columns. Without it, `row[field]` is read directly.
|
|
31
|
+
*/
|
|
32
|
+
columnLookup?: GridColumnLookup;
|
|
33
|
+
/** Tree data / row grouping: the column that shows the expand toggle and group labels. */
|
|
34
|
+
hierarchyField?: string;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export interface GridRowPipelineResult<R extends GridRowModel> {
|
|
28
38
|
filteredRows: R[];
|
|
39
|
+
/**
|
|
40
|
+
* The filtered data rows, independent of hierarchy expansion. Under row grouping and tree
|
|
41
|
+
* data, filteredRows is the visible hierarchy instead (group rows that already carry their
|
|
42
|
+
* subtotals plus whichever children are expanded), so anything that totals or counts rows
|
|
43
|
+
* must use dataRows.
|
|
44
|
+
*/
|
|
45
|
+
dataRows: R[];
|
|
29
46
|
pinnedTopRows: R[];
|
|
30
47
|
unpinnedRows: R[];
|
|
31
48
|
pinnedBottomRows: R[];
|
|
32
49
|
sortedUnpinnedRows: R[];
|
|
33
50
|
paginatedUnpinnedRows: R[];
|
|
51
|
+
/**
|
|
52
|
+
* The page the rows are sliced at: `effectivePaginationModel.page` clamped to the last page
|
|
53
|
+
* when the grid pages client-side, so a page left past the end after the rows shrink shows
|
|
54
|
+
* the last page instead of nothing. Under server pagination it is the requested page.
|
|
55
|
+
*/
|
|
56
|
+
currentPage: number;
|
|
57
|
+
/**
|
|
58
|
+
* Pinned-top rows, the rows on the current page (all rows without pagination) and
|
|
59
|
+
* pinned-bottom rows: exactly the rows the grid view renders. Loading placeholders for
|
|
60
|
+
* infinite scroll are not included; GridVirtualRows draws those itself.
|
|
61
|
+
*/
|
|
34
62
|
allRenderableRows: R[];
|
|
35
63
|
}
|
|
36
64
|
|
|
@@ -42,68 +70,70 @@ export function useGridRowPipeline<R extends GridRowModel>(
|
|
|
42
70
|
activeHierarchyHandlers,
|
|
43
71
|
filterMode,
|
|
44
72
|
filterModel,
|
|
45
|
-
dataSource,
|
|
46
73
|
sortModel,
|
|
47
74
|
sortingMode,
|
|
48
75
|
pagination,
|
|
49
76
|
paginationMode,
|
|
50
77
|
effectivePaginationModel,
|
|
51
78
|
pinnedRows,
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
getRowId,
|
|
80
|
+
columnLookup,
|
|
81
|
+
hierarchyField,
|
|
54
82
|
} = params;
|
|
55
83
|
|
|
84
|
+
// Server modes mean "the rows are already filtered/sorted/paged", with or without a
|
|
85
|
+
// dataSource (a consumer can fetch pages itself from onPaginationModelChange etc.).
|
|
56
86
|
const filteredRows = useMemo<R[]>(() => {
|
|
57
|
-
if (activeHierarchyHandlers) return (activeHierarchyHandlers.getVisibleRows() || []) as R[];
|
|
58
|
-
if (filterMode === 'server'
|
|
59
|
-
return filterRows(effectiveRows, filterModel) as R[];
|
|
60
|
-
}, [effectiveRows, filterModel, activeHierarchyHandlers, filterMode,
|
|
87
|
+
if (activeHierarchyHandlers) return (activeHierarchyHandlers.getVisibleRows({ labelField: hierarchyField }) || []) as R[];
|
|
88
|
+
if (filterMode === 'server') return effectiveRows;
|
|
89
|
+
return filterRows(effectiveRows, filterModel, columnLookup) as R[];
|
|
90
|
+
}, [effectiveRows, filterModel, activeHierarchyHandlers, hierarchyField, filterMode, columnLookup]);
|
|
91
|
+
|
|
92
|
+
const dataRows = useMemo<R[]>(() => {
|
|
93
|
+
if (!activeHierarchyHandlers) return filteredRows;
|
|
94
|
+
if (filterMode === 'server') return effectiveRows;
|
|
95
|
+
return filterRows(effectiveRows, filterModel, columnLookup) as R[];
|
|
96
|
+
}, [activeHierarchyHandlers, filteredRows, filterMode, effectiveRows, filterModel, columnLookup]);
|
|
61
97
|
|
|
62
98
|
const { top: pinnedTopRows, center: unpinnedRows, bottom: pinnedBottomRows } = useMemo(() => {
|
|
63
99
|
if (activeHierarchyHandlers) return { top: [] as R[], center: filteredRows, bottom: [] as R[] };
|
|
64
|
-
return getPinnedRowGroups(filteredRows, pinnedRows);
|
|
65
|
-
}, [filteredRows, pinnedRows, activeHierarchyHandlers]);
|
|
100
|
+
return getPinnedRowGroups(filteredRows, pinnedRows, getRowId);
|
|
101
|
+
}, [filteredRows, pinnedRows, activeHierarchyHandlers, getRowId]);
|
|
66
102
|
|
|
67
103
|
const sortedUnpinnedRows = useMemo<R[]>(() => {
|
|
68
104
|
if (activeHierarchyHandlers) return unpinnedRows;
|
|
69
|
-
if (sortingMode === 'server'
|
|
70
|
-
return sortRows(unpinnedRows, sortModel) as R[];
|
|
71
|
-
}, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode,
|
|
105
|
+
if (sortingMode === 'server') return unpinnedRows;
|
|
106
|
+
return sortRows(unpinnedRows, sortModel, columnLookup) as R[];
|
|
107
|
+
}, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, columnLookup]);
|
|
108
|
+
|
|
109
|
+
const isClientPaged = pagination && paginationMode !== 'server';
|
|
110
|
+
const pageSize = normalizePageSize(effectivePaginationModel.pageSize);
|
|
111
|
+
const currentPage = isClientPaged
|
|
112
|
+
? clampPage(effectivePaginationModel.page, getPageCount(sortedUnpinnedRows.length, pageSize))
|
|
113
|
+
: effectivePaginationModel.page;
|
|
72
114
|
|
|
73
115
|
const paginatedUnpinnedRows = useMemo<R[]>(() => {
|
|
74
|
-
if (!
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}, [sortedUnpinnedRows, pagination, effectivePaginationModel.page, effectivePaginationModel.pageSize, paginationMode, dataSource]);
|
|
116
|
+
if (!isClientPaged) return sortedUnpinnedRows;
|
|
117
|
+
const start = currentPage * pageSize;
|
|
118
|
+
return sortedUnpinnedRows.slice(start, start + pageSize);
|
|
119
|
+
}, [sortedUnpinnedRows, isClientPaged, currentPage, pageSize]);
|
|
79
120
|
|
|
121
|
+
// What keyboard navigation walks: the rendered rows. With tree data and pagination that is the
|
|
122
|
+
// current page, the same as a flat grid (hierarchy modes have no pinned rows).
|
|
80
123
|
const allRenderableRows = useMemo<R[]>(() => {
|
|
81
|
-
if (activeHierarchyHandlers) return unpinnedRows;
|
|
82
124
|
const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (paginationMode === 'infinite' && isLoading && base.length > 0) {
|
|
86
|
-
const skeletonCount = Math.min(pageSize, 20);
|
|
87
|
-
const skeletons = Array.from({ length: skeletonCount }, (_, i) => ({
|
|
88
|
-
id: `__skeleton_${i}__`,
|
|
89
|
-
_isSkeleton: true,
|
|
90
|
-
}));
|
|
91
|
-
return [...base, ...skeletons] as R[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return base;
|
|
95
|
-
}, [
|
|
96
|
-
pinnedTopRows, paginatedUnpinnedRows, sortedUnpinnedRows, pinnedBottomRows,
|
|
97
|
-
pagination, activeHierarchyHandlers, unpinnedRows, paginationMode, isLoading, pageSize,
|
|
98
|
-
]);
|
|
125
|
+
return [...pinnedTopRows, ...centerRows, ...pinnedBottomRows];
|
|
126
|
+
}, [pinnedTopRows, paginatedUnpinnedRows, sortedUnpinnedRows, pinnedBottomRows, pagination]);
|
|
99
127
|
|
|
100
128
|
return {
|
|
101
129
|
filteredRows,
|
|
130
|
+
dataRows,
|
|
102
131
|
pinnedTopRows,
|
|
103
132
|
unpinnedRows,
|
|
104
133
|
pinnedBottomRows,
|
|
105
134
|
sortedUnpinnedRows,
|
|
106
135
|
paginatedUnpinnedRows,
|
|
136
|
+
currentPage,
|
|
107
137
|
allRenderableRows,
|
|
108
138
|
};
|
|
109
139
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import type { GridRowId } from '../../types';
|
|
3
|
+
|
|
4
|
+
// ── Pure selection transitions (shared by the UI handlers and the imperative API) ──────────
|
|
5
|
+
|
|
6
|
+
/** Selects or deselects `ids`. With `single`, selecting keeps only the last id. */
|
|
7
|
+
export function nextSelectionForRows(
|
|
8
|
+
current: ReadonlySet<GridRowId>,
|
|
9
|
+
ids: readonly GridRowId[],
|
|
10
|
+
isSelected: boolean,
|
|
11
|
+
single: boolean
|
|
12
|
+
): GridRowId[] {
|
|
13
|
+
if (isSelected && single) {
|
|
14
|
+
return ids.length > 0 ? [ids[ids.length - 1]] : Array.from(current);
|
|
15
|
+
}
|
|
16
|
+
const next = new Set(current);
|
|
17
|
+
ids.forEach(id => (isSelected ? next.add(id) : next.delete(id)));
|
|
18
|
+
return Array.from(next);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A row click toggles the row; with `single` it replaces the selection instead of adding to it. */
|
|
22
|
+
export function nextSelectionForClick(current: ReadonlySet<GridRowId>, id: GridRowId, single: boolean): GridRowId[] {
|
|
23
|
+
if (single) return current.has(id) ? [] : [id];
|
|
24
|
+
return nextSelectionForRows(current, [id], !current.has(id), false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether `next` holds exactly the ids of `current` (order ignored). */
|
|
28
|
+
export function isSameSelection(current: ReadonlySet<GridRowId>, next: readonly GridRowId[]): boolean {
|
|
29
|
+
return next.length === current.size && next.every(id => current.has(id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UseGridRowSelectionParams {
|
|
33
|
+
selectedRowIds: Set<GridRowId>;
|
|
34
|
+
/** Applies a new model: internal state when uncontrolled, plus onRowSelectionModelChange. */
|
|
35
|
+
onSelectionModelChange: (model: GridRowId[]) => void;
|
|
36
|
+
disableMultipleRowSelection: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The rows the header checkbox acts on and reflects: the data rows that pass the filter
|
|
39
|
+
* (including pinned rows, excluding synthetic group rows).
|
|
40
|
+
*/
|
|
41
|
+
selectableRowIds: readonly GridRowId[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UseGridRowSelectionReturn {
|
|
45
|
+
/** Checkbox / Space-key toggle of one row. */
|
|
46
|
+
toggleRow: (id: GridRowId, isSelected: boolean) => void;
|
|
47
|
+
/** Row-click toggle. */
|
|
48
|
+
clickRow: (id: GridRowId) => void;
|
|
49
|
+
/** Header checkbox; undefined when disableMultipleRowSelection is set (no select-all). */
|
|
50
|
+
selectAll: ((isSelected: boolean) => void) | undefined;
|
|
51
|
+
allSelected: boolean;
|
|
52
|
+
someSelected: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useGridRowSelection(params: UseGridRowSelectionParams): UseGridRowSelectionReturn {
|
|
56
|
+
const { selectedRowIds, onSelectionModelChange, disableMultipleRowSelection, selectableRowIds } = params;
|
|
57
|
+
|
|
58
|
+
const commit = useCallback((next: GridRowId[]) => {
|
|
59
|
+
if (isSameSelection(selectedRowIds, next)) return;
|
|
60
|
+
onSelectionModelChange(next);
|
|
61
|
+
}, [selectedRowIds, onSelectionModelChange]);
|
|
62
|
+
|
|
63
|
+
const toggleRow = useCallback((id: GridRowId, isSelected: boolean) => {
|
|
64
|
+
commit(nextSelectionForRows(selectedRowIds, [id], isSelected, disableMultipleRowSelection));
|
|
65
|
+
}, [commit, selectedRowIds, disableMultipleRowSelection]);
|
|
66
|
+
|
|
67
|
+
const clickRow = useCallback((id: GridRowId) => {
|
|
68
|
+
commit(nextSelectionForClick(selectedRowIds, id, disableMultipleRowSelection));
|
|
69
|
+
}, [commit, selectedRowIds, disableMultipleRowSelection]);
|
|
70
|
+
|
|
71
|
+
// Select-all adds the selectable rows to the selection and deselect-all removes them, so
|
|
72
|
+
// rows hidden by the filter are never selected by it (and keep their own state).
|
|
73
|
+
const selectAllHandler = useCallback((isSelected: boolean) => {
|
|
74
|
+
commit(nextSelectionForRows(selectedRowIds, selectableRowIds, isSelected, false));
|
|
75
|
+
}, [commit, selectedRowIds, selectableRowIds]);
|
|
76
|
+
|
|
77
|
+
const { allSelected, someSelected } = useMemo(() => {
|
|
78
|
+
let selectedCount = 0;
|
|
79
|
+
for (const id of selectableRowIds) if (selectedRowIds.has(id)) selectedCount++;
|
|
80
|
+
const all = selectableRowIds.length > 0 && selectedCount === selectableRowIds.length;
|
|
81
|
+
return { allSelected: all, someSelected: !all && selectedCount > 0 };
|
|
82
|
+
}, [selectableRowIds, selectedRowIds]);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
toggleRow,
|
|
86
|
+
clickRow,
|
|
87
|
+
selectAll: disableMultipleRowSelection ? undefined : selectAllHandler,
|
|
88
|
+
allSelected,
|
|
89
|
+
someSelected,
|
|
90
|
+
};
|
|
91
|
+
}
|