@opencorestack/opengridx 2.0.4 → 3.0.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/CHANGELOG.md +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- 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 -161
- 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 +45 -22
- package/docs/components/datagrid.md +14 -12
- 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 +100 -36
- 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 +147 -41
- 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 +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -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 +11 -0
- 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 +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- 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 +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- 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 +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- 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 +15 -3
- 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 +284 -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 +123 -0
- 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 +452 -250
- 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 +20 -7
- 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 +86 -90
- 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 +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -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 +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- 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 +344 -121
- 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 +78 -0
- 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 +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- 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/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 -137
- 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,77 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
import type { GridToolbarHost } from './gridToolbarHostContext';
|
|
4
|
+
|
|
5
|
+
export interface UseGridColumnsPanelResult {
|
|
6
|
+
/** Provide through `GridToolbarHostContext` around the toolbar slot. */
|
|
7
|
+
toolbarHost: GridToolbarHost;
|
|
8
|
+
/** Passed to the toolbar as `forceColumnsOpen`. */
|
|
9
|
+
toolbarPanelRequested: boolean;
|
|
10
|
+
/** Passed to the toolbar as `onColumnsPanelClose`. */
|
|
11
|
+
closeToolbarPanel: () => void;
|
|
12
|
+
/** Whether the standalone Columns panel (used when no toolbar can show one) is open. */
|
|
13
|
+
standalonePanelOpen: boolean;
|
|
14
|
+
closeStandalonePanel: () => void;
|
|
15
|
+
/** Attach to the standalone panel; a mousedown outside it closes it. */
|
|
16
|
+
standalonePanelRef: RefObject<HTMLDivElement | null>;
|
|
17
|
+
/** The column menu's **Manage columns** action. */
|
|
18
|
+
openColumnsPanel: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Opens the Columns panel for the column menu: in the toolbar when a `GridToolbar` has registered
|
|
23
|
+
* through `GridToolbarHostContext`, otherwise as the standalone panel, which closes on Escape or a
|
|
24
|
+
* mousedown outside it.
|
|
25
|
+
*/
|
|
26
|
+
export function useGridColumnsPanel(): UseGridColumnsPanelResult {
|
|
27
|
+
const [toolbarPanelRequested, setToolbarPanelRequested] = useState(false);
|
|
28
|
+
const [standalonePanelOpen, setStandalonePanelOpen] = useState(false);
|
|
29
|
+
const standalonePanelRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
const toolbarHostCountRef = useRef(0);
|
|
31
|
+
|
|
32
|
+
const toolbarHost = useMemo<GridToolbarHost>(() => ({
|
|
33
|
+
registerColumnsPanel: () => {
|
|
34
|
+
toolbarHostCountRef.current += 1;
|
|
35
|
+
return () => { toolbarHostCountRef.current -= 1; };
|
|
36
|
+
},
|
|
37
|
+
}), []);
|
|
38
|
+
|
|
39
|
+
const openColumnsPanel = useCallback(() => {
|
|
40
|
+
if (toolbarHostCountRef.current > 0) {
|
|
41
|
+
setToolbarPanelRequested(true);
|
|
42
|
+
} else {
|
|
43
|
+
setStandalonePanelOpen(true);
|
|
44
|
+
}
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const closeToolbarPanel = useCallback(() => setToolbarPanelRequested(false), []);
|
|
48
|
+
const closeStandalonePanel = useCallback(() => setStandalonePanelOpen(false), []);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!standalonePanelOpen) return;
|
|
52
|
+
function handleMouseDown(e: MouseEvent) {
|
|
53
|
+
if (standalonePanelRef.current && !standalonePanelRef.current.contains(e.target as Node)) {
|
|
54
|
+
setStandalonePanelOpen(false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
58
|
+
if (e.key === 'Escape') setStandalonePanelOpen(false);
|
|
59
|
+
}
|
|
60
|
+
document.addEventListener('mousedown', handleMouseDown);
|
|
61
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
62
|
+
return () => {
|
|
63
|
+
document.removeEventListener('mousedown', handleMouseDown);
|
|
64
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
65
|
+
};
|
|
66
|
+
}, [standalonePanelOpen]);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
toolbarHost,
|
|
70
|
+
toolbarPanelRequested,
|
|
71
|
+
closeToolbarPanel,
|
|
72
|
+
standalonePanelOpen,
|
|
73
|
+
closeStandalonePanel,
|
|
74
|
+
standalonePanelRef,
|
|
75
|
+
openColumnsPanel,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { useState, useCallback, useMemo } from 'react';
|
|
1
|
+
import { useState, useCallback, useMemo, useLayoutEffect, useRef } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
GridRowId,
|
|
4
4
|
GridSortItem,
|
|
5
|
+
GridFilterModel,
|
|
5
6
|
GridColumnPinning,
|
|
6
7
|
GridPaginationModel,
|
|
7
8
|
GridAggregationModel,
|
|
8
9
|
GridPivotModel,
|
|
9
10
|
} from '../../types';
|
|
10
|
-
import type { GridInitialState } from '../../state/types';
|
|
11
|
+
import type { GridInitialState, GridDensityState } from '../../state/types';
|
|
12
|
+
|
|
13
|
+
export type GridDensity = GridDensityState['density'];
|
|
11
14
|
|
|
12
15
|
export interface UseGridControlledStateParams {
|
|
13
16
|
initialState?: GridInitialState;
|
|
@@ -15,6 +18,9 @@ export interface UseGridControlledStateParams {
|
|
|
15
18
|
sortModel?: GridSortItem[];
|
|
16
19
|
onSortModelChange?: (model: GridSortItem[]) => void;
|
|
17
20
|
|
|
21
|
+
filterModel?: GridFilterModel;
|
|
22
|
+
onFilterModelChange?: (model: GridFilterModel) => void;
|
|
23
|
+
|
|
18
24
|
aggregationModel?: GridAggregationModel;
|
|
19
25
|
onAggregationModelChange?: (model: GridAggregationModel) => void;
|
|
20
26
|
|
|
@@ -29,9 +35,13 @@ export interface UseGridControlledStateParams {
|
|
|
29
35
|
|
|
30
36
|
paginationModel?: GridPaginationModel;
|
|
31
37
|
onPaginationModelChange?: (model: GridPaginationModel) => void;
|
|
38
|
+
/** Used to pick the uncontrolled default page size: 100 when offered, else the first option. */
|
|
39
|
+
pageSizeOptions?: number[];
|
|
32
40
|
|
|
33
41
|
rowSelectionModel?: GridRowId[];
|
|
34
42
|
onRowSelectionModelChange?: (model: GridRowId[]) => void;
|
|
43
|
+
|
|
44
|
+
density?: GridDensity;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
export interface UseGridControlledStateReturn {
|
|
@@ -39,6 +49,14 @@ export interface UseGridControlledStateReturn {
|
|
|
39
49
|
sortModel: GridSortItem[];
|
|
40
50
|
isSortControlled: boolean;
|
|
41
51
|
setInternalSortModel: React.Dispatch<React.SetStateAction<GridSortItem[]>>;
|
|
52
|
+
/** Sets the internal model when uncontrolled and fires onSortModelChange. */
|
|
53
|
+
handleSortModelChange: (model: GridSortItem[]) => void;
|
|
54
|
+
|
|
55
|
+
// filter
|
|
56
|
+
filterModel: GridFilterModel;
|
|
57
|
+
isFilterControlled: boolean;
|
|
58
|
+
/** Sets the internal model when uncontrolled and fires onFilterModelChange. */
|
|
59
|
+
handleFilterModelChange: (model: GridFilterModel) => void;
|
|
42
60
|
|
|
43
61
|
// aggregation
|
|
44
62
|
aggregationModel: GridAggregationModel;
|
|
@@ -65,14 +83,37 @@ export interface UseGridControlledStateReturn {
|
|
|
65
83
|
selectedRowIds: Set<GridRowId>;
|
|
66
84
|
isSelectionControlled: boolean;
|
|
67
85
|
setInternalRowSelectionModel: React.Dispatch<React.SetStateAction<GridRowId[]>>;
|
|
86
|
+
/** Sets the internal model when uncontrolled and fires onRowSelectionModelChange. */
|
|
87
|
+
handleRowSelectionModelChange: (model: GridRowId[]) => void;
|
|
88
|
+
/**
|
|
89
|
+
* The model passed to `handleRowSelectionModelChange` since the last commit, or null when
|
|
90
|
+
* there is none: a change made in this tick that has not rendered yet (read from
|
|
91
|
+
* onRowSelectionModelChange or right after apiRef.selectRows). Stable identity.
|
|
92
|
+
*/
|
|
93
|
+
getPendingRowSelectionModel: () => GridRowId[] | null;
|
|
94
|
+
|
|
95
|
+
// density (prop, else initialState.density, else 'standard')
|
|
96
|
+
density: GridDensity;
|
|
68
97
|
}
|
|
69
98
|
|
|
70
99
|
const EMPTY_PIVOT_MODEL: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
|
|
100
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
101
|
+
|
|
102
|
+
/** The uncontrolled page size must be one the page-size select can show. */
|
|
103
|
+
export function getDefaultPageSize(pageSizeOptions?: number[]): number {
|
|
104
|
+
if (!pageSizeOptions || pageSizeOptions.length === 0 || pageSizeOptions.includes(DEFAULT_PAGE_SIZE)) {
|
|
105
|
+
return DEFAULT_PAGE_SIZE;
|
|
106
|
+
}
|
|
107
|
+
return pageSizeOptions[0];
|
|
108
|
+
}
|
|
71
109
|
|
|
72
110
|
export function useGridControlledState(params: UseGridControlledStateParams): UseGridControlledStateReturn {
|
|
73
111
|
const {
|
|
74
112
|
initialState,
|
|
75
113
|
sortModel: propSortModel,
|
|
114
|
+
onSortModelChange,
|
|
115
|
+
filterModel: propFilterModel,
|
|
116
|
+
onFilterModelChange,
|
|
76
117
|
aggregationModel: propAggregationModel,
|
|
77
118
|
onAggregationModelChange,
|
|
78
119
|
columnVisibilityModel: propColumnVisibilityModel,
|
|
@@ -83,8 +124,10 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
|
|
|
83
124
|
onPivotModelChange,
|
|
84
125
|
paginationModel: propPaginationModel,
|
|
85
126
|
onPaginationModelChange,
|
|
127
|
+
pageSizeOptions,
|
|
86
128
|
rowSelectionModel: propRowSelectionModel,
|
|
87
129
|
onRowSelectionModelChange,
|
|
130
|
+
density: propDensity,
|
|
88
131
|
} = params;
|
|
89
132
|
|
|
90
133
|
// ── Sort ─────────────────────────────────────────────────────────────────
|
|
@@ -94,12 +137,36 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
|
|
|
94
137
|
const isSortControlled = propSortModel !== undefined;
|
|
95
138
|
const sortModel = isSortControlled ? propSortModel! : internalSortModel;
|
|
96
139
|
|
|
140
|
+
const handleSortModelChange = useCallback((model: GridSortItem[]) => {
|
|
141
|
+
if (!isSortControlled) setInternalSortModel(model);
|
|
142
|
+
onSortModelChange?.(model);
|
|
143
|
+
}, [isSortControlled, onSortModelChange]);
|
|
144
|
+
|
|
145
|
+
// ── Filter ───────────────────────────────────────────────────────────────
|
|
146
|
+
const [internalFilterModel, setInternalFilterModel] = useState<GridFilterModel>(
|
|
147
|
+
() => initialState?.filter?.filterModel ?? { items: [] }
|
|
148
|
+
);
|
|
149
|
+
// `!= null` so a JS consumer's `filterModel={null}` behaves like "not passed", as before.
|
|
150
|
+
const isFilterControlled = propFilterModel != null;
|
|
151
|
+
const filterModel = isFilterControlled ? propFilterModel : internalFilterModel;
|
|
152
|
+
|
|
153
|
+
const handleFilterModelChange = useCallback((model: GridFilterModel) => {
|
|
154
|
+
if (!isFilterControlled) setInternalFilterModel(model);
|
|
155
|
+
onFilterModelChange?.(model);
|
|
156
|
+
}, [isFilterControlled, onFilterModelChange]);
|
|
157
|
+
|
|
97
158
|
// ── Aggregation ──────────────────────────────────────────────────────────
|
|
98
159
|
const isAggregationControlled = propAggregationModel !== undefined;
|
|
99
160
|
const [internalAggregationModel, setInternalAggregationModel] = useState<GridAggregationModel>(
|
|
100
161
|
() => propAggregationModel ?? {}
|
|
101
162
|
);
|
|
102
|
-
|
|
163
|
+
// Identity follows content: `aggregationModel={{ salary: 'sum' }}` is a new object on every parent
|
|
164
|
+
// render, and the fetches and memos keyed on the model must not rerun for an unchanged model.
|
|
165
|
+
const aggregationModelKey = JSON.stringify(isAggregationControlled ? propAggregationModel! : internalAggregationModel);
|
|
166
|
+
const aggregationModel = useMemo<GridAggregationModel>(
|
|
167
|
+
() => JSON.parse(aggregationModelKey) as GridAggregationModel,
|
|
168
|
+
[aggregationModelKey],
|
|
169
|
+
);
|
|
103
170
|
|
|
104
171
|
const handleAggregationModelChange = useCallback((model: GridAggregationModel) => {
|
|
105
172
|
if (!isAggregationControlled) setInternalAggregationModel(model);
|
|
@@ -146,7 +213,7 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
|
|
|
146
213
|
// ── Pagination ───────────────────────────────────────────────────────────
|
|
147
214
|
const isPaginationControlled = propPaginationModel !== undefined;
|
|
148
215
|
const [internalPaginationModel, setInternalPaginationModel] = useState<GridPaginationModel>(
|
|
149
|
-
() => initialState?.pagination?.paginationModel ?? propPaginationModel ?? { page: 0, pageSize:
|
|
216
|
+
() => initialState?.pagination?.paginationModel ?? propPaginationModel ?? { page: 0, pageSize: getDefaultPageSize(pageSizeOptions) }
|
|
150
217
|
);
|
|
151
218
|
const effectivePaginationModel = isPaginationControlled ? propPaginationModel : internalPaginationModel;
|
|
152
219
|
|
|
@@ -161,13 +228,35 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
|
|
|
161
228
|
const rowSelectionModel = isSelectionControlled ? propRowSelectionModel! : internalRowSelectionModel;
|
|
162
229
|
const selectedRowIds = useMemo(() => new Set(rowSelectionModel), [rowSelectionModel]);
|
|
163
230
|
|
|
164
|
-
//
|
|
165
|
-
|
|
231
|
+
// Cleared on every commit: the rendered model is current again (a controlled parent may
|
|
232
|
+
// also have rejected the change).
|
|
233
|
+
const pendingRowSelectionRef = useRef<GridRowId[] | null>(null);
|
|
234
|
+
useLayoutEffect(() => {
|
|
235
|
+
pendingRowSelectionRef.current = null;
|
|
236
|
+
});
|
|
237
|
+
const getPendingRowSelectionModel = useCallback(() => pendingRowSelectionRef.current, []);
|
|
238
|
+
|
|
239
|
+
const handleRowSelectionModelChange = useCallback((model: GridRowId[]) => {
|
|
240
|
+
pendingRowSelectionRef.current = model;
|
|
241
|
+
if (!isSelectionControlled) setInternalRowSelectionModel(model);
|
|
242
|
+
onRowSelectionModelChange?.(model);
|
|
243
|
+
}, [isSelectionControlled, onRowSelectionModelChange]);
|
|
244
|
+
|
|
245
|
+
// ── Density ──────────────────────────────────────────────────────────────
|
|
246
|
+
// No built-in density UI exists, so there is no internal setter: the prop wins,
|
|
247
|
+
// otherwise the persisted/initial density applies.
|
|
248
|
+
const [initialDensity] = useState<GridDensity>(() => initialState?.density?.density ?? 'standard');
|
|
249
|
+
const density = propDensity ?? initialDensity;
|
|
166
250
|
|
|
167
251
|
return {
|
|
168
252
|
sortModel: sortModel as GridSortItem[],
|
|
169
253
|
isSortControlled,
|
|
170
254
|
setInternalSortModel: setInternalSortModel as React.Dispatch<React.SetStateAction<GridSortItem[]>>,
|
|
255
|
+
handleSortModelChange,
|
|
256
|
+
|
|
257
|
+
filterModel,
|
|
258
|
+
isFilterControlled,
|
|
259
|
+
handleFilterModelChange,
|
|
171
260
|
|
|
172
261
|
aggregationModel,
|
|
173
262
|
handleAggregationModelChange,
|
|
@@ -188,5 +277,9 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
|
|
|
188
277
|
selectedRowIds,
|
|
189
278
|
isSelectionControlled,
|
|
190
279
|
setInternalRowSelectionModel,
|
|
280
|
+
handleRowSelectionModelChange,
|
|
281
|
+
getPendingRowSelectionModel,
|
|
282
|
+
|
|
283
|
+
density,
|
|
191
284
|
};
|
|
192
285
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { useDetailPanelHeights } from '../features/useDetailPanelHeights';
|
|
3
|
+
import { getDetailPanelRowIds } from '../../utils/detailPanel';
|
|
4
|
+
import type { GridRowId, GridRowMeta } from '../../types';
|
|
5
|
+
|
|
6
|
+
export interface UseGridDetailPanelParams {
|
|
7
|
+
/** Whether `getDetailPanelContent` was passed: the grid then has an expand column. */
|
|
8
|
+
hasDetailPanel: boolean;
|
|
9
|
+
/** `detailPanelExpandedRowIds` prop (controlled when defined). */
|
|
10
|
+
controlledExpandedRowIds: Set<GridRowId> | undefined;
|
|
11
|
+
onDetailPanelExpandedRowIdsChange: ((expandedRowIds: Set<GridRowId>) => void) | undefined;
|
|
12
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseGridDetailPanelResult {
|
|
16
|
+
hasDetailPanel: boolean;
|
|
17
|
+
/** What renders and is laid out: synthetic group rows never show a detail panel. */
|
|
18
|
+
expandedRowIds: Set<GridRowId>;
|
|
19
|
+
handleDetailPanelToggle: (rowId: GridRowId) => void;
|
|
20
|
+
/** Measured heights of `'auto'` panels. */
|
|
21
|
+
detailPanelHeights: Map<GridRowId, number>;
|
|
22
|
+
reportDetailPanelHeight: (rowId: GridRowId, height: number) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Detail-panel expansion (controlled or uncontrolled) and the measured heights of `'auto'` panels. */
|
|
26
|
+
export function useGridDetailPanel(params: UseGridDetailPanelParams): UseGridDetailPanelResult {
|
|
27
|
+
const { hasDetailPanel, controlledExpandedRowIds, onDetailPanelExpandedRowIdsChange, rowMetaMap } = params;
|
|
28
|
+
|
|
29
|
+
const [internalExpandedRowIds, setInternalExpandedRowIds] = useState<Set<GridRowId>>(new Set());
|
|
30
|
+
const requestedExpandedRowIds = controlledExpandedRowIds ?? internalExpandedRowIds;
|
|
31
|
+
const expandedRowIds = useMemo(() => getDetailPanelRowIds(requestedExpandedRowIds, rowMetaMap), [requestedExpandedRowIds, rowMetaMap]);
|
|
32
|
+
|
|
33
|
+
const handleDetailPanelToggle = useCallback((rowId: GridRowId) => {
|
|
34
|
+
// Synthetic group / subtotal rows have no detail panel (keyboard Space on the expand cell lands here too).
|
|
35
|
+
if (rowMetaMap.get(rowId)?.isGroupRow) return;
|
|
36
|
+
const newExpandedRowIds = new Set(requestedExpandedRowIds);
|
|
37
|
+
if (newExpandedRowIds.has(rowId)) {
|
|
38
|
+
newExpandedRowIds.delete(rowId);
|
|
39
|
+
} else {
|
|
40
|
+
newExpandedRowIds.add(rowId);
|
|
41
|
+
}
|
|
42
|
+
if (controlledExpandedRowIds === undefined) {
|
|
43
|
+
setInternalExpandedRowIds(newExpandedRowIds);
|
|
44
|
+
}
|
|
45
|
+
onDetailPanelExpandedRowIdsChange?.(newExpandedRowIds);
|
|
46
|
+
}, [requestedExpandedRowIds, rowMetaMap, controlledExpandedRowIds, onDetailPanelExpandedRowIdsChange]);
|
|
47
|
+
|
|
48
|
+
const { detailPanelHeights, reportDetailPanelHeight } = useDetailPanelHeights();
|
|
49
|
+
|
|
50
|
+
return { hasDetailPanel, expandedRowIds, handleDetailPanelToggle, detailPanelHeights, reportDetailPanelHeight };
|
|
51
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface UseGridDevWarningsParams {
|
|
5
|
+
paginationRequested: boolean;
|
|
6
|
+
isRowGrouping: boolean;
|
|
7
|
+
autoHeight: boolean;
|
|
8
|
+
viewportHeight: number;
|
|
9
|
+
renderedRowCount: number;
|
|
10
|
+
totalRowCount: number;
|
|
11
|
+
/** `pinnedRows` is set while tree data or row grouping is active, where it has no effect. */
|
|
12
|
+
pinnedRowsIgnored?: boolean;
|
|
13
|
+
/** Pixel height of the scrollable content. */
|
|
14
|
+
scrollHeight?: number;
|
|
15
|
+
/** `listView` is set but `listViewColumn` is not, so the grid view is shown. */
|
|
16
|
+
listViewWithoutColumn?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Below this many rows, rendering everything is harmless, so an unbounded container is not worth a warning.
|
|
20
|
+
const UNBOUNDED_WARN_THRESHOLD = 200;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Browsers cap element heights (Firefox at about 17.9M px, Chromium and Safari at about 33.5M px):
|
|
24
|
+
* rows laid out below the cap cannot be scrolled to.
|
|
25
|
+
*/
|
|
26
|
+
export const MAX_SCROLLABLE_HEIGHT = 17_800_000;
|
|
27
|
+
|
|
28
|
+
export function useGridDevWarnings(params: UseGridDevWarningsParams): void {
|
|
29
|
+
const { paginationRequested, isRowGrouping, autoHeight, viewportHeight, renderedRowCount, totalRowCount, pinnedRowsIgnored = false, scrollHeight = 0, listViewWithoutColumn = false } = params;
|
|
30
|
+
const warnedPaginationRef = useRef(false);
|
|
31
|
+
const warnedUnboundedRef = useRef(false);
|
|
32
|
+
const warnedPinnedRowsRef = useRef(false);
|
|
33
|
+
const warnedHeightRef = useRef(false);
|
|
34
|
+
const warnedListViewRef = useRef(false);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (process.env.NODE_ENV === 'production' || warnedListViewRef.current || !listViewWithoutColumn) return;
|
|
38
|
+
warnedListViewRef.current = true;
|
|
39
|
+
console.warn(
|
|
40
|
+
'[OpenGridX] `listView` is set without a `listViewColumn`, so the grid view is shown instead. ' +
|
|
41
|
+
'Pass `listViewColumn` to render list items. See docs/features/list-view.md.'
|
|
42
|
+
);
|
|
43
|
+
}, [listViewWithoutColumn]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (process.env.NODE_ENV === 'production' || warnedPinnedRowsRef.current || !pinnedRowsIgnored) return;
|
|
47
|
+
warnedPinnedRowsRef.current = true;
|
|
48
|
+
console.warn(
|
|
49
|
+
'[OpenGridX] `pinnedRows` is ignored while treeData or rowGroupingModel is active: the rows stay in their ' +
|
|
50
|
+
'place in the hierarchy. See docs/features/pinning.md.'
|
|
51
|
+
);
|
|
52
|
+
}, [pinnedRowsIgnored]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (process.env.NODE_ENV === 'production' || warnedHeightRef.current || scrollHeight <= MAX_SCROLLABLE_HEIGHT) return;
|
|
56
|
+
warnedHeightRef.current = true;
|
|
57
|
+
console.warn(
|
|
58
|
+
`[OpenGridX] The grid content is ${Math.round(scrollHeight)}px tall. Browsers cap element heights ` +
|
|
59
|
+
'(about 17.9M px in Firefox, 33.5M px in Chrome and Safari), so the last rows may not be reachable by scrolling. ' +
|
|
60
|
+
'Use pagination, server-side or infinite loading, or a smaller rowHeight. See docs/features/virtualization.md.'
|
|
61
|
+
);
|
|
62
|
+
}, [scrollHeight]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (process.env.NODE_ENV === 'production' || warnedPaginationRef.current) return;
|
|
66
|
+
if (paginationRequested && isRowGrouping) {
|
|
67
|
+
warnedPaginationRef.current = true;
|
|
68
|
+
console.warn(
|
|
69
|
+
'[OpenGridX] `pagination` is ignored while `rowGroupingModel` is active — all group rows render in one ' +
|
|
70
|
+
'scrollable view. Give the grid a bounded height so it virtualizes. See docs/features/tree-data-grouping.md.'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}, [paginationRequested, isRowGrouping]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (process.env.NODE_ENV === 'production' || warnedUnboundedRef.current || autoHeight) return;
|
|
77
|
+
if (viewportHeight > 0 && totalRowCount > UNBOUNDED_WARN_THRESHOLD && renderedRowCount >= totalRowCount) {
|
|
78
|
+
warnedUnboundedRef.current = true;
|
|
79
|
+
console.warn(
|
|
80
|
+
`[OpenGridX] The grid viewport is ${Math.round(viewportHeight)}px tall and is rendering all ${totalRowCount} rows, ` +
|
|
81
|
+
'so row virtualization is effectively off. The grid container has no bounded height: it grows to fit its ' +
|
|
82
|
+
'content. Give it a fixed height, or if it is a flex child add `min-height: 0`. See docs/features/virtualization.md.'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}, [autoHeight, viewportHeight, renderedRowCount, totalRowCount]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* True when the grid's stylesheet does not reach `root` (the `.ogx` element). The stylesheet lays the
|
|
90
|
+
* root out as a flex column; without it the root is a plain block and the viewport does not scroll.
|
|
91
|
+
* jsdom applies no library CSS, so it is never reported there.
|
|
92
|
+
*/
|
|
93
|
+
export function isGridStylesheetMissing(root: HTMLElement): boolean {
|
|
94
|
+
if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return false;
|
|
95
|
+
if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) return false;
|
|
96
|
+
return window.getComputedStyle(root).display !== 'flex';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let warnedStylesheet = false;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Dev-only: warns once per page when a grid renders without `@opencorestack/opengridx/styles`.
|
|
103
|
+
* The JS bundle does not load the stylesheet, and without it the viewport grows to fit every row,
|
|
104
|
+
* which silently turns virtualization off.
|
|
105
|
+
*/
|
|
106
|
+
export function useGridStylesheetWarning(rootRef: RefObject<HTMLElement | null>): void {
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (process.env.NODE_ENV === 'production' || warnedStylesheet) return;
|
|
109
|
+
const root = rootRef.current;
|
|
110
|
+
if (!root || !isGridStylesheetMissing(root)) return;
|
|
111
|
+
warnedStylesheet = true;
|
|
112
|
+
console.warn(
|
|
113
|
+
'[OpenGridX] The grid stylesheet is not loaded, so the grid is unstyled and renders every row ' +
|
|
114
|
+
"(its viewport does not scroll). Add `import '@opencorestack/opengridx/styles'` once in your app root. " +
|
|
115
|
+
'See README.md, Getting Started.'
|
|
116
|
+
);
|
|
117
|
+
}, [rootRef]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Test helper: lets the stylesheet warning fire again. */
|
|
121
|
+
export function resetGridStylesheetWarning(): void {
|
|
122
|
+
warnedStylesheet = false;
|
|
123
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
import type { FocusedCell } from '../../utils/focus';
|
|
4
|
+
import type { GridCellParams, GridRowId, GridRowModel } from '../../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keyboard-mode flag: toggled via a DOM class name on the grid root, with no React state, so
|
|
8
|
+
* the focus ring appears instantly without a re-render cycle.
|
|
9
|
+
*/
|
|
10
|
+
export function useGridKeyboardMode(containerRef: RefObject<HTMLDivElement | null>): (on: boolean) => void {
|
|
11
|
+
return useCallback((on: boolean) => {
|
|
12
|
+
containerRef.current?.classList.toggle('ogx--kb', on);
|
|
13
|
+
}, [containerRef]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseGridPointerFocusHandlersParams<R extends GridRowModel> {
|
|
17
|
+
ensureGridFocus: () => void;
|
|
18
|
+
setKeyboardMode: (on: boolean) => void;
|
|
19
|
+
setFocusedCell: React.Dispatch<React.SetStateAction<FocusedCell | null>>;
|
|
20
|
+
getRowId: (row: GridRowModel) => GridRowId;
|
|
21
|
+
onCellClick: ((params: GridCellParams<R>) => void) | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseGridPointerFocusHandlersResult<R extends GridRowModel> {
|
|
25
|
+
handleCellClick: (params: GridCellParams<R>) => void;
|
|
26
|
+
handleHeaderClick: (field: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Cell and header clicks move grid focus there and leave keyboard mode. */
|
|
30
|
+
export function useGridPointerFocusHandlers<R extends GridRowModel>(
|
|
31
|
+
params: UseGridPointerFocusHandlersParams<R>
|
|
32
|
+
): UseGridPointerFocusHandlersResult<R> {
|
|
33
|
+
const { ensureGridFocus, setKeyboardMode, setFocusedCell, getRowId, onCellClick } = params;
|
|
34
|
+
|
|
35
|
+
const handleCellClick = useCallback((cellParams: GridCellParams<R>) => {
|
|
36
|
+
// Focus stays where the click put it (the cell, or a control rendered inside it).
|
|
37
|
+
ensureGridFocus();
|
|
38
|
+
setKeyboardMode(false);
|
|
39
|
+
setFocusedCell({ id: getRowId(cellParams.row), field: cellParams.field, rowIndex: cellParams.rowIndex });
|
|
40
|
+
onCellClick?.(cellParams);
|
|
41
|
+
}, [onCellClick, setKeyboardMode, setFocusedCell, ensureGridFocus, getRowId]);
|
|
42
|
+
|
|
43
|
+
const handleHeaderClick = useCallback((field: string) => {
|
|
44
|
+
ensureGridFocus();
|
|
45
|
+
setFocusedCell({ id: null, field });
|
|
46
|
+
setKeyboardMode(false);
|
|
47
|
+
}, [ensureGridFocus, setFocusedCell, setKeyboardMode]);
|
|
48
|
+
|
|
49
|
+
return { handleCellClick, handleHeaderClick };
|
|
50
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { GridColDef, GridColumnPinning, GridRowModel } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface UseGridGroupingColumnParams<R extends GridRowModel> {
|
|
5
|
+
/** The columns before the grouping column is added (the pivot's columns in pivot mode). */
|
|
6
|
+
baseColumns: GridColDef<R>[];
|
|
7
|
+
pinnedColumns: GridColumnPinning;
|
|
8
|
+
/** Show the synthetic `__group__` column (see `resolveGridModes`). */
|
|
9
|
+
hasGroupingColumn: boolean;
|
|
10
|
+
groupingColDef: Partial<GridColDef<R>> | undefined;
|
|
11
|
+
isTreeData: boolean;
|
|
12
|
+
getTreeDataPath: ((row: R) => string[]) | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseGridGroupingColumnResult<R extends GridRowModel> {
|
|
16
|
+
/** `baseColumns`, with the `__group__` column first when it is shown. */
|
|
17
|
+
activeColumns: GridColDef<R>[];
|
|
18
|
+
/** `pinnedColumns`, with `__group__` pinned first on the left when it is shown. */
|
|
19
|
+
effectivePinnedColumns: GridColumnPinning;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The dedicated `__group__` column that `groupingColDef` asks for while row grouping or tree
|
|
24
|
+
* data is active: prepended at position 0 and auto-pinned left.
|
|
25
|
+
*
|
|
26
|
+
* Both results MUST be memoized: a new array/object reference every render cascades through
|
|
27
|
+
* useRowGrouping's memos and effects into an infinite setState loop (Maximum update depth exceeded).
|
|
28
|
+
*/
|
|
29
|
+
export function useGridGroupingColumn<R extends GridRowModel>(
|
|
30
|
+
params: UseGridGroupingColumnParams<R>
|
|
31
|
+
): UseGridGroupingColumnResult<R> {
|
|
32
|
+
const { baseColumns, pinnedColumns, hasGroupingColumn, groupingColDef, isTreeData, getTreeDataPath } = params;
|
|
33
|
+
|
|
34
|
+
const effectivePinnedColumns = useMemo(() => (
|
|
35
|
+
hasGroupingColumn
|
|
36
|
+
? { ...pinnedColumns, left: ['__group__', ...((pinnedColumns?.left ?? []).filter(f => f !== '__group__'))] }
|
|
37
|
+
: pinnedColumns
|
|
38
|
+
), [hasGroupingColumn, pinnedColumns]);
|
|
39
|
+
|
|
40
|
+
const activeColumns = useMemo(() => (
|
|
41
|
+
hasGroupingColumn
|
|
42
|
+
? [
|
|
43
|
+
{
|
|
44
|
+
headerName: 'Group',
|
|
45
|
+
width: 220,
|
|
46
|
+
// Tree data: a row's own entry in the hierarchy is the last segment of its path.
|
|
47
|
+
...(isTreeData && getTreeDataPath ? { valueGetter: ({ row }: { row: R }) => { const path = getTreeDataPath(row); return path[path.length - 1]; } } : {}),
|
|
48
|
+
...groupingColDef,
|
|
49
|
+
field: '__group__',
|
|
50
|
+
hideable: false,
|
|
51
|
+
sortable: false,
|
|
52
|
+
filterable: false,
|
|
53
|
+
pinnable: false,
|
|
54
|
+
exportable: false,
|
|
55
|
+
} as unknown as GridColDef<R>,
|
|
56
|
+
...baseColumns,
|
|
57
|
+
]
|
|
58
|
+
: baseColumns
|
|
59
|
+
), [groupingColDef, hasGroupingColumn, isTreeData, getTreeDataPath, baseColumns]);
|
|
60
|
+
|
|
61
|
+
return { activeColumns, effectivePinnedColumns };
|
|
62
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import { upsertSortItem } from '../../utils/sorting';
|
|
3
|
+
import { pinColumnTo } from '../../utils/pinning';
|
|
4
|
+
import type {
|
|
5
|
+
GridColumnPinning,
|
|
6
|
+
GridColumnVisibilityModel,
|
|
7
|
+
GridPinnedPosition,
|
|
8
|
+
GridSortDirection,
|
|
9
|
+
GridSortItem,
|
|
10
|
+
} from '../../types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The sort model after sorting `field` by `direction` (null removes it). `multiSort` adds or updates
|
|
14
|
+
* the column in the model (shift-click); otherwise the model is replaced by this one column. The
|
|
15
|
+
* header, the keyboard and `apiRef.sortColumn` all build the next model with this.
|
|
16
|
+
*/
|
|
17
|
+
export function nextSortModelForColumn(
|
|
18
|
+
current: readonly GridSortItem[],
|
|
19
|
+
field: string,
|
|
20
|
+
direction: GridSortDirection,
|
|
21
|
+
multiSort: boolean
|
|
22
|
+
): GridSortItem[] {
|
|
23
|
+
if (multiSort) return upsertSortItem(current, field, direction);
|
|
24
|
+
return direction ? [{ field, sort: direction }] : current.filter(item => item.field !== field);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseGridSortHandlersParams {
|
|
28
|
+
sortModel: GridSortItem[];
|
|
29
|
+
/** Stores the model when uncontrolled and calls onSortModelChange (useGridControlledState). */
|
|
30
|
+
onSortModelChange: (model: GridSortItem[]) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UseGridSortHandlersResult {
|
|
34
|
+
/** Header click / Enter: replaces the sort model with this column (or clears it). */
|
|
35
|
+
handleSort: (field: string, direction: GridSortDirection) => void;
|
|
36
|
+
/** Shift-click / Shift+Enter: adds, updates or removes this column in the sort model. */
|
|
37
|
+
handleSortAdd: (field: string, direction: GridSortDirection) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Sort changes from the header and the keyboard; the same transition and handler as apiRef.sortColumn. */
|
|
41
|
+
export function useGridSortHandlers(params: UseGridSortHandlersParams): UseGridSortHandlersResult {
|
|
42
|
+
const { sortModel, onSortModelChange } = params;
|
|
43
|
+
|
|
44
|
+
const handleSort = useCallback((field: string, direction: GridSortDirection) => {
|
|
45
|
+
onSortModelChange(nextSortModelForColumn(sortModel, field, direction, false));
|
|
46
|
+
}, [sortModel, onSortModelChange]);
|
|
47
|
+
|
|
48
|
+
const handleSortAdd = useCallback((field: string, direction: GridSortDirection) => {
|
|
49
|
+
onSortModelChange(nextSortModelForColumn(sortModel, field, direction, true));
|
|
50
|
+
}, [sortModel, onSortModelChange]);
|
|
51
|
+
|
|
52
|
+
return { handleSort, handleSortAdd };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseGridColumnMenuHandlersParams {
|
|
56
|
+
columnVisibilityModel: GridColumnVisibilityModel;
|
|
57
|
+
onColumnVisibilityModelChange: (model: GridColumnVisibilityModel) => void;
|
|
58
|
+
pinnedColumns: GridColumnPinning;
|
|
59
|
+
onPinnedColumnsChange: (model: GridColumnPinning) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UseGridColumnMenuHandlersResult {
|
|
63
|
+
handleHideColumn: (field: string) => void;
|
|
64
|
+
handlePinColumn: (field: string, side: GridPinnedPosition | null) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The column menu's Hide and Pin actions. Each builds on the latest model, including a change made
|
|
69
|
+
* earlier in the same tick, so two hides (or pins) before a re-render both stick.
|
|
70
|
+
*/
|
|
71
|
+
export function useGridColumnMenuHandlers(params: UseGridColumnMenuHandlersParams): UseGridColumnMenuHandlersResult {
|
|
72
|
+
const { columnVisibilityModel, onColumnVisibilityModelChange, pinnedColumns, onPinnedColumnsChange } = params;
|
|
73
|
+
|
|
74
|
+
const latestRef = useRef({ columnVisibilityModel, pinnedColumns });
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
latestRef.current = { columnVisibilityModel, pinnedColumns };
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const handleHideColumn = useCallback((field: string) => {
|
|
80
|
+
const next = { ...latestRef.current.columnVisibilityModel, [field]: false };
|
|
81
|
+
latestRef.current = { ...latestRef.current, columnVisibilityModel: next };
|
|
82
|
+
onColumnVisibilityModelChange(next);
|
|
83
|
+
}, [onColumnVisibilityModelChange]);
|
|
84
|
+
|
|
85
|
+
const handlePinColumn = useCallback((field: string, side: GridPinnedPosition | null) => {
|
|
86
|
+
const next = pinColumnTo(latestRef.current.pinnedColumns, field, side);
|
|
87
|
+
latestRef.current = { ...latestRef.current, pinnedColumns: next };
|
|
88
|
+
onPinnedColumnsChange(next);
|
|
89
|
+
}, [onPinnedColumnsChange]);
|
|
90
|
+
|
|
91
|
+
return { handleHideColumn, handlePinColumn };
|
|
92
|
+
}
|