@opencorestack/opengridx 3.4.0 → 3.5.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 (76) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +6 -0
  3. package/dist/ai.cjs +2 -1
  4. package/dist/ai.cjs.map +1 -1
  5. package/dist/ai.d.ts +203 -3
  6. package/dist/ai.es.js +2 -1
  7. package/dist/ai.es.js.map +1 -1
  8. package/dist/index.d.ts +248 -4
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +7104 -5726
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -16
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +88 -7
  15. package/docs/README.md +3 -1
  16. package/docs/architecture/datagrid-orchestration.md +9 -4
  17. package/docs/components/filter-panel.md +1 -0
  18. package/docs/components/header.md +4 -1
  19. package/docs/components/toolbar.md +7 -0
  20. package/docs/customization/theming.md +3 -1
  21. package/docs/features/ai-toolkit.md +99 -5
  22. package/docs/features/cell-selection.md +8 -2
  23. package/docs/features/fill-handle.md +125 -0
  24. package/docs/features/filtering.md +3 -0
  25. package/docs/features/header-filters.md +138 -0
  26. package/docs/features/keyboard-navigation.md +20 -2
  27. package/docs/roadmap.md +2 -3
  28. package/lib/ai/agentTools.ts +160 -0
  29. package/lib/ai/index.ts +19 -2
  30. package/lib/ai/promptHandler.ts +109 -0
  31. package/lib/ai/types.ts +89 -25
  32. package/lib/ai/validate.ts +10 -0
  33. package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
  34. package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
  35. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
  36. package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
  37. package/lib/components/Cell/Cell.css +28 -0
  38. package/lib/components/Cell/Cell.tsx +6 -2
  39. package/lib/components/DataGrid/DataGrid.tsx +76 -3
  40. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
  41. package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
  42. package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
  43. package/lib/components/FilterPanel/FilterPanel.tsx +2 -9
  44. package/lib/components/Header/Header.tsx +44 -23
  45. package/lib/components/Header/headerColumnStyle.ts +28 -0
  46. package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
  47. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
  48. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
  49. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
  50. package/lib/components/Toolbar/GridToolbar.tsx +51 -0
  51. package/lib/hooks/core/gridApiPlaceholder.ts +8 -0
  52. package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
  53. package/lib/hooks/core/useGridApiMethods.ts +39 -0
  54. package/lib/hooks/core/useGridAriaRows.ts +5 -3
  55. package/lib/hooks/core/useGridCellSelection.ts +77 -11
  56. package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
  57. package/lib/hooks/core/useGridControlledState.ts +27 -0
  58. package/lib/hooks/core/useGridFillHandle.ts +323 -0
  59. package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
  60. package/lib/hooks/core/useGridToolbarProps.ts +19 -0
  61. package/lib/hooks/features/useGridAiAssistant.ts +284 -0
  62. package/lib/hooks/features/useGridBatchEdit.ts +2 -2
  63. package/lib/index.ts +3 -0
  64. package/lib/styles/opengridx.css +2 -0
  65. package/lib/theme/DataGridThemeProvider.tsx +2 -0
  66. package/lib/theme/types.ts +2 -0
  67. package/lib/types/index.ts +219 -1
  68. package/lib/utils/aiAssistant.ts +205 -0
  69. package/lib/utils/aiBrand.ts +17 -0
  70. package/lib/utils/cellSelection.ts +25 -5
  71. package/lib/utils/editing/fill.ts +238 -0
  72. package/lib/utils/filtering/index.ts +14 -0
  73. package/lib/utils/focus/index.ts +9 -3
  74. package/lib/utils/headerFilters.ts +189 -0
  75. package/llms.txt +44 -4
  76. package/package.json +1 -1
@@ -0,0 +1,74 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { attempt } from '../../utils/attempt';
3
+
4
+ /** The part of the Web Speech API's SpeechRecognition the panel uses. */
5
+ interface SpeechRecognitionLike {
6
+ lang: string;
7
+ interimResults: boolean;
8
+ maxAlternatives: number;
9
+ onresult: ((event: { results: ArrayLike<ArrayLike<{ transcript: string }>> }) => void) | null;
10
+ onend: (() => void) | null;
11
+ onerror: (() => void) | null;
12
+ start: () => void;
13
+ stop: () => void;
14
+ abort: () => void;
15
+ }
16
+
17
+ type SpeechRecognitionConstructor = new () => SpeechRecognitionLike;
18
+
19
+ function getSpeechRecognition(): SpeechRecognitionConstructor | null {
20
+ if (typeof window === 'undefined') return null;
21
+ const w = window as unknown as { SpeechRecognition?: SpeechRecognitionConstructor; webkitSpeechRecognition?: SpeechRecognitionConstructor };
22
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
23
+ }
24
+
25
+ /**
26
+ * Dictation for the prompt input. The browser asks for the microphone only when `toggle` starts it
27
+ * (a click), never on load; without speech recognition `toggle` does nothing.
28
+ */
29
+ export function useSpeechInput(onText: (text: string) => void): { listening: boolean; toggle: () => void } {
30
+ const [listening, setListening] = useState(false);
31
+ const recognitionRef = useRef<SpeechRecognitionLike | null>(null);
32
+ const onTextRef = useRef(onText);
33
+ useLayoutEffect(() => {
34
+ onTextRef.current = onText;
35
+ });
36
+
37
+ useEffect(() => () => {
38
+ const recognition = recognitionRef.current;
39
+ recognitionRef.current = null;
40
+ if (recognition) attempt(() => recognition.abort());
41
+ }, []);
42
+
43
+ const toggle = useCallback(() => {
44
+ const running = recognitionRef.current;
45
+ if (running) {
46
+ attempt(() => running.stop());
47
+ return;
48
+ }
49
+ const Recognition = getSpeechRecognition();
50
+ if (!Recognition) return;
51
+ const created = attempt(() => new Recognition());
52
+ if (!created.ok) return;
53
+ const recognition = created.value;
54
+ recognition.lang = typeof navigator === 'undefined' ? 'en-US' : navigator.language;
55
+ recognition.interimResults = false;
56
+ recognition.maxAlternatives = 1;
57
+ recognition.onresult = (event) => {
58
+ const first = event.results[0];
59
+ const transcript = first && first[0] ? first[0].transcript : '';
60
+ if (transcript) onTextRef.current(transcript);
61
+ };
62
+ const done = () => {
63
+ if (recognitionRef.current === recognition) recognitionRef.current = null;
64
+ setListening(false);
65
+ };
66
+ recognition.onend = done;
67
+ recognition.onerror = done;
68
+ recognitionRef.current = recognition;
69
+ setListening(true);
70
+ if (!attempt(() => recognition.start()).ok) done();
71
+ }, []);
72
+
73
+ return { listening, toggle };
74
+ }
@@ -52,6 +52,34 @@
52
52
  .ogx__cell--range-right {
53
53
  --ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
54
54
  }
55
+ /*
56
+ * Fill handle (v3.5): the square on the bottom-right cell of the range. The cell clips its overflow,
57
+ * so the 24px hit area lies inside the corner and the visible square sits in its corner.
58
+ * touch-action: none lets a touch drag on it fill instead of scrolling.
59
+ */
60
+ .ogx__cell-fill-handle {
61
+ position: absolute;
62
+ right: 0;
63
+ bottom: 0;
64
+ width: 24px;
65
+ height: 24px;
66
+ max-width: 100%;
67
+ max-height: 100%;
68
+ cursor: crosshair;
69
+ touch-action: none;
70
+ z-index: 1;
71
+ }
72
+ .ogx__cell-fill-handle::after {
73
+ content: '';
74
+ position: absolute;
75
+ right: 1px;
76
+ bottom: 1px;
77
+ width: 7px;
78
+ height: 7px;
79
+ box-sizing: border-box;
80
+ background-color: var(--ogx-fill-handle-color, var(--ogx-range-border));
81
+ border: 1px solid var(--ogx-grid-background, #fff);
82
+ }
55
83
  /* While a pointer drag selects cells, the browser must not select text as well. */
56
84
  .ogx--range-dragging,
57
85
  .ogx--range-dragging * {
@@ -7,7 +7,7 @@ import { GridEditInputCell } from './GridEditInputCell';
7
7
  import { CellErrorBoundary } from './CellErrorBoundary';
8
8
  import { formatValueByType } from '../../utils/values';
9
9
  import { attempt } from '../../utils/attempt';
10
- import { RANGE_BOTTOM, RANGE_CELL, RANGE_LEFT, RANGE_RIGHT, RANGE_TOP } from '../../utils/cellSelection';
10
+ import { RANGE_BOTTOM, RANGE_CELL, RANGE_FILL_HANDLE, RANGE_LEFT, RANGE_RIGHT, RANGE_TOP } from '../../utils/cellSelection';
11
11
 
12
12
  export interface CellProps<R extends GridRowModel = GridRowModel> {
13
13
  onCellClick?: (params: GridCellParams<R>) => void;
@@ -50,7 +50,8 @@ export interface CellProps<R extends GridRowModel = GridRowModel> {
50
50
  valueError?: unknown;
51
51
  /**
52
52
  * Place of the cell in the selected cell range (`cellSelection`), as bit flags: in range 1, top
53
- * edge 2, bottom 4, left 8, right 16. 0 or absent: not in a range. @since v3.3
53
+ * edge 2, bottom 4, left 8, right 16; 32 draws the fill handle (v3.5). 0 or absent: not in a range.
54
+ * @since v3.3
54
55
  */
55
56
  rangeFlags?: number;
56
57
  }
@@ -341,6 +342,9 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
341
342
  formattedValue
342
343
  )}
343
344
  </div>
345
+ {(rangeFlags & RANGE_FILL_HANDLE) !== 0 && !isEditing && (
346
+ <div className="ogx__cell-fill-handle" aria-hidden="true" />
347
+ )}
344
348
  </div>
345
349
  );
346
350
  }
@@ -30,6 +30,7 @@ import { useGridEditing } from '../../hooks/features/useGridEditing';
30
30
  import { useGridBatchEdit } from '../../hooks/features/useGridBatchEdit';
31
31
  import { useGridClipboardPaste } from '../../hooks/core/useGridClipboardPaste';
32
32
  import { useGridUndoRedo, useGridEditCommitChannel, UNDO_HISTORY_LIMIT } from '../../hooks/core/useGridUndoRedo';
33
+ import { useGridFillHandle } from '../../hooks/core/useGridFillHandle';
33
34
  import { useGridSpanning } from '../../hooks/features/useGridSpanning';
34
35
  import { useGridSpanRowWindow, useGridSpanColumnWindow } from '../../hooks/features/useGridSpanRenderWindow';
35
36
  import { useColumnGroupReorderGuard } from '../../hooks/features/useColumnGroupReorderGuard';
@@ -43,7 +44,7 @@ import { GridLoadingOverlay } from './GridLoadingOverlay';
43
44
  import { GridPinnedRows } from './GridPinnedRows';
44
45
  import { GridVirtualRows } from './GridVirtualRows';
45
46
  import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
46
- import { resolveGridModes, EMPTY_ROW_GROUPING_MODEL } from '../../utils/gridModes';
47
+ import { resolveGridModes } from '../../utils/gridModes';
47
48
  import { useGridGroupingColumn } from '../../hooks/core/useGridGroupingColumn';
48
49
  import { useGridRowIdOf, getDefaultRowId } from '../../hooks/core/useGridRowIdOf';
49
50
  import { useGridApiRefBinding } from '../../hooks/core/useGridApiRefBinding';
@@ -65,6 +66,9 @@ import { GridPaginationArea } from './GridPaginationArea';
65
66
  import { GridLiveRegion } from './GridLiveRegion';
66
67
  import { useGridAriaRows } from '../../hooks/core/useGridAriaRows';
67
68
  import { useGridToolbarProps } from '../../hooks/core/useGridToolbarProps';
69
+ import { useGridAiAssistant } from '../../hooks/features/useGridAiAssistant';
70
+ import { GridAiAssistantArea } from '../AiAssistant/GridAiAssistantArea';
71
+ import { DEFAULT_HEADER_FILTER_HEIGHT } from '../../utils/headerFilters';
68
72
  import type { DataGridProps, DataGridUntypedColumnsProps, GridValidRowModel, GridRowModel, GridRowId, GridColDef } from '../../types';
69
73
 
70
74
  /**
@@ -83,6 +87,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
83
87
  rowHeight: rowHeightProp,
84
88
  headerHeight: headerHeightProp,
85
89
  wrapHeaderText = false,
90
+ headerFilters = false,
91
+ headerFilterHeight = DEFAULT_HEADER_FILTER_HEIGHT,
86
92
  autoHeight = false,
87
93
  density,
88
94
  checkboxSelection = false,
@@ -98,6 +104,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
98
104
  onClipboardPaste,
99
105
  onBeforeClipboardPaste,
100
106
  undoRedo = false,
107
+ disableFillHandle = false,
108
+ onFill,
101
109
  onHistoryChange,
102
110
  rowSelectionModel: propRowSelectionModel,
103
111
  onRowSelectionModelChange: propOnRowSelectionModelChange,
@@ -138,6 +146,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
138
146
  groupingColDef,
139
147
 
140
148
  rowGroupingModel: propRowGroupingModel,
149
+ onRowGroupingModelChange,
141
150
 
142
151
  aggregationModel: propAggregationModel,
143
152
  onAggregationModelChange,
@@ -177,12 +186,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
177
186
  localeText,
178
187
  apiRef: propApiRef,
179
188
  overscanRowCount = 3,
189
+ aiAssistant,
190
+ onAiAssistantApply,
191
+ onAiAssistantError,
180
192
  } = props;
181
193
  // Equal inline column definitions keep one identity, so the row passes memoised on it do not re-run.
182
194
  const columns = useStableColumns(columnsProp);
183
195
 
184
196
  const effectiveNoRowsLabel = localeText?.noRowsLabel ?? noRowsLabel;
185
- const rowGroupingModel = propRowGroupingModel || EMPTY_ROW_GROUPING_MODEL;
186
197
 
187
198
  const controlledState = useGridControlledState({
188
199
  initialState,
@@ -192,6 +203,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
192
203
  onFilterModelChange,
193
204
  aggregationModel: propAggregationModel,
194
205
  onAggregationModelChange,
206
+ rowGroupingModel: propRowGroupingModel,
207
+ onRowGroupingModelChange,
195
208
  columnVisibilityModel: propColumnVisibilityModel,
196
209
  onColumnVisibilityModelChange,
197
210
  pinnedColumns: propPinnedColumns,
@@ -213,6 +226,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
213
226
  selectedRowIds: selectionModelRowIds,
214
227
  handleRowSelectionModelChange,
215
228
  density: effectiveDensity,
229
+ rowGroupingModel, handleRowGroupingModelChange,
216
230
  } = controlledState;
217
231
 
218
232
  const pivot = useGridPivot({
@@ -226,6 +240,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
226
240
  hierarchyRequested: treeData || rowGroupingModel.length > 0,
227
241
  });
228
242
  const isPivotActive = pivot.isActive;
243
+ // The header filter row filters the source columns; pivot columns are generated, so it is off then.
244
+ const showHeaderFilters = headerFilters && !isPivotActive;
229
245
  const {
230
246
  rowReordering, hierarchyRowGroupingModel, isTreeDataRequested, isTreeData,
231
247
  isRowGrouping, isHierarchyEnabled, hasGroupingColumn, pagination, listView,
@@ -459,6 +475,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
459
475
  dataRows,
460
476
  }),
461
477
  visibleColumns: renderOrderedColumns as unknown as GridColDef[],
478
+ rowGroupingModel,
479
+ onRowGroupingModelChange: handleRowGroupingModelChange,
480
+ aggregationModel,
481
+ onAggregationModelChange: handleAggregationModelChange,
482
+ columnVisibilityModel,
483
+ onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
484
+ pivotModel: currentPivotModel,
485
+ onPivotModelChange: handlePivotModelChange,
462
486
  });
463
487
 
464
488
  // Indices address the consumer's own rows, whatever the sort, filter, page or pinning.
@@ -596,6 +620,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
596
620
  pinExpandColumn,
597
621
  rowHeight: effectiveRowHeight,
598
622
  cellSelection: cellSelection.keyboard,
623
+ headerFilters: showHeaderFilters,
599
624
  });
600
625
 
601
626
  const { handleCellClick, handleHeaderClick } = useGridPointerFocusHandlers<R>({
@@ -650,6 +675,23 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
650
675
  pinnedTopRowCount: pinnedTopRows.length,
651
676
  });
652
677
 
678
+ // Fill handle (cellSelection): drag the range's corner, Ctrl/Cmd+D and Ctrl/Cmd+R.
679
+ const fillHandle = useGridFillHandle<R>({
680
+ containerRef, viewportRef,
681
+ selection: cellSelection,
682
+ disableFillHandle,
683
+ isEditing: editingHandlers.editingCell !== null,
684
+ allRenderableRows,
685
+ getRowId: getRowIdOf,
686
+ navigationColumns: navigationColumns as unknown as GridColDef<R>[],
687
+ columnIndexMap,
688
+ isCellEditable,
689
+ rowMetaMap,
690
+ getSpanOrigin: spanning.getSpanOrigin,
691
+ batchEdit,
692
+ onFill,
693
+ });
694
+
653
695
  const { handleHideColumn, handlePinColumn } = useGridColumnMenuHandlers({
654
696
  columnVisibilityModel,
655
697
  onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
@@ -659,6 +701,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
659
701
 
660
702
  const ariaRows = useGridAriaRows({
661
703
  columnGroupingModel,
704
+ headerFilterRow: showHeaderFilters,
662
705
  pinnedTopCount: pinnedTopRows.length,
663
706
  pinnedBottomCount: pinnedBottomRows.length,
664
707
  pagination,
@@ -713,6 +756,15 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
713
756
  const columnsPanel = useGridColumnsPanel();
714
757
  useGridStylesheetWarning(containerRef);
715
758
 
759
+ // The aiAssistant prompt panel (v3.5): applies validated replies through apiRef; apiRef.openAiAssistant.
760
+ const aiAssistantPanel = useGridAiAssistant({
761
+ apiRef,
762
+ options: aiAssistant,
763
+ columns: activeColumns as unknown as GridColDef[],
764
+ onApply: onAiAssistantApply,
765
+ onError: onAiAssistantError,
766
+ });
767
+
716
768
  const NoRowsOverlaySlot = slots?.noRowsOverlay;
717
769
  const LoadingOverlaySlot = slots?.loadingOverlay;
718
770
  const loadingOverlay = LoadingOverlaySlot ? <LoadingOverlaySlot {...slotProps?.loadingOverlay} /> : undefined;
@@ -743,6 +795,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
743
795
  disableColumnReorder, columnGroupingModel, moveColumn, resetColumnOrder,
744
796
  forceColumnsOpen: columnsPanel.toolbarPanelRequested,
745
797
  onColumnsPanelClose: columnsPanel.closeToolbarPanel,
798
+ aiAssistantEnabled: aiAssistantPanel.enabled,
799
+ aiAssistantOpen: aiAssistantPanel.open,
800
+ onAiAssistantToggle: aiAssistantPanel.togglePanel,
801
+ aiAssistantLabel: localeText?.aiAssistantButton,
802
+ forceFiltersOpen: columnsPanel.toolbarFiltersRequested,
803
+ onFiltersPanelClose: columnsPanel.closeToolbarFiltersPanel,
746
804
  });
747
805
 
748
806
  // Props the row renderers (top-pinned, centre and bottom-pinned rows) all take.
@@ -764,6 +822,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
764
822
  rowMetaMap,
765
823
  cellRange: cellSelection.displayRange,
766
824
  cellRangeHasRowSpan: cellSelection.hasRowSpan,
825
+ fillHandle: fillHandle.handle,
767
826
  };
768
827
 
769
828
  return (
@@ -774,7 +833,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
774
833
  ...style,
775
834
  height: height ?? style?.height,
776
835
  '--ogx-row-height': `${effectiveRowHeight}px`,
777
- '--ogx-header-height': `${headerHeight}px`
836
+ '--ogx-header-height': `${headerHeight}px`,
837
+ ...(showHeaderFilters ? { '--ogx-header-filter-height': `${headerFilterHeight}px` } : undefined),
778
838
  } as unknown as React.CSSProperties}
779
839
  aria-busy={effectiveLoading}
780
840
  >
@@ -784,6 +844,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
784
844
  </GridToolbarHostContext.Provider>
785
845
  )}
786
846
 
847
+ {aiAssistantPanel.panelProps && (
848
+ <GridAiAssistantArea
849
+ panelProps={aiAssistantPanel.panelProps}
850
+ slot={slots?.aiAssistantPanel}
851
+ slotProps={slotProps?.aiAssistantPanel}
852
+ />
853
+ )}
854
+
787
855
  {/* Used by the column menu's Manage columns when no GridToolbar can show the panel */}
788
856
  <GridStandaloneColumnPanel<R>
789
857
  isOpen={columnsPanel.standalonePanelOpen}
@@ -912,6 +980,10 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
912
980
  onPinColumn={handlePinColumn}
913
981
  wrapHeaderText={wrapHeaderText}
914
982
  headerHeight={headerHeight}
983
+ headerFilters={showHeaderFilters}
984
+ filterModel={filterModel}
985
+ onFilterModelChange={handleFilterModelChange}
986
+ onOpenFilterPanel={columnsPanel.openFiltersPanel}
915
987
  />
916
988
 
917
989
  <GridPinnedRows<R>
@@ -1035,6 +1107,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1035
1107
  dataRowCount={dataRows.length}
1036
1108
  filterModel={filterModel}
1037
1109
  cellSelectionAnnouncement={cellSelection.announcement}
1110
+ aiAssistantAnnouncement={aiAssistantPanel.announcement}
1038
1111
  />
1039
1112
 
1040
1113
  {showLoadingOverRows && (
@@ -13,13 +13,15 @@ interface GridLiveRegionProps {
13
13
  filterModel: GridFilterModel | undefined;
14
14
  /** The size of the selected cell range (`cellSelection`), already debounced. */
15
15
  cellSelectionAnnouncement?: string;
16
+ /** The `aiAssistant` status line ("Thinking…", "Applied 3 changes", an error). */
17
+ aiAssistantAnnouncement?: string;
16
18
  }
17
19
 
18
20
  /**
19
21
  * Polite status announcements: loading, a load error, no rows, or the row count of an active filter;
20
- * and the size of the selected cell range.
22
+ * the size of the selected cell range; and the AI assistant's status.
21
23
  */
22
- export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement }: GridLiveRegionProps) {
24
+ export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement, aiAssistantAnnouncement }: GridLiveRegionProps) {
23
25
  return (
24
26
  <div className="ogx-aria-live-status" role="status" aria-live="polite">
25
27
  {loading ? 'Loading data...' : ''}
@@ -32,6 +34,7 @@ export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount,
32
34
  : ''
33
35
  )}
34
36
  {cellSelectionAnnouncement ? <span className="ogx-aria-live-status__cells"> {cellSelectionAnnouncement}</span> : null}
37
+ {aiAssistantAnnouncement ? <span className="ogx-aria-live-status__ai"> {aiAssistantAnnouncement}</span> : null}
35
38
  </div>
36
39
  );
37
40
  }
@@ -17,6 +17,7 @@ import type {
17
17
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
18
18
  import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
19
19
  import { getRowCellRange } from '../../utils/cellSelection';
20
+ import type { GridFillHandlePosition } from '../../utils/cellSelection';
20
21
  import type { GridEditingState } from '../../hooks/features/useGridEditing';
21
22
 
22
23
  interface PinnedRowEditingHandlers {
@@ -64,6 +65,8 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
64
65
  cellRange?: GridDisplayCellRange | null;
65
66
  /** The grid has row spans: rows tell their cells how far the range bottom is. */
66
67
  cellRangeHasRowSpan?: boolean;
68
+ /** Where the fill handle is drawn (v3.5), or null. */
69
+ fillHandle?: GridFillHandlePosition | null;
67
70
  /** Pinned rows are edited like any other row. */
68
71
  editingHandlers?: PinnedRowEditingHandlers;
69
72
  isCellEditable?: (params: GridCellParams<R>) => boolean;
@@ -111,6 +114,7 @@ export function GridPinnedRows<R extends GridRowModel>({
111
114
  rowReordering = false,
112
115
  cellRange,
113
116
  cellRangeHasRowSpan = false,
117
+ fillHandle,
114
118
  }: GridPinnedRowsProps<R>) {
115
119
  if (rows.length === 0) return null;
116
120
 
@@ -156,7 +160,7 @@ export function GridPinnedRows<R extends GridRowModel>({
156
160
  onEditCellValueChange={editingHandlers?.setEditCellValue}
157
161
  isCellEditable={isCellEditable}
158
162
  rowReordering={rowReordering}
159
- cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan)}
163
+ cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan, fillHandle)}
160
164
  />
161
165
  );
162
166
  })}
@@ -20,6 +20,7 @@ import type { GridEditingState } from '../../hooks/features/useGridEditing';
20
20
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
21
21
  import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
22
22
  import { getRowCellRange } from '../../utils/cellSelection';
23
+ import type { GridFillHandlePosition } from '../../utils/cellSelection';
23
24
 
24
25
  interface RowEditingHandlers {
25
26
  editingCell: GridEditingState['editingCell'];
@@ -77,6 +78,8 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
77
78
  cellRange?: GridDisplayCellRange | null;
78
79
  /** The grid has row spans: rows tell their cells how far the range bottom is. */
79
80
  cellRangeHasRowSpan?: boolean;
81
+ /** Where the fill handle is drawn (v3.5), or null. */
82
+ fillHandle?: GridFillHandlePosition | null;
80
83
  }
81
84
 
82
85
  export function GridVirtualRows<R extends GridRowModel>({
@@ -123,6 +126,7 @@ export function GridVirtualRows<R extends GridRowModel>({
123
126
  columnIndexMap,
124
127
  cellRange,
125
128
  cellRangeHasRowSpan = false,
129
+ fillHandle,
126
130
  }: GridVirtualRowsProps<R>) {
127
131
  const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
128
132
  ? baseColumns
@@ -214,7 +218,7 @@ export function GridVirtualRows<R extends GridRowModel>({
214
218
  rowSpanningCaches={rowSpanningCaches}
215
219
  rowHeight={rowHeight}
216
220
  rowMeta={rowMetaMap.get(id)}
217
- cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan)}
221
+ cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan, fillHandle)}
218
222
  />
219
223
  ))
220
224
  )}
@@ -6,8 +6,7 @@ import type {
6
6
  GridColDef,
7
7
  GridFilterOperator
8
8
  } from '../../types';
9
- import { getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
10
- import { toLocalDateString } from '../../utils/values';
9
+ import { filterValueToInputText, getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
11
10
  import { Input } from '../ui/Input';
12
11
 
13
12
 
@@ -74,13 +73,7 @@ function matchesOption(option: ValueOption, value: unknown): boolean {
74
73
 
75
74
  /** Text shown in the value input for a committed value. Never used to emit anything. */
76
75
  function toInputText(value: unknown, col: GridColDef): string {
77
- if (value == null) return '';
78
- if (col.type === 'date' || value instanceof Date) {
79
- const day = toLocalDateString(value);
80
- return day || (typeof value === 'string' ? value : '');
81
- }
82
- if (Array.isArray(value)) return value.map(v => String(v)).join(', ');
83
- return String(value);
76
+ return filterValueToInputText(value, col.type);
84
77
  }
85
78
 
86
79
  /** The item value for typed text. isAnyOf takes a comma-separated list. */
@@ -2,12 +2,14 @@ import React, { useId } from 'react';
2
2
  import { Checkbox } from '../ui/Checkbox';
3
3
  import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
4
4
  import { clampResizeWidth } from '../ColumnResizeHandle/clampResizeWidth';
5
- import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel } from '../../types';
5
+ import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel, GridFilterModel } from '../../types';
6
6
  import { isColumnPinned, calculatePinnedPositions, getPinnedEdgeFields } from '../../utils/pinning';
7
7
  import { getRenderedColumnWidth } from '../../utils/columnWidth';
8
8
  import { buildColumnGroupRow, getColumnGroupDepth, getColumnGroupPaths } from '../../utils/columnGroups';
9
9
  import { ColumnMenu } from './ColumnMenu';
10
10
  import { getHeaderLineClamp, shouldWrapHeaderText } from '../../utils/headerWrap';
11
+ import { getHeaderColumnStyle } from './headerColumnStyle';
12
+ import { HeaderFilterRow } from '../HeaderFilter/HeaderFilterRow';
11
13
 
12
14
 
13
15
  function MenuIcon() {
@@ -60,8 +62,11 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
60
62
  onDragOver?: (field: string) => (event: React.DragEvent) => void;
61
63
  onDragEnd?: () => void;
62
64
  onDrop?: (targetField: string) => (event: React.DragEvent) => void;
63
- /** The grid's focus position; `id: null` means a header cell is focused. */
64
- focusedCell?: { id: string | number | null; field: string } | null;
65
+ /**
66
+ * The grid's focus position; `id: null` means a header cell is focused, and `headerFilter: true`
67
+ * a cell of the header filter row.
68
+ */
69
+ focusedCell?: { id: string | number | null; field: string; headerFilter?: boolean } | null;
65
70
  onHeaderClick?: (field: string) => void;
66
71
  onSortAdd?: (field: string, direction: GridSortDirection) => void;
67
72
  multiSort?: boolean;
@@ -74,6 +79,14 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
74
79
  wrapHeaderText?: boolean;
75
80
  /** Header row height in pixels; sets how many lines a wrapped title may take. Defaults to 56. */
76
81
  headerHeight?: number;
82
+ /** Renders the header filter row under the column headers (`DataGridProps.headerFilters`). */
83
+ headerFilters?: boolean;
84
+ /** The filter model the header filter row reads. */
85
+ filterModel?: GridFilterModel;
86
+ /** Receives the filter model after a header filter changes. */
87
+ onFilterModelChange?: (model: GridFilterModel) => void;
88
+ /** Opens the filter panel; a header filter cell showing "Custom filter" calls it. */
89
+ onOpenFilterPanel?: () => void;
77
90
  }
78
91
 
79
92
  export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
@@ -112,6 +125,10 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
112
125
  columnIndexMap,
113
126
  wrapHeaderText = false,
114
127
  headerHeight = 56,
128
+ headerFilters = false,
129
+ filterModel,
130
+ onFilterModelChange,
131
+ onOpenFilterPanel,
115
132
  } = props;
116
133
 
117
134
  const selectAllId = useId();
@@ -148,7 +165,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
148
165
  setMenuOpenParams({ colDef, anchorEl: anchor });
149
166
  };
150
167
 
151
- const isHeaderFocused = (field: string) => focusedCell?.id === null && focusedCell.field === field;
168
+ const isHeaderFocused = (field: string) => focusedCell?.id === null && !focusedCell.headerFilter && focusedCell.field === field;
152
169
 
153
170
  // aria-colindex: system columns first (reorder, expand, checkbox), then the data columns.
154
171
  const reorderColIndex = rowReordering ? 1 : 0;
@@ -437,30 +454,12 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
437
454
  colDef.headerClassName
438
455
  ].filter(Boolean).join(' ');
439
456
 
440
- const effectiveWidth = columnWidths[colDef.field] ?? colDef.width ?? 100;
441
-
442
457
  const style: React.CSSProperties = {
443
- width: effectiveWidth,
444
- minWidth: colDef.minWidth,
445
- maxWidth: colDef.maxWidth,
446
- flexGrow: colDef.flex ?? 0,
447
- flexShrink: 0,
448
- flexBasis: colDef.flex ? 'auto' : 'auto',
449
- boxSizing: 'border-box',
450
- position: isPinned ? 'sticky' : 'relative',
458
+ ...getHeaderColumnStyle(colDef, columnWidths, pinnedPosition, pinnedPosition ? pinnedPositions[colDef.field] : undefined),
451
459
  zIndex: colDef.zIndex,
452
460
  opacity: isDragging ? 0.5 : 1
453
461
  };
454
462
 
455
- if (isPinned && pinnedPosition) {
456
- const offset = pinnedPositions[colDef.field];
457
- if (pinnedPosition === 'left') {
458
- style.left = offset;
459
- } else {
460
- style.right = offset;
461
- }
462
- }
463
-
464
463
  const headerContent = colDef.renderHeader
465
464
  ? colDef.renderHeader({ field: colDef.field, colDef: colDef as unknown as GridColDef, colIndex })
466
465
  : colDef.headerName || colDef.field;
@@ -565,6 +564,28 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
565
564
  />
566
565
  )}
567
566
  </div>{/* end .ogx__header */}
567
+
568
+ {/* ── Header filter row ── */}
569
+ {headerFilters && (
570
+ <HeaderFilterRow
571
+ columns={columns as unknown as GridColDef[]}
572
+ columnWidths={columnWidths}
573
+ pinnedColumns={pinnedColumns}
574
+ pinnedPositions={pinnedPositions}
575
+ pinnedEdges={pinnedEdges}
576
+ rowReordering={rowReordering}
577
+ hasDetailPanel={hasDetailPanel}
578
+ checkboxSelection={checkboxSelection}
579
+ pinExpandColumn={pinExpandColumn}
580
+ pinCheckboxColumn={pinCheckboxColumn}
581
+ columnIndexMap={columnIndexMap}
582
+ ariaRowIndex={groupDepth + 2}
583
+ filterModel={filterModel}
584
+ onFilterModelChange={onFilterModelChange}
585
+ onOpenFilterPanel={onOpenFilterPanel}
586
+ focusedField={focusedCell?.id === null && focusedCell.headerFilter ? focusedCell.field : null}
587
+ />
588
+ )}
568
589
  </div>
569
590
  );
570
591
  }
@@ -0,0 +1,28 @@
1
+ import type React from 'react';
2
+ import type { GridColDef, GridRowModel } from '../../types';
3
+
4
+ /**
5
+ * Width, flex and sticky offset of a data column's cell in a header row. The column header row
6
+ * and the header filter row both use it, so their cells line up whatever the pinning and widths.
7
+ */
8
+ export function getHeaderColumnStyle<R extends GridRowModel>(
9
+ colDef: GridColDef<R>,
10
+ columnWidths: Record<string, number>,
11
+ pinnedPosition: 'left' | 'right' | null,
12
+ pinnedOffset: number | undefined,
13
+ ): React.CSSProperties {
14
+ const style: React.CSSProperties = {
15
+ width: columnWidths[colDef.field] ?? colDef.width ?? 100,
16
+ minWidth: colDef.minWidth,
17
+ maxWidth: colDef.maxWidth,
18
+ flexGrow: colDef.flex ?? 0,
19
+ flexShrink: 0,
20
+ flexBasis: 'auto',
21
+ boxSizing: 'border-box',
22
+ position: pinnedPosition ? 'sticky' : 'relative',
23
+ };
24
+ if (pinnedPosition === 'left') style.left = pinnedOffset;
25
+ else if (pinnedPosition === 'right') style.right = pinnedOffset;
26
+ return style;
27
+ }
28
+