@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
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from
|
|
|
2
2
|
import ReactDOM from 'react-dom';
|
|
3
3
|
import type { GridColDef } from '../../types';
|
|
4
4
|
import type { GridPivotModel, GridPivotAggFn, GridPivotValueField } from '../../types';
|
|
5
|
+
import { getViewportWidth } from '../../utils/viewport';
|
|
5
6
|
|
|
6
7
|
const AGG_FNS: GridPivotAggFn[] = ['sum', 'avg', 'count', 'min', 'max'];
|
|
7
8
|
|
|
@@ -31,7 +32,7 @@ export interface PivotPanelProps {
|
|
|
31
32
|
export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: PivotPanelProps) {
|
|
32
33
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
33
34
|
const [pos, setPos] = useState({ top: 0, right: 0 });
|
|
34
|
-
const [portalContainer, setPortalContainer] = useState<Element>(
|
|
35
|
+
const [portalContainer, setPortalContainer] = useState<Element | null>(null);
|
|
35
36
|
|
|
36
37
|
useLayoutEffect(() => {
|
|
37
38
|
setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
|
|
@@ -40,7 +41,7 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
40
41
|
const computePos = useCallback(() => {
|
|
41
42
|
if (!anchorRef.current) return { top: 0, right: 0 };
|
|
42
43
|
const rect = anchorRef.current.getBoundingClientRect();
|
|
43
|
-
return { top: rect.bottom + 6, right:
|
|
44
|
+
return { top: rect.bottom + 6, right: getViewportWidth() - rect.right };
|
|
44
45
|
}, [anchorRef]);
|
|
45
46
|
|
|
46
47
|
useEffect(() => {
|
|
@@ -75,6 +76,20 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
75
76
|
const inValue = new Set(model.valueFields.map((v) => v.field));
|
|
76
77
|
const isUsed = (f: string) => inRow.has(f) || inCol.has(f) || inValue.has(f);
|
|
77
78
|
|
|
79
|
+
const colDefOf = (field: string) => columns.find((c) => c.field === field);
|
|
80
|
+
const colLabel = (field: string) => colDefOf(field)?.headerName ?? field;
|
|
81
|
+
|
|
82
|
+
// The same column rules the grid applies elsewhere: availableAggregationFunctions limits the
|
|
83
|
+
// functions, aggregable: false keeps a column out of the values (as in the Summaries panel), and
|
|
84
|
+
// groupable: false keeps it out of the row and column dimensions (as in row grouping).
|
|
85
|
+
const allowedFns = (field: string): GridPivotAggFn[] => {
|
|
86
|
+
const allowed = colDefOf(field)?.availableAggregationFunctions;
|
|
87
|
+
return allowed ? AGG_FNS.filter((fn) => allowed.includes(fn)) : AGG_FNS;
|
|
88
|
+
};
|
|
89
|
+
const canBeValue = (col: GridColDef) =>
|
|
90
|
+
col.aggregable !== false && (col.type === 'number' || col.aggregable === true) && allowedFns(col.field).length > 0;
|
|
91
|
+
const canBeDimension = (col: GridColDef) => col.groupable !== false;
|
|
92
|
+
|
|
78
93
|
function addRow(field: string) {
|
|
79
94
|
if (inRow.has(field)) return;
|
|
80
95
|
onChange({ ...model, rowFields: [...model.rowFields, field] });
|
|
@@ -83,35 +98,33 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
83
98
|
if (inCol.has(field)) return;
|
|
84
99
|
onChange({ ...model, columnFields: [...model.columnFields, field] });
|
|
85
100
|
}
|
|
86
|
-
function addValue(field: string
|
|
101
|
+
function addValue(field: string) {
|
|
87
102
|
if (inValue.has(field)) return;
|
|
88
|
-
onChange({ ...model, valueFields: [...model.valueFields, { field, aggFn }] });
|
|
103
|
+
onChange({ ...model, valueFields: [...model.valueFields, { field, aggFn: allowedFns(field)[0] ?? 'sum' }] });
|
|
89
104
|
}
|
|
90
|
-
function removeRow(
|
|
91
|
-
onChange({ ...model, rowFields: model.rowFields.filter((
|
|
105
|
+
function removeRow(index: number) {
|
|
106
|
+
onChange({ ...model, rowFields: model.rowFields.filter((_, i) => i !== index) });
|
|
92
107
|
}
|
|
93
|
-
function removeCol(
|
|
94
|
-
onChange({ ...model, columnFields: model.columnFields.filter((
|
|
108
|
+
function removeCol(index: number) {
|
|
109
|
+
onChange({ ...model, columnFields: model.columnFields.filter((_, i) => i !== index) });
|
|
95
110
|
}
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
// Value chips are addressed by position: one field may appear with several functions.
|
|
112
|
+
function removeValue(index: number) {
|
|
113
|
+
onChange({ ...model, valueFields: model.valueFields.filter((_, i) => i !== index) });
|
|
98
114
|
}
|
|
99
|
-
function changeValueFn(
|
|
115
|
+
function changeValueFn(index: number, aggFn: GridPivotAggFn) {
|
|
100
116
|
onChange({
|
|
101
117
|
...model,
|
|
102
|
-
valueFields: model.valueFields.map((v) =>
|
|
118
|
+
valueFields: model.valueFields.map((v, i) => i === index ? { ...v, aggFn } : v),
|
|
103
119
|
});
|
|
104
120
|
}
|
|
105
121
|
function reset() {
|
|
106
122
|
onChange({ rowFields: [], columnFields: [], valueFields: [] });
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
const numericFields = new Set(columns.filter((c) => c.type === 'number' || c.aggregable).map((c) => c.field));
|
|
110
|
-
|
|
111
125
|
const totalActive = model.rowFields.length + model.columnFields.length + model.valueFields.length;
|
|
112
126
|
|
|
113
|
-
const
|
|
114
|
-
columns.find((c) => c.field === field)?.headerName ?? field;
|
|
127
|
+
const availableColumns = columns.filter((col) => !isUsed(col.field) && (canBeDimension(col) || canBeValue(col)));
|
|
115
128
|
|
|
116
129
|
const panel = (
|
|
117
130
|
<div
|
|
@@ -128,7 +141,7 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
128
141
|
Pivot Mode
|
|
129
142
|
</span>
|
|
130
143
|
{totalActive > 0 && (
|
|
131
|
-
<button className="ogx-pivot-panel__reset-btn" onClick={reset} title="Reset pivot">
|
|
144
|
+
<button type="button" className="ogx-pivot-panel__reset-btn" onClick={reset} title="Reset pivot">
|
|
132
145
|
Reset
|
|
133
146
|
</button>
|
|
134
147
|
)}
|
|
@@ -146,10 +159,16 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
146
159
|
className={`ogx-pivot-chips${model.rowFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
|
|
147
160
|
data-placeholder="Add fields here — each becomes a row label"
|
|
148
161
|
>
|
|
149
|
-
{model.rowFields.map((f) => (
|
|
150
|
-
<span key={f} className="ogx-pivot-chip ogx-pivot-chip--row">
|
|
162
|
+
{model.rowFields.map((f, i) => (
|
|
163
|
+
<span key={`${i}:${f}`} className="ogx-pivot-chip ogx-pivot-chip--row">
|
|
151
164
|
{colLabel(f)}
|
|
152
|
-
<button
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
className="ogx-pivot-chip__remove"
|
|
168
|
+
onClick={() => removeRow(i)}
|
|
169
|
+
title={`Remove ${colLabel(f)}`}
|
|
170
|
+
aria-label={`Remove ${colLabel(f)} from Row Fields`}
|
|
171
|
+
>×</button>
|
|
153
172
|
</span>
|
|
154
173
|
))}
|
|
155
174
|
</div>
|
|
@@ -165,10 +184,16 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
165
184
|
className={`ogx-pivot-chips${model.columnFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
|
|
166
185
|
data-placeholder="Add fields here — unique values become column headers"
|
|
167
186
|
>
|
|
168
|
-
{model.columnFields.map((f) => (
|
|
169
|
-
<span key={f} className="ogx-pivot-chip ogx-pivot-chip--col">
|
|
187
|
+
{model.columnFields.map((f, i) => (
|
|
188
|
+
<span key={`${i}:${f}`} className="ogx-pivot-chip ogx-pivot-chip--col">
|
|
170
189
|
{colLabel(f)}
|
|
171
|
-
<button
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
className="ogx-pivot-chip__remove"
|
|
193
|
+
onClick={() => removeCol(i)}
|
|
194
|
+
title={`Remove ${colLabel(f)}`}
|
|
195
|
+
aria-label={`Remove ${colLabel(f)} from Column Fields`}
|
|
196
|
+
>×</button>
|
|
172
197
|
</span>
|
|
173
198
|
))}
|
|
174
199
|
</div>
|
|
@@ -184,22 +209,33 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
184
209
|
className={`ogx-pivot-chips${model.valueFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
|
|
185
210
|
data-placeholder="Add numeric fields here — values are aggregated"
|
|
186
211
|
>
|
|
187
|
-
{model.valueFields.map((vf: GridPivotValueField) =>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
212
|
+
{model.valueFields.map((vf: GridPivotValueField, i) => {
|
|
213
|
+
const allowed = allowedFns(vf.field);
|
|
214
|
+
const fnOptions = allowed.includes(vf.aggFn) ? allowed : [vf.aggFn, ...allowed];
|
|
215
|
+
return (
|
|
216
|
+
<span key={`${i}:${vf.field}:${vf.aggFn}`} className="ogx-pivot-chip ogx-pivot-chip--value">
|
|
217
|
+
{colLabel(vf.field)}
|
|
218
|
+
<select
|
|
219
|
+
className="ogx-pivot-chip__fn-select"
|
|
220
|
+
value={vf.aggFn}
|
|
221
|
+
onChange={(e) => changeValueFn(i, e.target.value as GridPivotAggFn)}
|
|
222
|
+
title="Aggregation function"
|
|
223
|
+
aria-label={`Aggregation function for ${colLabel(vf.field)}`}
|
|
224
|
+
>
|
|
225
|
+
{fnOptions.map((fn) => (
|
|
226
|
+
<option key={fn} value={fn}>{fn.toUpperCase()}</option>
|
|
227
|
+
))}
|
|
228
|
+
</select>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
className="ogx-pivot-chip__remove"
|
|
232
|
+
onClick={() => removeValue(i)}
|
|
233
|
+
title={`Remove ${colLabel(vf.field)}`}
|
|
234
|
+
aria-label={`Remove ${colLabel(vf.field)} (${vf.aggFn.toUpperCase()}) from Value Fields`}
|
|
235
|
+
>×</button>
|
|
236
|
+
</span>
|
|
237
|
+
);
|
|
238
|
+
})}
|
|
203
239
|
</div>
|
|
204
240
|
</div>
|
|
205
241
|
|
|
@@ -209,26 +245,30 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
209
245
|
<div>
|
|
210
246
|
<div className="ogx-pivot-panel__fields-label">Available Fields</div>
|
|
211
247
|
<div className="ogx-pivot-fields">
|
|
212
|
-
{
|
|
213
|
-
const isNum =
|
|
214
|
-
const
|
|
215
|
-
|
|
248
|
+
{availableColumns.map((col) => {
|
|
249
|
+
const isNum = canBeValue(col);
|
|
250
|
+
const isDimension = canBeDimension(col);
|
|
251
|
+
const label = col.headerName ?? col.field;
|
|
216
252
|
return (
|
|
217
253
|
<div key={col.field} style={{ display: 'flex', alignItems: 'center', gap: 3 }}>
|
|
218
254
|
<span className={`ogx-pivot-field-btn${isNum ? ' ogx-pivot-field-btn--numeric' : ''}`}>
|
|
219
|
-
{
|
|
255
|
+
{label}
|
|
220
256
|
</span>
|
|
221
257
|
<div className="ogx-pivot-field-actions">
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
{isDimension && (
|
|
259
|
+
<button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--row" onClick={() => addRow(col.field)} title="Add to Row Fields" aria-label={`Add ${label} to Row Fields`}>Row</button>
|
|
260
|
+
)}
|
|
261
|
+
{isDimension && (
|
|
262
|
+
<button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--col" onClick={() => addCol(col.field)} title="Add to Column Fields" aria-label={`Add ${label} to Column Fields`}>Col</button>
|
|
263
|
+
)}
|
|
224
264
|
{isNum && (
|
|
225
|
-
<button className="ogx-pivot-add-btn ogx-pivot-add-btn--value" onClick={() => addValue(col.field)} title="Add to Value Fields">Val</button>
|
|
265
|
+
<button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--value" onClick={() => addValue(col.field)} title="Add to Value Fields" aria-label={`Add ${label} to Value Fields`}>Val</button>
|
|
226
266
|
)}
|
|
227
267
|
</div>
|
|
228
268
|
</div>
|
|
229
269
|
);
|
|
230
270
|
})}
|
|
231
|
-
{
|
|
271
|
+
{availableColumns.length === 0 && (
|
|
232
272
|
<span style={{ fontSize: 12, color: 'var(--ogx-color-gray-400, #94a3b8)' }}>
|
|
233
273
|
All fields are in use.
|
|
234
274
|
</span>
|
|
@@ -240,5 +280,5 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
|
|
|
240
280
|
</div>
|
|
241
281
|
);
|
|
242
282
|
|
|
243
|
-
return ReactDOM.createPortal(panel, portalContainer);
|
|
283
|
+
return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
|
|
244
284
|
}
|
|
@@ -22,6 +22,26 @@
|
|
|
22
22
|
animation: ogx-tooltip-in-bottom 0.15s ease-out;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
.ogx-tooltip--left {
|
|
26
|
+
transform: translate(-100%, -50%);
|
|
27
|
+
animation: ogx-tooltip-in-left 0.15s ease-out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ogx-tooltip--right {
|
|
31
|
+
transform: translate(0, -50%);
|
|
32
|
+
animation: ogx-tooltip-in-right 0.15s ease-out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@keyframes ogx-tooltip-in-left {
|
|
36
|
+
from { opacity: 0; transform: translate(-95%, -50%) scale(0.98); }
|
|
37
|
+
to { opacity: 1; transform: translate(-100%, -50%) scale(1); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@keyframes ogx-tooltip-in-right {
|
|
41
|
+
from { opacity: 0; transform: translate(4px, -50%) scale(0.98); }
|
|
42
|
+
to { opacity: 1; transform: translate(0, -50%) scale(1); }
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
@keyframes ogx-tooltip-in-top {
|
|
26
46
|
from { opacity: 0; transform: translate(-50%, -95%) scale(0.98); }
|
|
27
47
|
to { opacity: 1; transform: translate(-50%, -100%) scale(1); }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useState, useRef, useLayoutEffect, useCallback } from 'react';
|
|
1
|
+
import React, { useState, useRef, useLayoutEffect, useEffect, useCallback, useId, cloneElement, isValidElement } from 'react';
|
|
2
2
|
import ReactDOM from 'react-dom';
|
|
3
3
|
import './Tooltip.css';
|
|
4
4
|
|
|
5
|
-
interface GridTooltipProps {
|
|
5
|
+
export interface GridTooltipProps {
|
|
6
6
|
title: React.ReactNode;
|
|
7
7
|
children: React.ReactElement;
|
|
8
8
|
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
@@ -10,6 +10,13 @@ interface GridTooltipProps {
|
|
|
10
10
|
leaveDelay?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** Distance in px between the child and the tooltip. */
|
|
14
|
+
const GAP = 8;
|
|
15
|
+
|
|
16
|
+
interface ChildAriaProps {
|
|
17
|
+
'aria-describedby'?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export function GridTooltip({
|
|
14
21
|
title,
|
|
15
22
|
children,
|
|
@@ -19,52 +26,62 @@ export function GridTooltip({
|
|
|
19
26
|
}: GridTooltipProps) {
|
|
20
27
|
const [open, setOpen] = useState(false);
|
|
21
28
|
const [coords, setCoords] = useState({ top: 0, left: 0 });
|
|
22
|
-
|
|
29
|
+
// Resolved when the tooltip opens: the closest DataGridThemeProvider, so the tooltip picks up
|
|
30
|
+
// the theme's --ogx-overlay-* variables like the grid's other popovers, or document.body.
|
|
31
|
+
const [portalTarget, setPortalTarget] = useState<Element | null>(null);
|
|
32
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
23
33
|
const childRef = useRef<HTMLElement | null>(null);
|
|
34
|
+
const tooltipId = useId();
|
|
24
35
|
|
|
36
|
+
// Viewport coordinates of the anchor point; the tooltip is position: fixed and its CSS
|
|
37
|
+
// transform (per placement) moves it off that point.
|
|
25
38
|
const updateCoords = useCallback(() => {
|
|
26
39
|
if (!childRef.current) return;
|
|
27
40
|
const rect = childRef.current.getBoundingClientRect();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (placement === 'top') {
|
|
37
|
-
t = rect.top + scrollY - 8;
|
|
38
|
-
l = rect.left + scrollX + rect.width / 2;
|
|
39
|
-
} else if (placement === 'bottom') {
|
|
40
|
-
t = rect.bottom + scrollY + 8;
|
|
41
|
-
l = rect.left + scrollX + rect.width / 2;
|
|
41
|
+
const centerX = rect.left + rect.width / 2;
|
|
42
|
+
const centerY = rect.top + rect.height / 2;
|
|
43
|
+
switch (placement) {
|
|
44
|
+
case 'bottom': setCoords({ top: rect.bottom + GAP, left: centerX }); break;
|
|
45
|
+
case 'left': setCoords({ top: centerY, left: rect.left - GAP }); break;
|
|
46
|
+
case 'right': setCoords({ top: centerY, left: rect.right + GAP }); break;
|
|
47
|
+
default: setCoords({ top: rect.top - GAP, left: centerX });
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
setCoords({ top: t, left: l });
|
|
45
49
|
}, [placement]);
|
|
46
50
|
|
|
51
|
+
const clearTimer = useCallback(() => {
|
|
52
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
53
|
+
timerRef.current = null;
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
47
56
|
useLayoutEffect(() => {
|
|
48
|
-
if (open)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
57
|
+
if (!open) return;
|
|
58
|
+
updateCoords();
|
|
59
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
60
|
+
if (e.key === 'Escape') { clearTimer(); setOpen(false); }
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener('scroll', updateCoords, true);
|
|
63
|
+
window.addEventListener('resize', updateCoords);
|
|
64
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
53
65
|
return () => {
|
|
54
66
|
window.removeEventListener('scroll', updateCoords, true);
|
|
55
67
|
window.removeEventListener('resize', updateCoords);
|
|
68
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
56
69
|
};
|
|
57
|
-
}, [open, updateCoords]);
|
|
70
|
+
}, [open, updateCoords, clearTimer]);
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
// A pending enter/leave timer must not fire after unmount.
|
|
73
|
+
useEffect(() => clearTimer, [clearTimer]);
|
|
74
|
+
|
|
75
|
+
const show = () => {
|
|
76
|
+
clearTimer();
|
|
61
77
|
timerRef.current = setTimeout(() => {
|
|
78
|
+
setPortalTarget(childRef.current?.closest('.ogx-theme-provider') ?? document.body);
|
|
62
79
|
setOpen(true);
|
|
63
80
|
}, enterDelay);
|
|
64
81
|
};
|
|
65
82
|
|
|
66
|
-
const
|
|
67
|
-
|
|
83
|
+
const hide = () => {
|
|
84
|
+
clearTimer();
|
|
68
85
|
timerRef.current = setTimeout(() => {
|
|
69
86
|
setOpen(false);
|
|
70
87
|
}, leaveDelay);
|
|
@@ -77,29 +94,45 @@ export function GridTooltip({
|
|
|
77
94
|
childRef.current = node ? (node.firstElementChild as HTMLElement) ?? node : null;
|
|
78
95
|
}, []);
|
|
79
96
|
|
|
97
|
+
const isShown = open && Boolean(title) && portalTarget !== null;
|
|
98
|
+
|
|
99
|
+
// While shown, the child is described by the tooltip (added to any aria-describedby it has).
|
|
100
|
+
let child: React.ReactNode = children;
|
|
101
|
+
if (isShown && isValidElement(children)) {
|
|
102
|
+
const element = children as React.ReactElement<ChildAriaProps>;
|
|
103
|
+
const describedBy = element.props['aria-describedby'];
|
|
104
|
+
child = cloneElement(element, {
|
|
105
|
+
'aria-describedby': describedBy ? `${describedBy} ${tooltipId}` : tooltipId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
80
109
|
return (
|
|
81
110
|
<>
|
|
82
111
|
<span
|
|
83
112
|
ref={wrapperRef}
|
|
84
113
|
style={{ display: 'inline-block', lineHeight: 0 }}
|
|
85
|
-
onMouseEnter={
|
|
86
|
-
onMouseLeave={
|
|
114
|
+
onMouseEnter={show}
|
|
115
|
+
onMouseLeave={hide}
|
|
116
|
+
onFocus={show}
|
|
117
|
+
onBlur={hide}
|
|
87
118
|
>
|
|
88
|
-
{
|
|
119
|
+
{child}
|
|
89
120
|
</span>
|
|
90
|
-
{
|
|
121
|
+
{isShown && portalTarget && ReactDOM.createPortal(
|
|
91
122
|
<div
|
|
123
|
+
id={tooltipId}
|
|
124
|
+
role="tooltip"
|
|
92
125
|
className={`ogx-tooltip ogx-tooltip--${placement}`}
|
|
93
126
|
style={{
|
|
94
127
|
top: coords.top,
|
|
95
128
|
left: coords.left,
|
|
96
|
-
position: '
|
|
129
|
+
position: 'fixed',
|
|
97
130
|
zIndex: 99999
|
|
98
131
|
}}
|
|
99
132
|
>
|
|
100
133
|
{title}
|
|
101
134
|
</div>,
|
|
102
|
-
|
|
135
|
+
portalTarget
|
|
103
136
|
)}
|
|
104
137
|
</>
|
|
105
138
|
);
|
|
@@ -15,6 +15,8 @@ export const Button: React.FC<ButtonProps> = ({
|
|
|
15
15
|
color = 'default',
|
|
16
16
|
disabled = false,
|
|
17
17
|
className = '',
|
|
18
|
+
// A <button> defaults to type="submit"; the grid often sits inside a form.
|
|
19
|
+
type = 'button',
|
|
18
20
|
children,
|
|
19
21
|
...props
|
|
20
22
|
}) => {
|
|
@@ -29,6 +31,7 @@ export const Button: React.FC<ButtonProps> = ({
|
|
|
29
31
|
|
|
30
32
|
return (
|
|
31
33
|
<button
|
|
34
|
+
type={type}
|
|
32
35
|
className={classNames}
|
|
33
36
|
disabled={disabled}
|
|
34
37
|
{...props}
|
|
@@ -41,11 +41,12 @@ export const Checkbox: React.FC<CheckboxProps> = ({
|
|
|
41
41
|
|
|
42
42
|
return (
|
|
43
43
|
<label className={wrapperClassNames}>
|
|
44
|
+
{/* No fallback aria-label: it would override the name from `label`, a <label htmlFor>
|
|
45
|
+
or aria-labelledby. Pass `label` or `aria-label` when nothing else names it. */}
|
|
44
46
|
<input
|
|
45
47
|
ref={setRef}
|
|
46
48
|
type="checkbox"
|
|
47
49
|
className="ogx-checkbox__input"
|
|
48
|
-
aria-label={label || props['aria-label'] || (indeterminate ? 'Select some' : props.checked ? 'Deselect' : 'Select')}
|
|
49
50
|
{...props}
|
|
50
51
|
/>
|
|
51
52
|
<span className="ogx-checkbox__box">
|
|
@@ -27,12 +27,3 @@
|
|
|
27
27
|
display: block;
|
|
28
28
|
transition: transform 0.2s ease;
|
|
29
29
|
}
|
|
30
|
-
@media (prefers-color-scheme: dark) {
|
|
31
|
-
.ogx-expand-icon {
|
|
32
|
-
color: var(--ogx-color-gray-400);
|
|
33
|
-
}
|
|
34
|
-
.ogx-expand-icon:hover {
|
|
35
|
-
background: var(--ogx-color-gray-800);
|
|
36
|
-
color: var(--ogx-color-gray-100);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -5,9 +5,11 @@ export interface ExpandIconProps {
|
|
|
5
5
|
isExpanded: boolean;
|
|
6
6
|
onClick?: (event: React.MouseEvent) => void;
|
|
7
7
|
variant?: 'chevron' | 'plus-minus';
|
|
8
|
+
/** Pass -1 inside grid cells: the cell, not the button, is the keyboard focus stop. */
|
|
9
|
+
tabIndex?: number;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export function ExpandIcon({ isExpanded, onClick, variant = 'chevron' }: ExpandIconProps) {
|
|
12
|
+
export function ExpandIcon({ isExpanded, onClick, variant = 'chevron', tabIndex }: ExpandIconProps) {
|
|
11
13
|
return (
|
|
12
14
|
<button
|
|
13
15
|
className={`ogx-expand-icon ${isExpanded ? 'ogx-expand-icon--expanded' : ''}`}
|
|
@@ -18,6 +20,7 @@ export function ExpandIcon({ isExpanded, onClick, variant = 'chevron' }: ExpandI
|
|
|
18
20
|
}}
|
|
19
21
|
aria-label={isExpanded ? 'Collapse row' : 'Expand row'}
|
|
20
22
|
type="button"
|
|
23
|
+
tabIndex={tabIndex}
|
|
21
24
|
>
|
|
22
25
|
{variant === 'plus-minus' ? (
|
|
23
26
|
<svg
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GridApi } from '../../types';
|
|
2
|
+
|
|
3
|
+
const noop = () => {};
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A complete GridApi whose methods are safe no-ops. `useGridApiRef()` starts with one so
|
|
7
|
+
* `apiRef.current` is never null (its type says it is not) before the grid has mounted;
|
|
8
|
+
* the grid replaces it with the live API when it mounts.
|
|
9
|
+
*/
|
|
10
|
+
export function createGridApiPlaceholder(): GridApi {
|
|
11
|
+
return {
|
|
12
|
+
getRow: () => null,
|
|
13
|
+
getAllRows: () => [],
|
|
14
|
+
getVisibleRows: () => [],
|
|
15
|
+
getAllFilteredRows: () => [],
|
|
16
|
+
getGroupedExportRows: () => null,
|
|
17
|
+
getAggregationResult: () => null,
|
|
18
|
+
getAggregationModel: () => null,
|
|
19
|
+
getColumn: () => null,
|
|
20
|
+
getAllColumns: () => [],
|
|
21
|
+
getVisibleColumns: () => [],
|
|
22
|
+
selectRow: noop,
|
|
23
|
+
selectRows: noop,
|
|
24
|
+
getSelectedRows: () => [],
|
|
25
|
+
sortColumn: noop,
|
|
26
|
+
getSortModel: () => [],
|
|
27
|
+
setFilterModel: noop,
|
|
28
|
+
getFilterModel: () => ({ items: [] }),
|
|
29
|
+
setPage: noop,
|
|
30
|
+
setPageSize: noop,
|
|
31
|
+
scrollToIndexes: noop,
|
|
32
|
+
copySelectedRows: () => Promise.resolve(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lets a `GridToolbar` rendered in `slots.toolbar` tell the grid that it will show the Columns
|
|
5
|
+
* panel when the column menu asks for it (through `forceColumnsOpen`). When no toolbar has
|
|
6
|
+
* registered (a custom toolbar that does not render `GridToolbar`, or one that does not forward
|
|
7
|
+
* the grid's props), the column menu's **Manage columns** opens the standalone panel instead.
|
|
8
|
+
*/
|
|
9
|
+
export interface GridToolbarHost {
|
|
10
|
+
/** Registers a toolbar that can show the Columns panel. Returns the function that unregisters it. */
|
|
11
|
+
registerColumnsPanel: () => () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const GridToolbarHostContext = createContext<GridToolbarHost | null>(null);
|