@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.
Files changed (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. 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>(document.body);
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: window.innerWidth - rect.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, aggFn: GridPivotAggFn = 'sum') {
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(field: string) {
91
- onChange({ ...model, rowFields: model.rowFields.filter((f) => f !== field) });
105
+ function removeRow(index: number) {
106
+ onChange({ ...model, rowFields: model.rowFields.filter((_, i) => i !== index) });
92
107
  }
93
- function removeCol(field: string) {
94
- onChange({ ...model, columnFields: model.columnFields.filter((f) => f !== field) });
108
+ function removeCol(index: number) {
109
+ onChange({ ...model, columnFields: model.columnFields.filter((_, i) => i !== index) });
95
110
  }
96
- function removeValue(field: string) {
97
- onChange({ ...model, valueFields: model.valueFields.filter((v) => v.field !== field) });
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(field: string, aggFn: GridPivotAggFn) {
115
+ function changeValueFn(index: number, aggFn: GridPivotAggFn) {
100
116
  onChange({
101
117
  ...model,
102
- valueFields: model.valueFields.map((v) => v.field === field ? { ...v, aggFn } : 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 colLabel = (field: string) =>
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 className="ogx-pivot-chip__remove" onClick={() => removeRow(f)} title={`Remove ${colLabel(f)}`}>×</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 className="ogx-pivot-chip__remove" onClick={() => removeCol(f)} title={`Remove ${colLabel(f)}`}>×</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
- <span key={vf.field} className="ogx-pivot-chip ogx-pivot-chip--value">
189
- {colLabel(vf.field)}
190
- <select
191
- className="ogx-pivot-chip__fn-select"
192
- value={vf.aggFn}
193
- onChange={(e) => changeValueFn(vf.field, e.target.value as GridPivotAggFn)}
194
- title="Aggregation function"
195
- >
196
- {AGG_FNS.map((fn) => (
197
- <option key={fn} value={fn}>{fn.toUpperCase()}</option>
198
- ))}
199
- </select>
200
- <button className="ogx-pivot-chip__remove" onClick={() => removeValue(vf.field)} title={`Remove ${colLabel(vf.field)}`}>×</button>
201
- </span>
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
- {columns.map((col) => {
213
- const isNum = numericFields.has(col.field);
214
- const used = isUsed(col.field);
215
- if (used) return null;
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
- {col.headerName ?? col.field}
255
+ {label}
220
256
  </span>
221
257
  <div className="ogx-pivot-field-actions">
222
- <button className="ogx-pivot-add-btn ogx-pivot-add-btn--row" onClick={() => addRow(col.field)} title="Add to Row Fields">Row</button>
223
- <button className="ogx-pivot-add-btn ogx-pivot-add-btn--col" onClick={() => addCol(col.field)} title="Add to Column Fields">Col</button>
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
- {columns.every((col) => isUsed(col.field)) && (
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
- const timerRef = useRef<NodeJS.Timeout | null>(null);
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
- let t = 0;
30
- let l = 0;
31
-
32
- // Note: These calculations are for positioning absolute relative to document.body
33
- const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
34
- const scrollY = window.pageYOffset || document.documentElement.scrollTop;
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
- updateCoords();
50
- window.addEventListener('scroll', updateCoords, true);
51
- window.addEventListener('resize', updateCoords);
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
- const handleMouseEnter = () => {
60
- if (timerRef.current) clearTimeout(timerRef.current);
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 handleMouseLeave = () => {
67
- if (timerRef.current) clearTimeout(timerRef.current);
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={handleMouseEnter}
86
- onMouseLeave={handleMouseLeave}
114
+ onMouseEnter={show}
115
+ onMouseLeave={hide}
116
+ onFocus={show}
117
+ onBlur={hide}
87
118
  >
88
- {children}
119
+ {child}
89
120
  </span>
90
- {open && title && ReactDOM.createPortal(
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: 'absolute',
129
+ position: 'fixed',
97
130
  zIndex: 99999
98
131
  }}
99
132
  >
100
133
  {title}
101
134
  </div>,
102
- document.body
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);