@cdx-ui/components 0.3.11 → 0.3.13

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 (72) hide show
  1. package/docs/components/data-grid/content.md +966 -15
  2. package/docs/components/list-item/content.md +1 -1
  3. package/docs/components/table/content.md +84 -0
  4. package/docs/foundations/expressions/icons/content.md +1 -1
  5. package/docs/llms.txt +2 -0
  6. package/docs/resources/release-notes/0.3.11/content.md +7 -0
  7. package/docs/resources/release-notes/0.3.12/content.md +15 -0
  8. package/docs/resources/release-notes/content.md +2 -0
  9. package/lib/commonjs/components/CurrencyInput/styles.js +1 -1
  10. package/lib/commonjs/components/DatePicker/index.js +2 -0
  11. package/lib/commonjs/components/DatePicker/index.js.map +1 -1
  12. package/lib/commonjs/components/DateRangePicker/index.js +2 -0
  13. package/lib/commonjs/components/DateRangePicker/index.js.map +1 -1
  14. package/lib/commonjs/components/Icon/index.js +8 -8
  15. package/lib/commonjs/components/Icon/index.js.map +1 -1
  16. package/lib/commonjs/components/ListItem/styles.js +1 -1
  17. package/lib/commonjs/components/ListItem/styles.js.map +1 -1
  18. package/lib/commonjs/components/SystemAlert/styles.js +4 -1
  19. package/lib/commonjs/components/SystemAlert/styles.js.map +1 -1
  20. package/lib/commonjs/components/picker/styles.js +8 -1
  21. package/lib/commonjs/components/picker/styles.js.map +1 -1
  22. package/lib/commonjs/components/picker/surfaces.js +4 -2
  23. package/lib/commonjs/components/picker/surfaces.js.map +1 -1
  24. package/lib/commonjs/figma/CurrencyInput.figma.js +4 -4
  25. package/lib/commonjs/figma/CurrencyInput.figma.js.map +1 -1
  26. package/lib/commonjs/figma/ProgressBar.figma.js +8 -5
  27. package/lib/commonjs/figma/ProgressBar.figma.js.map +1 -1
  28. package/lib/commonjs/figma/ProgressCircle.figma.js +8 -2
  29. package/lib/commonjs/figma/ProgressCircle.figma.js.map +1 -1
  30. package/lib/module/components/CurrencyInput/styles.js +1 -1
  31. package/lib/module/components/DatePicker/index.js +2 -0
  32. package/lib/module/components/DatePicker/index.js.map +1 -1
  33. package/lib/module/components/DateRangePicker/index.js +2 -0
  34. package/lib/module/components/DateRangePicker/index.js.map +1 -1
  35. package/lib/module/components/Icon/index.js +8 -8
  36. package/lib/module/components/Icon/index.js.map +1 -1
  37. package/lib/module/components/ListItem/styles.js +1 -1
  38. package/lib/module/components/ListItem/styles.js.map +1 -1
  39. package/lib/module/components/SystemAlert/styles.js +4 -1
  40. package/lib/module/components/SystemAlert/styles.js.map +1 -1
  41. package/lib/module/components/picker/styles.js +7 -0
  42. package/lib/module/components/picker/styles.js.map +1 -1
  43. package/lib/module/components/picker/surfaces.js +4 -2
  44. package/lib/module/components/picker/surfaces.js.map +1 -1
  45. package/lib/module/figma/CurrencyInput.figma.js +4 -4
  46. package/lib/module/figma/CurrencyInput.figma.js.map +1 -1
  47. package/lib/module/figma/ProgressBar.figma.js +8 -5
  48. package/lib/module/figma/ProgressBar.figma.js.map +1 -1
  49. package/lib/module/figma/ProgressCircle.figma.js +8 -2
  50. package/lib/module/figma/ProgressCircle.figma.js.map +1 -1
  51. package/lib/typescript/components/ColorSwatch/styles.d.ts +1 -1
  52. package/lib/typescript/components/DatePicker/index.d.ts.map +1 -1
  53. package/lib/typescript/components/DateRangePicker/index.d.ts.map +1 -1
  54. package/lib/typescript/components/ListItem/index.d.ts +2 -2
  55. package/lib/typescript/components/SystemAlert/styles.d.ts.map +1 -1
  56. package/lib/typescript/components/picker/styles.d.ts +6 -0
  57. package/lib/typescript/components/picker/styles.d.ts.map +1 -1
  58. package/lib/typescript/components/picker/surfaces.d.ts +2 -1
  59. package/lib/typescript/components/picker/surfaces.d.ts.map +1 -1
  60. package/lib/typescript/figma/CurrencyInput.figma.d.ts.map +1 -1
  61. package/lib/typescript/figma/ProgressBar.figma.d.ts.map +1 -1
  62. package/lib/typescript/figma/ProgressCircle.figma.d.ts.map +1 -1
  63. package/package.json +6 -6
  64. package/src/components/CurrencyInput/styles.ts +1 -1
  65. package/src/components/DatePicker/index.tsx +2 -0
  66. package/src/components/DateRangePicker/index.tsx +2 -0
  67. package/src/components/Icon/index.tsx +8 -8
  68. package/src/components/ListItem/index.tsx +2 -2
  69. package/src/components/ListItem/styles.ts +1 -1
  70. package/src/components/SystemAlert/styles.ts +13 -7
  71. package/src/components/picker/styles.ts +7 -0
  72. package/src/components/picker/surfaces.tsx +3 -0
@@ -15,14 +15,14 @@ import {
15
15
  Text,
16
16
  VStack,
17
17
  } from '@cdx-ui/components';
18
- import { DataGrid } from '@cdx-ui/data-grid';
18
+ import { DataGrid, gridDeltaNumberColDef, gridPolarityNumberColDef } from '@cdx-ui/data-grid';
19
19
  ```
20
20
 
21
21
  ## Basic
22
22
 
23
23
  Pass `rows` and `columns`. This account grid starts with the features most applications need: formatted values, status `Chip` cells, search, header filters, selection, sorting, and pagination.
24
24
 
25
- Search is **not** always on. Set `showToolbar` to show the search field (same opt-in as MUI Community’s toolbar / quick filter). Set `headerFilters` to show per-column filter inputs in a second header row.
25
+ Search and the filter button are **not** always on. Set `showToolbar` to show both (same opt-in as MUI Community’s toolbar). Set `headerFilters` to show per-column filter inputs in a second header row.
26
26
 
27
27
  The grid is a fixed viewport like MUI Data Grid Community: **300px** tall by default. Column headers stay visible while rows scroll; pagination stays at the bottom. Override height with a `className` such as `h-full` or a spacing-scale height (`h-130`).
28
28
 
@@ -65,7 +65,7 @@ const columns: GridColDef<Account>[] = [
65
65
  field: 'balance',
66
66
  headerName: 'Balance',
67
67
  width: 140,
68
- align: 'end',
68
+ type: 'number',
69
69
  valueFormatter: (_, row) => currencyFormatter.format(row.balance),
70
70
  },
71
71
  ];
@@ -87,6 +87,174 @@ export default function DataGridBasicDemo() {
87
87
  }
88
88
  ```
89
89
 
90
+ ## Column types
91
+
92
+ Set `type` on a column for MUI Community defaults: alignment, display formatting, and client sort. `number` right-aligns and formats with the locale (no forced decimals). `date` and `dateTime` expect a `Date` — convert strings with `valueGetter`. `boolean` renders a check or close icon with accessible Yes / No text. `singleSelect` shows the matching `valueOptions` label. Explicit `align`, `headerAlign`, `valueFormatter`, and `renderCell` override the defaults.
93
+
94
+ ```tsx
95
+ import { DataGrid, type GridColDef } from '@cdx-ui/data-grid';
96
+
97
+ // GridValidRowModel's index signature rejects otherwise equivalent interfaces.
98
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
99
+ type Product = {
100
+ id: string;
101
+ name: string;
102
+ price: number;
103
+ listed: Date;
104
+ updated: Date;
105
+ active: boolean;
106
+ status: string;
107
+ };
108
+
109
+ const columns: GridColDef<Product>[] = [
110
+ { field: 'name', headerName: 'Name', type: 'string', flex: 1, minWidth: 160 },
111
+ { field: 'price', headerName: 'Price', type: 'number', width: 120 },
112
+ { field: 'listed', headerName: 'Listed', type: 'date', width: 140 },
113
+ { field: 'updated', headerName: 'Updated', type: 'dateTime', width: 200 },
114
+ { field: 'active', headerName: 'Active', type: 'boolean', width: 100 },
115
+ {
116
+ field: 'status',
117
+ headerName: 'Status',
118
+ type: 'singleSelect',
119
+ width: 140,
120
+ valueOptions: [
121
+ { value: 'open', label: 'Open' },
122
+ { value: 'pending', label: 'Pending review' },
123
+ { value: 'closed', label: 'Closed' },
124
+ ],
125
+ },
126
+ ];
127
+
128
+ const rows: Product[] = [
129
+ {
130
+ id: '1',
131
+ name: 'Operating reserve',
132
+ price: 1234.5,
133
+ listed: new Date(2026, 0, 5),
134
+ updated: new Date(2026, 0, 5, 14, 30, 0),
135
+ active: true,
136
+ status: 'open',
137
+ },
138
+ {
139
+ id: '2',
140
+ name: 'Payroll sweep',
141
+ price: 80,
142
+ listed: new Date(2026, 2, 12),
143
+ updated: new Date(2026, 2, 12, 9, 0, 0),
144
+ active: false,
145
+ status: 'pending',
146
+ },
147
+ {
148
+ id: '3',
149
+ name: 'Merchant hold',
150
+ price: 10450,
151
+ listed: new Date(2025, 10, 2),
152
+ updated: new Date(2025, 11, 18, 16, 45, 0),
153
+ active: true,
154
+ status: 'closed',
155
+ },
156
+ ];
157
+
158
+ export default function DataGridColumnTypesDemo() {
159
+ return (
160
+ <DataGrid
161
+ rows={rows}
162
+ columns={columns}
163
+ hideFooter
164
+ headerFilters
165
+ aria-label="Column types"
166
+ className="h-80"
167
+ />
168
+ );
169
+ }
170
+ ```
171
+
172
+ ## Filters
173
+
174
+ `showToolbar` shows the search field and a filter button. The panel has one row per condition: column, an operator for that column's type, and a value. And or Or on the second row applies to the whole model, not to a single row.
175
+
176
+ Text columns use contains, does not contain, equals, starts with, ends with, is empty, is not empty, and is any of. Number columns use `=`, `!=`, `>`, `>=`, `<`, `<=`, plus the empty and is any of operators. Date columns use is, is not, is after, is on or after, is before, and is on or before. Boolean columns use is, and the value is any, true, or false. `singleSelect` columns use is, is not, and is any of.
177
+
178
+ An item with no `operator` still means contains, which is what each header filter input writes. `isEmpty` and `isNotEmpty` need no value. `isAnyOf` takes a list. Type numbers without grouping (`10450`, not `10,450`). Date and date time values use the date field, and a date time filter matches that calendar day. Pass `filterLogicOperator` beside `filterModel`. In server mode the grid sends the operator and the typed value and does not filter `rows`.
179
+
180
+ ```tsx
181
+ import { DataGrid, type GridColDef } from '@cdx-ui/data-grid';
182
+
183
+ // GridValidRowModel's index signature rejects otherwise equivalent interfaces.
184
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
185
+ type Product = {
186
+ id: string;
187
+ name: string;
188
+ price: number;
189
+ listed: Date;
190
+ updated: Date;
191
+ active: boolean;
192
+ status: string;
193
+ };
194
+
195
+ const columns: GridColDef<Product>[] = [
196
+ { field: 'name', headerName: 'Name', type: 'string', flex: 1, minWidth: 160 },
197
+ { field: 'price', headerName: 'Price', type: 'number', width: 120 },
198
+ { field: 'listed', headerName: 'Listed', type: 'date', width: 140 },
199
+ { field: 'updated', headerName: 'Updated', type: 'dateTime', width: 200 },
200
+ { field: 'active', headerName: 'Active', type: 'boolean', width: 100 },
201
+ {
202
+ field: 'status',
203
+ headerName: 'Status',
204
+ type: 'singleSelect',
205
+ width: 140,
206
+ valueOptions: [
207
+ { value: 'open', label: 'Open' },
208
+ { value: 'pending', label: 'Pending review' },
209
+ { value: 'closed', label: 'Closed' },
210
+ ],
211
+ },
212
+ ];
213
+
214
+ const rows: Product[] = [
215
+ {
216
+ id: '1',
217
+ name: 'Operating reserve',
218
+ price: 1234.5,
219
+ listed: new Date(2026, 0, 5),
220
+ updated: new Date(2026, 0, 5, 14, 30, 0),
221
+ active: true,
222
+ status: 'open',
223
+ },
224
+ {
225
+ id: '2',
226
+ name: 'Payroll sweep',
227
+ price: 80,
228
+ listed: new Date(2026, 2, 12),
229
+ updated: new Date(2026, 2, 12, 9, 0, 0),
230
+ active: false,
231
+ status: 'pending',
232
+ },
233
+ {
234
+ id: '3',
235
+ name: 'Merchant hold',
236
+ price: 10450,
237
+ listed: new Date(2025, 10, 2),
238
+ updated: new Date(2025, 11, 18, 16, 45, 0),
239
+ active: true,
240
+ status: 'closed',
241
+ },
242
+ ];
243
+
244
+ export default function DataGridFiltersDemo() {
245
+ return (
246
+ <DataGrid
247
+ rows={rows}
248
+ columns={columns}
249
+ showToolbar
250
+ hideFooter
251
+ autoHeight
252
+ aria-label="Filters"
253
+ />
254
+ );
255
+ }
256
+ ```
257
+
90
258
  ## Rich cells
91
259
 
92
260
  Use `renderCell` to compose Forge UI components inside a cell. This operations view combines `Avatar`, `Chip`, `ProgressBar`, and an action `Menu`, while `valueFormatter` keeps sortable numeric data separate from its display value.
@@ -163,13 +331,13 @@ const columns: GridColDef<Account>[] = [
163
331
  field: 'balance',
164
332
  headerName: 'Balance',
165
333
  width: 140,
166
- align: 'end',
334
+ type: 'number',
167
335
  valueFormatter: (_, row) => currencyFormatter.format(row.balance),
168
336
  },
169
337
  {
170
338
  field: 'actions',
171
339
  headerName: 'Actions',
172
- width: 72,
340
+ width: 100,
173
341
  align: 'center',
174
342
  headerAlign: 'center',
175
343
  sortable: false,
@@ -262,9 +430,144 @@ export default function DataGridNumericColumnsDemo() {
262
430
  }
263
431
  ```
264
432
 
433
+ ## Polarity and delta numbers
434
+
435
+ `gridPolarityNumberColDef` colors a number by its sign. A positive value uses success, a negative value uses danger and a minus sign (−), and zero stays primary.
436
+
437
+ `gridDeltaNumberColDef` shows a change. The symbol follows the direction of the value, and the color follows `increaseIsGood`: an increase is success when an increase is favorable and danger when it is not, and a decrease does the opposite. Zero stays primary. `symbol` is `arrow` (no icon at zero), `trendline` (including a flat icon at zero), or `sign` (+ or − before the value). Arrow and trendline hide the minus sign.
438
+
439
+ Spread the preset into the column. It sets `type` to `number`, so sorting compares the raw number and empty values sort last. The header filter matches that number's locale text, not the colored cell. A `valueFormatter` passed to the preset, or set on the column after it, formats the visible number. `density` selects body-md or body-sm.
440
+
441
+ Pass `increaseIsGood: false` when a rising number is unfavorable, such as contract terminations. The screen reader hears the direction, for example "Decrease 2.5%".
442
+
443
+ ```tsx
444
+ import {
445
+ DataGrid,
446
+ gridDeltaNumberColDef,
447
+ gridPolarityNumberColDef,
448
+ type GridColDef,
449
+ } from '@cdx-ui/data-grid';
450
+
451
+ // GridValidRowModel's index signature rejects otherwise equivalent interfaces.
452
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
453
+ type PerformanceRow = {
454
+ id: string;
455
+ metric: string;
456
+ amount: number;
457
+ change: number;
458
+ terminations: number;
459
+ };
460
+
461
+ const currency = new Intl.NumberFormat('en-US', {
462
+ style: 'currency',
463
+ currency: 'USD',
464
+ });
465
+
466
+ const points = new Intl.NumberFormat('en-US', {
467
+ minimumFractionDigits: 1,
468
+ maximumFractionDigits: 1,
469
+ });
470
+
471
+ const formatPoints = (value: number) => `${points.format(value)}%`;
472
+
473
+ const rows: PerformanceRow[] = [
474
+ { id: 'interest', metric: 'Interest income', amount: 1280.5, change: 2.5, terminations: -3.1 },
475
+ { id: 'fees', metric: 'Fee income', amount: -640, change: -1.2, terminations: 4.2 },
476
+ { id: 'transfers', metric: 'Transfers', amount: 0, change: 0, terminations: 0 },
477
+ ];
478
+
479
+ const columns: GridColDef<PerformanceRow>[] = [
480
+ { field: 'metric', headerName: 'Metric', flex: 1, minWidth: 160 },
481
+ {
482
+ field: 'amount',
483
+ headerName: 'Amount',
484
+ width: 140,
485
+ ...gridPolarityNumberColDef<PerformanceRow>({
486
+ valueFormatter: (value) => currency.format(value),
487
+ }),
488
+ },
489
+ {
490
+ field: 'change',
491
+ headerName: 'Change',
492
+ width: 120,
493
+ ...gridDeltaNumberColDef<PerformanceRow>({
494
+ increaseIsGood: true,
495
+ symbol: 'arrow',
496
+ valueFormatter: formatPoints,
497
+ }),
498
+ },
499
+ {
500
+ field: 'terminations',
501
+ headerName: 'Terminations',
502
+ width: 150,
503
+ ...gridDeltaNumberColDef<PerformanceRow>({
504
+ increaseIsGood: false,
505
+ symbol: 'arrow',
506
+ valueFormatter: formatPoints,
507
+ }),
508
+ },
509
+ {
510
+ field: 'trend',
511
+ headerName: 'Trend',
512
+ width: 120,
513
+ ...gridDeltaNumberColDef<PerformanceRow>({
514
+ increaseIsGood: true,
515
+ symbol: 'trendline',
516
+ valueFormatter: formatPoints,
517
+ }),
518
+ valueGetter: (_value, row) => row.change,
519
+ },
520
+ {
521
+ field: 'signed',
522
+ headerName: 'Signed change',
523
+ width: 150,
524
+ ...gridDeltaNumberColDef<PerformanceRow>({
525
+ increaseIsGood: true,
526
+ symbol: 'sign',
527
+ valueFormatter: formatPoints,
528
+ }),
529
+ valueGetter: (_value, row) => row.change,
530
+ },
531
+ {
532
+ field: 'terminationsTrend',
533
+ headerName: 'Terminations trend',
534
+ width: 170,
535
+ ...gridDeltaNumberColDef<PerformanceRow>({
536
+ increaseIsGood: false,
537
+ symbol: 'trendline',
538
+ valueFormatter: formatPoints,
539
+ }),
540
+ valueGetter: (_value, row) => row.terminations,
541
+ },
542
+ {
543
+ field: 'terminationsSigned',
544
+ headerName: 'Terminations sign',
545
+ width: 170,
546
+ ...gridDeltaNumberColDef<PerformanceRow>({
547
+ increaseIsGood: false,
548
+ symbol: 'sign',
549
+ valueFormatter: formatPoints,
550
+ }),
551
+ valueGetter: (_value, row) => row.terminations,
552
+ },
553
+ ];
554
+
555
+ export default function DataGridNumberCellsDemo() {
556
+ return (
557
+ <DataGrid
558
+ rows={rows}
559
+ columns={columns}
560
+ hideFooter
561
+ autoHeight
562
+ aria-label="Performance with polarity and delta numbers"
563
+ />
564
+ );
565
+ }
566
+ ```
567
+
265
568
  ## Toolbar actions
266
569
 
267
- `toolbarActions` is an extension slot for your own toolbar controls beside search — export, density, view toggles, and so on. DataGrid does not ship toolbar buttons; you compose them and wire `onPress` to state that drives grid props.
570
+ `toolbarActions` is an extension slot for your own controls beside search and the filter button — export, density, view toggles, and so on. Wire `onPress` to state that drives grid props.
268
571
 
269
572
  A common pattern is a **Filters** button that toggles the `headerFilters` row on demand instead of leaving filter inputs always visible:
270
573
 
@@ -505,13 +808,657 @@ export default function DataGridCheckboxCellsDemo() {
505
808
  }
506
809
  ```
507
810
 
811
+ ## Column visibility
812
+
813
+ Set a field to `false` in `columnVisibilityModel` to hide its column while keeping it in `columns`. A hidden column still sorts, filters, and matches search, and the filter panel still lists it, so a single visible column can show a rich cell while other fields drive the toolbar controls. With `treeData`, the first visible column becomes the tree column. The grid has no built-in control for showing or hiding columns, so keep the model in your own state. The API picker in [Tree data](#tree-data) uses it.
814
+
815
+ ## Tree data
816
+
817
+ Set `treeData` and return each row's path from `getTreeDataPath`, such as `['Statements', '2026', 'Checking 4821 Oct.pdf']`. The first column becomes the tree column: an expand toggle, then the row content, indented by depth. Pass `groupingColDef` to override that column's header, width, or cell. A row attaches to its nearest ancestor that exists in `rows`, so a path does not need a row for every level.
818
+
819
+ Every other column renders, sorts, and filters as it does in a flat grid, so parents and children share the same columns. Leave a value empty where it does not apply to a level, such as a folder's size. Sorting reorders siblings at every level and keeps children under their parent.
820
+
821
+ Groups start collapsed. `defaultGroupingExpansionDepth` expands groups down to a depth (`-1` expands everything), and `isGroupExpandedByDefault` decides per row. While search, header filters, or the filter panel are active, a row stays visible when it or a descendant matches, and every group on the way to a match opens. Clearing the filter restores the groups the user had open.
822
+
823
+ Pagination counts top-level rows, so a page never splits a group. Tree data sorts and filters on the client. It cannot be combined with server pagination, sorting, or filtering.
824
+
825
+ This document library uses `groupingColDef` to draw a file-type icon and each folder's file count in the tree column, and opens the current year's statements with `isGroupExpandedByDefault`.
826
+
827
+ ```tsx
828
+ import { HStack, Icon, type IconColor, Text } from '@cdx-ui/components';
829
+ import {
830
+ DataGrid,
831
+ type DataGridProps,
832
+ type GridColDef,
833
+ type GridTreeNodeParams,
834
+ } from '@cdx-ui/data-grid';
835
+ import { Csv, Description, Folder, type ForgeIcon, PictureAsPdf, TableChart } from '@cdx-ui/icons';
836
+
837
+ type DocumentType = 'Folder' | 'PDF' | 'CSV' | 'Spreadsheet' | 'Document';
838
+
839
+ // GridValidRowModel's index signature rejects otherwise equivalent interfaces.
840
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
841
+ type BankDocument = {
842
+ id: string;
843
+ path: string[];
844
+ type: DocumentType;
845
+ modified: Date;
846
+ size?: number;
847
+ };
848
+
849
+ const typeIcons: Record<DocumentType, { icon: ForgeIcon; color: IconColor }> = {
850
+ Folder: { icon: Folder, color: 'action' },
851
+ PDF: { icon: PictureAsPdf, color: 'danger' },
852
+ CSV: { icon: Csv, color: 'success' },
853
+ Spreadsheet: { icon: TableChart, color: 'success' },
854
+ Document: { icon: Description, color: 'info' },
855
+ };
856
+
857
+ const documents: BankDocument[] = [
858
+ { id: 'statements', path: ['Statements'], type: 'Folder', modified: new Date(2026, 9, 31) },
859
+ {
860
+ id: 'statements-2026',
861
+ path: ['Statements', '2026'],
862
+ type: 'Folder',
863
+ modified: new Date(2026, 9, 31),
864
+ },
865
+ {
866
+ id: 'checking-2026-10',
867
+ path: ['Statements', '2026', 'Checking 4821 Oct.pdf'],
868
+ type: 'PDF',
869
+ modified: new Date(2026, 9, 31),
870
+ size: 184_320,
871
+ },
872
+ {
873
+ id: 'checking-2026-09',
874
+ path: ['Statements', '2026', 'Checking 4821 Sep.pdf'],
875
+ type: 'PDF',
876
+ modified: new Date(2026, 8, 30),
877
+ size: 176_128,
878
+ },
879
+ {
880
+ id: 'savings-2026-q3',
881
+ path: ['Statements', '2026', 'Savings 9310 Q3.pdf'],
882
+ type: 'PDF',
883
+ modified: new Date(2026, 8, 30),
884
+ size: 98_304,
885
+ },
886
+ {
887
+ id: 'statements-2025',
888
+ path: ['Statements', '2025'],
889
+ type: 'Folder',
890
+ modified: new Date(2025, 11, 31),
891
+ },
892
+ {
893
+ id: 'checking-2025-12',
894
+ path: ['Statements', '2025', 'Checking 4821 Dec.pdf'],
895
+ type: 'PDF',
896
+ modified: new Date(2025, 11, 31),
897
+ size: 192_512,
898
+ },
899
+ {
900
+ id: 'savings-2025-q4',
901
+ path: ['Statements', '2025', 'Savings 9310 Q4.pdf'],
902
+ type: 'PDF',
903
+ modified: new Date(2025, 11, 31),
904
+ size: 101_376,
905
+ },
906
+ { id: 'tax', path: ['Tax documents'], type: 'Folder', modified: new Date(2026, 0, 31) },
907
+ {
908
+ id: 'tax-1099-int',
909
+ path: ['Tax documents', '1099-INT 2025.pdf'],
910
+ type: 'PDF',
911
+ modified: new Date(2026, 0, 31),
912
+ size: 64_512,
913
+ },
914
+ {
915
+ id: 'tax-1098',
916
+ path: ['Tax documents', '1098 Mortgage 2025.pdf'],
917
+ type: 'PDF',
918
+ modified: new Date(2026, 0, 29),
919
+ size: 71_680,
920
+ },
921
+ { id: 'loans', path: ['Loans'], type: 'Folder', modified: new Date(2026, 8, 15) },
922
+ {
923
+ id: 'mortgage',
924
+ path: ['Loans', 'Home mortgage 7702'],
925
+ type: 'Folder',
926
+ modified: new Date(2026, 8, 15),
927
+ },
928
+ {
929
+ id: 'mortgage-closing',
930
+ path: ['Loans', 'Home mortgage 7702', 'Closing disclosure.pdf'],
931
+ type: 'PDF',
932
+ modified: new Date(2024, 4, 22),
933
+ size: 1_258_291,
934
+ },
935
+ {
936
+ id: 'mortgage-amortization',
937
+ path: ['Loans', 'Home mortgage 7702', 'Amortization.xlsx'],
938
+ type: 'Spreadsheet',
939
+ modified: new Date(2024, 4, 22),
940
+ size: 48_128,
941
+ },
942
+ {
943
+ id: 'mortgage-escrow',
944
+ path: ['Loans', 'Home mortgage 7702', 'Escrow analysis.pdf'],
945
+ type: 'PDF',
946
+ modified: new Date(2026, 8, 15),
947
+ size: 215_040,
948
+ },
949
+ {
950
+ id: 'auto-loan',
951
+ path: ['Loans', 'Auto loan 3318'],
952
+ type: 'Folder',
953
+ modified: new Date(2026, 6, 2),
954
+ },
955
+ {
956
+ id: 'auto-payoff',
957
+ path: ['Loans', 'Auto loan 3318', 'Payoff quote.pdf'],
958
+ type: 'PDF',
959
+ modified: new Date(2026, 6, 2),
960
+ size: 52_224,
961
+ },
962
+ {
963
+ id: 'transactions-export',
964
+ path: ['Transactions Oct 2026.csv'],
965
+ type: 'CSV',
966
+ modified: new Date(2026, 10, 2),
967
+ size: 23_552,
968
+ },
969
+ {
970
+ id: 'direct-deposit',
971
+ path: ['Direct deposit form.pdf'],
972
+ type: 'PDF',
973
+ modified: new Date(2026, 2, 9),
974
+ size: 88_064,
975
+ },
976
+ {
977
+ id: 'beneficiary',
978
+ path: ['Beneficiaries.docx'],
979
+ type: 'Document',
980
+ modified: new Date(2025, 10, 18),
981
+ size: 36_864,
982
+ },
983
+ ];
984
+
985
+ const fileCountOf = new Map(
986
+ documents
987
+ .filter((folder) => folder.type === 'Folder')
988
+ .map((folder) => [
989
+ folder.id,
990
+ documents.filter(
991
+ (doc) =>
992
+ doc.type !== 'Folder' &&
993
+ folder.path.every((segment, index) => doc.path[index] === segment),
994
+ ).length,
995
+ ]),
996
+ );
997
+
998
+ const sizeFormatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 });
999
+
1000
+ function formatSize(bytes: number) {
1001
+ if (bytes < 1024) {
1002
+ return `${sizeFormatter.format(bytes)} B`;
1003
+ }
1004
+ if (bytes < 1024 * 1024) {
1005
+ return `${sizeFormatter.format(bytes / 1024)} KB`;
1006
+ }
1007
+ return `${sizeFormatter.format(bytes / (1024 * 1024))} MB`;
1008
+ }
1009
+
1010
+ const columns: GridColDef<BankDocument>[] = [
1011
+ { field: 'name', headerName: 'Name', valueGetter: (_, row) => row.path.at(-1) },
1012
+ { field: 'type', headerName: 'Type', width: 120 },
1013
+ { field: 'modified', headerName: 'Modified', type: 'date', width: 130 },
1014
+ {
1015
+ field: 'size',
1016
+ headerName: 'Size',
1017
+ type: 'number',
1018
+ width: 100,
1019
+ valueFormatter: (value) => (typeof value === 'number' ? formatSize(value) : ''),
1020
+ },
1021
+ ];
1022
+
1023
+ const groupingColDef: DataGridProps<BankDocument>['groupingColDef'] = {
1024
+ flex: 1,
1025
+ minWidth: 240,
1026
+ renderCell: ({ row }) => {
1027
+ const { icon, color } = typeIcons[row.type];
1028
+ const isFolder = row.type === 'Folder';
1029
+ return (
1030
+ <HStack space="sm" className="min-w-0 flex-1 items-center">
1031
+ <Icon as={icon} color={color} />
1032
+ <Text numberOfLines={1} color={isFolder ? 'action' : 'primary'}>
1033
+ {isFolder
1034
+ ? `${String(row.path.at(-1))} (${String(fileCountOf.get(row.id) ?? 0)})`
1035
+ : row.path.at(-1)}
1036
+ </Text>
1037
+ </HStack>
1038
+ );
1039
+ },
1040
+ };
1041
+
1042
+ const getTreeDataPath = (row: BankDocument) => row.path;
1043
+ const isGroupExpandedByDefault = ({ id }: GridTreeNodeParams<BankDocument>) =>
1044
+ id === 'statements' || id === 'statements-2026';
1045
+
1046
+ export default function DataGridTreeDataColumnsDemo() {
1047
+ return (
1048
+ <DataGrid
1049
+ rows={documents}
1050
+ columns={columns}
1051
+ treeData
1052
+ getTreeDataPath={getTreeDataPath}
1053
+ groupingColDef={groupingColDef}
1054
+ isGroupExpandedByDefault={isGroupExpandedByDefault}
1055
+ aria-label="Documents"
1056
+ className="h-130"
1057
+ />
1058
+ );
1059
+ }
1060
+ ```
1061
+
1062
+ With `checkboxSelection`, the checkbox sits in the tree column. By default, selecting a group selects its descendants, and a group is checked when all of its children are and shows a mixed state when only some are. Set `rowSelectionPropagation` to `{ descendants: false, parents: false }` to select rows independently. Selection is kept for rows that a filter hides, unlike MUI X, and select-all applies to the rows that match the filter.
1063
+
1064
+ This picker shows each API as one rich row. `description` and `method` are hidden with `columnVisibilityModel` (see [Column visibility](#column-visibility)), so search also matches descriptions. The filter panel still lists the hidden `method` column, so filtering by method uses the panel that already exists.
1065
+
1066
+ ```tsx
1067
+ import { useState } from 'react';
1068
+ import { Chip, HStack, Text, VStack } from '@cdx-ui/components';
1069
+ import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@cdx-ui/data-grid';
1070
+
1071
+ type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
1072
+
1073
+ // GridValidRowModel's index signature rejects otherwise equivalent interfaces.
1074
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
1075
+ type Api = {
1076
+ id: string;
1077
+ path: string[];
1078
+ description: string;
1079
+ method?: Method;
1080
+ };
1081
+
1082
+ const methodColors: Record<Method, 'info' | 'success' | 'warning' | 'danger'> = {
1083
+ GET: 'info',
1084
+ POST: 'success',
1085
+ PUT: 'warning',
1086
+ DELETE: 'danger',
1087
+ };
1088
+
1089
+ const apis: Api[] = [
1090
+ { id: 'accounts', path: ['Accounts'], description: 'Account information and balances' },
1091
+ { id: 'oauth', path: ['Accounts', 'OAuth V1'], description: 'Token-scoped account APIs' },
1092
+ {
1093
+ id: 'list-accounts',
1094
+ path: ['Accounts', 'OAuth V1', 'List customer accounts'],
1095
+ description: 'Returns every account the customer can see',
1096
+ method: 'GET',
1097
+ },
1098
+ {
1099
+ id: 'close-account',
1100
+ path: ['Accounts', 'OAuth V1', 'Close account'],
1101
+ description: 'Closes an account with a zero balance',
1102
+ method: 'DELETE',
1103
+ },
1104
+ {
1105
+ id: 'open-account',
1106
+ path: ['Accounts', 'OAuth V1', 'Open account'],
1107
+ description: 'Opens a new deposit account',
1108
+ method: 'POST',
1109
+ },
1110
+ { id: 'balances', path: ['Accounts', 'Balances'], description: 'Real-time balance APIs' },
1111
+ {
1112
+ id: 'get-balance',
1113
+ path: ['Accounts', 'Balances', 'Get balance'],
1114
+ description: 'Returns the available and ledger balance',
1115
+ method: 'GET',
1116
+ },
1117
+ { id: 'payments', path: ['Payments'], description: 'Transfers and bill pay' },
1118
+ {
1119
+ id: 'create-transfer',
1120
+ path: ['Payments', 'Create transfer'],
1121
+ description: 'Moves money between two accounts',
1122
+ method: 'POST',
1123
+ },
1124
+ {
1125
+ id: 'update-payee',
1126
+ path: ['Payments', 'Update payee'],
1127
+ description: 'Changes a bill pay payee',
1128
+ method: 'PUT',
1129
+ },
1130
+ ];
1131
+
1132
+ const columns: GridColDef<Api>[] = [
1133
+ {
1134
+ field: 'name',
1135
+ headerName: 'APIs',
1136
+ flex: 1,
1137
+ minWidth: 280,
1138
+ valueGetter: (_, row) => row.path.at(-1),
1139
+ renderCell: ({ row }) => (
1140
+ <HStack space="sm" className="flex-1 items-center">
1141
+ <VStack className="min-w-0 flex-1">
1142
+ <Text numberOfLines={1} weight="medium">
1143
+ {row.path.at(-1)}
1144
+ </Text>
1145
+ <Text numberOfLines={1} color="secondary" size="sm">
1146
+ {row.description}
1147
+ </Text>
1148
+ </VStack>
1149
+ {row.method ? (
1150
+ <Chip color={methodColors[row.method]} size="small">
1151
+ <Chip.Label>{row.method}</Chip.Label>
1152
+ </Chip>
1153
+ ) : null}
1154
+ </HStack>
1155
+ ),
1156
+ },
1157
+ { field: 'description', headerName: 'Description' },
1158
+ { field: 'method', headerName: 'Method' },
1159
+ ];
1160
+
1161
+ const columnVisibilityModel = { description: false, method: false };
1162
+ const endpointCount = apis.filter((api) => api.method).length;
1163
+
1164
+ export default function DataGridTreeDataSelectionDemo() {
1165
+ const [selection, setSelection] = useState<GridRowSelectionModel>({
1166
+ type: 'include',
1167
+ ids: new Set(['list-accounts', 'create-transfer']),
1168
+ });
1169
+ const selectedEndpoints = apis.filter((api) => api.method && selection.ids.has(api.id)).length;
1170
+
1171
+ return (
1172
+ <DataGrid
1173
+ rows={apis}
1174
+ columns={columns}
1175
+ columnVisibilityModel={columnVisibilityModel}
1176
+ treeData
1177
+ getTreeDataPath={(row) => row.path}
1178
+ defaultGroupingExpansionDepth={1}
1179
+ checkboxSelection
1180
+ rowSelectionModel={selection}
1181
+ onRowSelectionModelChange={setSelection}
1182
+ showToolbar
1183
+ toolbarActions={
1184
+ <Text color="secondary" size="sm">
1185
+ {`${selectedEndpoints.toString()} of ${endpointCount.toString()} endpoints selected`}
1186
+ </Text>
1187
+ }
1188
+ hideFooter
1189
+ aria-label="APIs"
1190
+ className="h-130"
1191
+ />
1192
+ );
1193
+ }
1194
+ ```
1195
+
1196
+ ## Detail panel
1197
+
1198
+ Return content from `getDetailPanelContent` to add a toggle column at the start of the grid. Opening a row shows a panel under it that spans every column. Return `null` for rows that have no details, and those rows get no toggle. Use `defaultDetailPanelExpandedRowIds`, or control `detailPanelExpandedRowIds` with `onDetailPanelExpandedRowIdsChange`, to choose which panels are open. The panel grows to fit its content.
1199
+
1200
+ The panel scrolls with the grid when it is wider than the viewport, so keep its content narrow enough to read without horizontal scrolling. When any column is pinned to start, the toggle column pins after the selection column.
1201
+
1202
+ ```tsx
1203
+ import { Button, Chip, HStack, Text, VStack } from '@cdx-ui/components';
1204
+ import { DataGrid, type GridColDef } from '@cdx-ui/data-grid';
1205
+ import { Check, Close, ContentCopy } from '@cdx-ui/icons';
1206
+
1207
+ // GridValidRowModel's index signature rejects otherwise equivalent interfaces.
1208
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
1209
+ type AuditEvent = {
1210
+ id: string;
1211
+ time: string;
1212
+ user: string;
1213
+ category: 'Access' | 'Change';
1214
+ domain: string;
1215
+ eventType: string;
1216
+ status: 'Success' | 'Failed';
1217
+ details: [label: string, value: string][];
1218
+ };
1219
+
1220
+ const events: AuditEvent[] = [
1221
+ {
1222
+ id: 'evt_8f2a6c91',
1223
+ time: 'Jan 12, 2026, 11:51:06 AM',
1224
+ user: 'Mary Jane',
1225
+ category: 'Change',
1226
+ domain: 'Track and publish',
1227
+ eventType: 'Login',
1228
+ status: 'Failed',
1229
+ details: [
1230
+ ['Event ID', 'evt_8f2a6c91'],
1231
+ ['Organisation ID', 'org_acme'],
1232
+ ['Category', 'change'],
1233
+ ['Environment ID', 'env_production'],
1234
+ ['Occurred at', '2026-01-12T11:51:06Z'],
1235
+ ['Target collection', 'articles'],
1236
+ ['Actor type', 'user'],
1237
+ ['Version before', '12'],
1238
+ ['User ID', 'usr_10482'],
1239
+ ['Version after', '13'],
1240
+ ['User email', 'mary.jane@example.com'],
1241
+ ['Patch path', '/title'],
1242
+ ['Correlation ID', 'req_7d4b9e23'],
1243
+ ['HTTP status code', '403'],
1244
+ ],
1245
+ },
1246
+ {
1247
+ id: 'evt_51ab07e4',
1248
+ time: 'Jan 12, 2026, 10:05:41 AM',
1249
+ user: 'Peter Parker',
1250
+ category: 'Access',
1251
+ domain: 'Branding and theming',
1252
+ eventType: 'History',
1253
+ status: 'Success',
1254
+ details: [
1255
+ ['Event ID', 'evt_51ab07e4'],
1256
+ ['Organisation ID', 'org_acme'],
1257
+ ['Category', 'access'],
1258
+ ['Environment ID', 'env_production'],
1259
+ ['User ID', 'usr_20311'],
1260
+ ['HTTP status code', '200'],
1261
+ ],
1262
+ },
1263
+ {
1264
+ id: 'evt_c3d94f10',
1265
+ time: 'Jan 11, 2026, 4:18:22 PM',
1266
+ user: 'Jane Cooper',
1267
+ category: 'Access',
1268
+ domain: 'Track and publish',
1269
+ eventType: 'Login',
1270
+ status: 'Success',
1271
+ details: [
1272
+ ['Event ID', 'evt_c3d94f10'],
1273
+ ['Organisation ID', 'org_acme'],
1274
+ ['User ID', 'usr_10077'],
1275
+ ['HTTP status code', '200'],
1276
+ ],
1277
+ },
1278
+ ];
1279
+
1280
+ const columns: GridColDef<AuditEvent>[] = [
1281
+ { field: 'time', headerName: 'Time', width: 220 },
1282
+ { field: 'user', headerName: 'User', width: 140 },
1283
+ {
1284
+ field: 'category',
1285
+ headerName: 'Category',
1286
+ width: 120,
1287
+ renderCell: ({ row }) => (
1288
+ <Chip size="small" color={row.category === 'Access' ? 'info' : 'neutral'}>
1289
+ <Chip.Label>{row.category}</Chip.Label>
1290
+ </Chip>
1291
+ ),
1292
+ },
1293
+ { field: 'domain', headerName: 'Domain', flex: 1, minWidth: 180 },
1294
+ { field: 'eventType', headerName: 'Event type', width: 130 },
1295
+ {
1296
+ field: 'status',
1297
+ headerName: 'Status',
1298
+ width: 120,
1299
+ renderCell: ({ row }) => (
1300
+ <Chip size="small" color={row.status === 'Success' ? 'success' : 'danger'}>
1301
+ <Chip.Icon as={row.status === 'Success' ? Check : Close} />
1302
+ <Chip.Label>{row.status}</Chip.Label>
1303
+ </Chip>
1304
+ ),
1305
+ },
1306
+ ];
1307
+
1308
+ function AuditEventDetails({ event }: { event: AuditEvent }) {
1309
+ return (
1310
+ <VStack space="md" className="rounded-lg bg-surface-secondary p-4">
1311
+ <HStack className="flex-wrap">
1312
+ {event.details.map(([label, value]) => (
1313
+ <HStack key={label} space="md" className="w-1/2 py-1">
1314
+ <Text color="secondary" size="sm" className="w-40">
1315
+ {label}:
1316
+ </Text>
1317
+ <Text size="sm" className="flex-1">
1318
+ {value}
1319
+ </Text>
1320
+ </HStack>
1321
+ ))}
1322
+ </HStack>
1323
+ <Button
1324
+ variant="outline"
1325
+ size="small"
1326
+ className="self-end"
1327
+ onPress={() => {
1328
+ void navigator.clipboard.writeText(
1329
+ JSON.stringify(Object.fromEntries(event.details), null, 2),
1330
+ );
1331
+ }}
1332
+ >
1333
+ <Button.Icon as={ContentCopy} position="leading" />
1334
+ <Button.Label>Copy</Button.Label>
1335
+ </Button>
1336
+ </VStack>
1337
+ );
1338
+ }
1339
+
1340
+ export default function DataGridDetailPanelDemo() {
1341
+ return (
1342
+ <DataGrid
1343
+ rows={events}
1344
+ columns={columns}
1345
+ getDetailPanelContent={({ row }) => <AuditEventDetails event={row} />}
1346
+ defaultDetailPanelExpandedRowIds={new Set(['evt_8f2a6c91'])}
1347
+ hideFooter
1348
+ aria-label="Audit log"
1349
+ className="h-130"
1350
+ />
1351
+ );
1352
+ }
1353
+ ```
1354
+
1355
+ ## Column pinning
1356
+
1357
+ Pass `pinnedColumns` with `start` and/or `end` field lists so key columns stay visible while the grid scrolls horizontally. Deprecated `left` / `right` keys are aliases for `start` / `end` (same mapping as `align`). MUI X `initialState.pinnedColumns` moves to the `pinnedColumns` prop.
1358
+
1359
+ Pinned columns use a fixed pixel `width` (default 100) and ignore `flex`. When `checkboxSelection` is on and any column is pinned to start, the selection column pins first at the start edge. The MUI X checkbox field `__check__` is accepted as an alias for that column.
1360
+
1361
+ ```tsx
1362
+ import { Chip, IconButton, Menu } from '@cdx-ui/components';
1363
+ import { DataGrid, type GridColDef } from '@cdx-ui/data-grid';
1364
+ import { MoreHoriz } from '@cdx-ui/icons';
1365
+ import { accounts, type Account, type AccountStatus } from './_data';
1366
+
1367
+ const currencyFormatter = new Intl.NumberFormat('en-US', {
1368
+ style: 'currency',
1369
+ currency: 'USD',
1370
+ });
1371
+
1372
+ const statusColors: Record<AccountStatus, 'success' | 'warning' | 'info' | 'neutral'> = {
1373
+ Active: 'success',
1374
+ Pending: 'warning',
1375
+ Frozen: 'info',
1376
+ Closed: 'neutral',
1377
+ };
1378
+
1379
+ const columns: GridColDef<Account>[] = [
1380
+ { field: 'id', headerName: 'Account', width: 120 },
1381
+ { field: 'name', headerName: 'Name', width: 180 },
1382
+ { field: 'type', headerName: 'Type', width: 140 },
1383
+ {
1384
+ field: 'status',
1385
+ headerName: 'Status',
1386
+ width: 120,
1387
+ renderCell: ({ row }) => (
1388
+ <Chip color={statusColors[row.status]} size="small">
1389
+ <Chip.Label>{row.status}</Chip.Label>
1390
+ </Chip>
1391
+ ),
1392
+ },
1393
+ { field: 'owner', headerName: 'Owner', width: 160 },
1394
+ {
1395
+ field: 'balance',
1396
+ headerName: 'Balance',
1397
+ width: 140,
1398
+ align: 'end',
1399
+ valueFormatter: (_, row) => currencyFormatter.format(row.balance),
1400
+ },
1401
+ {
1402
+ field: 'automation',
1403
+ headerName: 'Automated',
1404
+ width: 140,
1405
+ valueFormatter: (_, row) => `${row.automation.toString()}%`,
1406
+ },
1407
+ {
1408
+ field: 'actions',
1409
+ headerName: 'Actions',
1410
+ width: 100,
1411
+ align: 'center',
1412
+ headerAlign: 'center',
1413
+ sortable: false,
1414
+ filterable: false,
1415
+ renderCell: ({ row }) => (
1416
+ <Menu placement="bottom-end">
1417
+ <Menu.Trigger asChild>
1418
+ <IconButton
1419
+ as={MoreHoriz}
1420
+ variant="ghost"
1421
+ size="small"
1422
+ accessibilityLabel={`Actions for ${row.name}`}
1423
+ />
1424
+ </Menu.Trigger>
1425
+ <Menu.Popup>
1426
+ <Menu.Content accessibilityLabel={`${row.name} actions`}>
1427
+ <Menu.Item>
1428
+ <Menu.ItemLabel>View account</Menu.ItemLabel>
1429
+ </Menu.Item>
1430
+ <Menu.Item>
1431
+ <Menu.ItemLabel>Assign owner</Menu.ItemLabel>
1432
+ </Menu.Item>
1433
+ </Menu.Content>
1434
+ </Menu.Popup>
1435
+ </Menu>
1436
+ ),
1437
+ },
1438
+ ];
1439
+
1440
+ export default function DataGridColumnPinningDemo() {
1441
+ return (
1442
+ <DataGrid
1443
+ rows={accounts}
1444
+ columns={columns}
1445
+ pinnedColumns={{ start: ['id'], end: ['actions'] }}
1446
+ aria-label="Accounts with pinned columns"
1447
+ defaultPaginationModel={{ page: 0, pageSize: 5 }}
1448
+ pageSizeOptions={[5, 10, 25]}
1449
+ className="h-105"
1450
+ />
1451
+ );
1452
+ }
1453
+ ```
1454
+
508
1455
  ## Server data
509
1456
 
510
1457
  Set `paginationMode="server"` and `sortingMode="server"` to let your data source own those operations. `filterMode` follows `paginationMode` unless you set it, so server pagination also means server filtering. Keep the models controlled, pass the current page in `rows`, and provide the full `rowCount` of matching rows.
511
1458
 
512
- With server filtering, search and header filters no longer filter `rows`. Send `quickFilter` and `filterModel` to your data source instead. Both change on every keystroke, so debounce the request and return to page 0 when the debounced query changes, as this demo does. Passing `filterMode="client"` with server pagination filters only the current page of `rows`.
1459
+ With server filtering, search, header filters, and the filter panel no longer filter `rows`. Send `quickFilter`, `filterModel`, and `filterLogicOperator` to your data source instead. Search and header filters change on every keystroke, so debounce the request and return to page 0 when the debounced query changes, as this demo does. Passing `filterMode="client"` with server pagination filters only the current page of `rows`.
513
1460
 
514
- `GridFilterItem` will gain an optional `operator` field, defaulting to `'contains'`, in a future minor release. Existing `{ field, value }` items keep working, so server handlers should treat a missing `operator` as `'contains'`.
1461
+ A missing `operator` still means contains, so existing `{ field, value }` items keep working. When `operator` is set, `value` is typed: a string, a number, a `Date`, a boolean, a `singleSelect` value, or an array for `isAnyOf`. `filterLogicOperator` is a separate prop and defaults to `and`.
515
1462
 
516
1463
  ```tsx
517
1464
  import { useEffect, useMemo, useState } from 'react';
@@ -702,7 +1649,7 @@ export default function DataGridEmptyDemo() {
702
1649
 
703
1650
  ### DataGrid
704
1651
 
705
- Web-only. Inspired by MUI Data Grid Community props; not a wrap of `@mui/x-data-grid`.
1652
+ Web-only. MUI X–shaped `rows` / `columns` API; not a wrap of `@mui/x-data-grid`.
706
1653
 
707
1654
  | Attribute | Values | Description |
708
1655
  | ----------- | ----------- | ----------- |
@@ -712,25 +1659,29 @@ Web-only. Inspired by MUI Data Grid Community props; not a wrap of `@mui/x-data-
712
1659
 
713
1660
  Column definition. `valueGetter`, `valueFormatter`, and `renderCell` take no `apiRef`.
714
1661
  Use `start`, `center`, or `end` for `align` and `headerAlign`; the legacy `left` and `right`
715
- values are deprecated aliases.
1662
+ values are deprecated aliases. `type` selects built-in alignment, formatting, and client
1663
+ sort. `valueOptions` is required when `type` is `singleSelect`.
716
1664
 
717
1665
  | Prop | Type | Default | Description |
718
1666
  | --- | --- | --- | --- |
719
- | `field` | `string` | — | — |
1667
+ | `field?` | `string` | — | — |
720
1668
  | `headerName?` | `string` | — | — |
721
1669
  | `width?` | `number` | — | — |
722
1670
  | `minWidth?` | `number` | — | — |
723
1671
  | `flex?` | `number` | — | — |
724
- | `align?` | `'start' \| 'center' \| 'end' \| 'left' \| 'right'` | `'start'` | Logical cell-content alignment. `left` and `right` are deprecated compatibility aliases for `start` and `end`. |
725
- | `headerAlign?` | `'start' \| 'center' \| 'end' \| 'left' \| 'right'` | — | Logical header-content alignment. Defaults to `align`. `left` and `right` are deprecated compatibility aliases for `start` and `end`. |
1672
+ | `align?` | `'start' \| 'center' \| 'end' \| 'left' \| 'right'` | — | — |
1673
+ | `headerAlign?` | `'start' \| 'center' \| 'end' \| 'left' \| 'right'` | — | — |
726
1674
  | `sortable?` | `boolean` | — | — |
727
1675
  | `filterable?` | `boolean` | — | — |
728
- | `type?` | `'string'` | — | — |
1676
+ | `type?` | `'string' \| 'number' \| 'boolean' \| 'date' \| 'dateTime' \| 'singleSelect'` | — | — |
729
1677
  | `valueGetter?` | `((value: unknown, row: GridValidRowModel, col: GridColDef<GridValidRowModel>) => unknown)` | — | — |
730
1678
  | `valueFormatter?` | `((value: unknown, row: GridValidRowModel, col: GridColDef<GridValidRowModel>) => ReactNode \| string)` | — | — |
731
1679
  | `renderCell?` | `((params: GridRenderCellParams<GridValidRowModel>) => ReactNode)` | — | — |
732
1680
  | `renderHeader?` | `(() => ReactNode)` | — | — |
1681
+ | `valueOptions?` | `readonly GridValueOption[] \| ((params: { row?: GridValidRowModel; }) => readonly GridValueOption[])` | — | — |
733
1682
 
734
1683
  ## Accessibility
735
1684
 
736
- The grid renders a semantic HTML table on web. Sortable headers expose `aria-sort`. Select-all and row checkboxes are labelled.
1685
+ The grid renders a semantic HTML table on web. Sortable headers expose `aria-sort`. Select-all and row checkboxes are labelled.
1686
+
1687
+ Tree and detail panel toggles are buttons with `aria-expanded`, labelled with the row they open, such as "Expand OAuth V1, Accounts". A partly selected tree group exposes `aria-checked="mixed"`. Expanding or collapsing never moves focus. When a filter removes the row that had focus, focus moves to the toolbar search field if `showToolbar` is on. Closing the filter panel with Escape, Remove all, or by removing the last filter returns focus to the control that opened it. A press outside the panel closes it and leaves focus where it went. The grid keeps table semantics rather than `treegrid`, so screen readers do not announce the tree level; the toggle label names the row's ancestors instead.