[DataGridPro] Add new multiSelect column type#21157
Merged
Merged
Conversation
- Add GridMultiSelectColDef interface with valueOptions, getOptionLabel, getOptionValue, separator - Register multiSelect in column types with default sortComparator (by array length), valueFormatter (join with separator), and pastedValueParser - Add multiSelectCell* CSS classes - Add isMultiSelectColDef helper - Add placeholder for header filter input
- Add GridMultiSelectCell with chip rendering and +N overflow - Wire renderCell in GRID_MULTI_SELECT_COL_DEF - Add cell display tests
- Add GridEditMultiSelectCell for editing with Autocomplete - Add GridFilterInputMultiSelect for filter panel - Add demo and documentation - Add tests for filtering, editing, quick filter - Update API docs and exports
- Add tests for overflow chip auto-focus on cell focus - Add tests for popup open/close via Space and Escape - Add openMultiSelectPopup helper in test/utils/helperFn.ts
…iSelect cell Performance improvement: removed per-cell ResizeObserver. - Measure chips and container width once on mount - React to colDef.computedWidth changes via delta calculation - Use useMemo for visible count calculation from cached measurements
- "+9" (1 digit) = 32px - "+99" (2 digits) = 38px - "+999" (3+ digits) = 44px Also update test comment to reflect actual skip reason (DOM layout, not ResizeObserver)
Skip useEffect reset on initial mount to prevent race condition with useLayoutEffect measurement
Custom popper slot makes disablePortal unnecessary. Updated docs to reflect custom popper approach.
- Add optional container param to getColumnValues helper - Use setProps instead of user.type to avoid debounce race conditions - Scope assertions to test container for parallel test isolation
- Render all chips in DOM, use CSS class to hide overflow chips - Show hidden chips during autosizing for proper width calculation - Extract calculateVisibleCount to utils with tests - Fix subpixel tolerance bug (1px per chip)
Contributor
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
|
Deploy preview: https://deploy-preview-21157--material-ui-x.netlify.app/ Updated pages:
Bundle size report
|
…i/mui-x into feat/multiselect
Base hydrated colDef on processed column (keeps dimensions, hasBeenResized, computed props), overlay multiSelect defaults only where user didn't set, skip dimension keys. Fixes resized width lost after repeated hydration.
arminmeh
reviewed
May 26, 2026
arminmeh
reviewed
May 26, 2026
Measure +N overflow chip widths/gap once per grid via a single GridMultiSelectMeasurer in <GridRoot>, instead of per-column via a renderHeader override. Metrics are grid-global (identical across columns). Removes the renderHeader override, fixing the default header rendering.
Recompute the +N overflow on committed column-width changes (resize stop), matching how other cells reflow, instead of subscribing each cell to a throttled drag broadcaster. Removes the per-grid columnResize listener, the cache drag pub/sub, and the chip-level containerWidth drag tracking.
Register multiSelect in the core column-type registry (read at call time) so core hydration applies its colDef defaults natively, in Pro and Premium only. A community DataGrid never loads the registration; a prop validator warns in dev if a multiSelect column is used there. Deletes useGridMultiSelectPreProcessors; the measurer now owns the overflow-metrics cache.
While a multiSelect column is being drag-resized, render every chip and let the cell clip them (live preview via the column width CSS variable). The +N overflow is recomputed once the resize is committed. Driven by a single resizing-field selector — no per-tick work or width broadcasting.
A quick double-click to autosize toggles the resizing state in short bursts; revealing every chip immediately flashed them. Debounce the reveal by 150ms so only a held drag reveals, while a transient click/double-click clears first.
…iltering tests - pastedValueParser now splits on the column's configured separator too, so a custom separator round-trips through copy/paste. - Move multiSelect rendering/overflow/resize tests out of the filtering test file into a dedicated multiSelect.DataGridPro.test.tsx.
multiSelect is registered in the shared core column-type registry, so importing a Pro/Premium grid made it resolvable in a community DataGrid too (same bundle). Gate column resolution on the grid signature: a community DataGrid falls back to the default column type for any non-community-registered type, even when it is registered globally. Drop the duplicate prop validator (checkMultiSelectColumns already warns).
cherniavskii
approved these changes
May 27, 2026
cherniavskii
left a comment
Member
There was a problem hiding this comment.
Sorry for taking so long to review, and thank you for your patience and follow-ups!
Comment on lines
+48
to
+50
| // Delay before revealing all chips on resize start, so a quick double-click (autosize) — which | ||
| // toggles the resizing state in short bursts — doesn't flash every chip. | ||
| const RESIZE_REVEAL_DELAY_MS = 150; |
MBilalShafi
approved these changes
May 30, 2026
MBilalShafi
left a comment
Member
There was a problem hiding this comment.
Nice work 🙌
Overall looks good to me, just a side question, do you plan to ship with the current keyboard navigation experience or possibly align it with Data Grid modals (filter, columns management, etc.)?
(Ref #21157 (comment))
- Register edit multiSelect cell utility classes in gridClasses - @ignore internal --hidden chip classes (view + edit) - Fix sortComparator element-boundary collision (join with separator) - Dedupe pasted multiSelect values
useGridProps update effect dropped signature, wiping state.props.signature to undefined after any watched-prop change. Community DataGrid then stopped falling back and resolved the globally-registered Pro multiSelect renderer. Add regression test.
siriwatknp
enabled auto-merge (squash)
June 8, 2026 07:41
siriwatknp
disabled auto-merge
June 8, 2026 07:58
mbrookes
pushed a commit
to mbrookes/mui-x
that referenced
this pull request
Jun 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
closes #4410
Summary
Adds a new
multiSelectcolumn type to DataGridPro that stores array values and displays them as chips. This is a Pro feature.Usage
Features
Ctrl/Cmd+Enterto commit,Escapeto cancelcontains/doesNotContainoperators with multi-valuebaseSelect(multiple) input.contains [A, B]matches rows containing A or B (OR semantics);doesNotContain [A, B]matches rows containing neither. PlusisEmpty/isNotEmptySelect(not Autocomplete) to avoid layout overflow issues with chips in the header filter rowlocaleCompare. Empty arrays sort to the start (ascending)valueOptionson pasteflexWrapwhen row has dynamic heightrowSpanValueGetterthat serializes arrays to sorted comma-joined strings for correct value comparison (arrays compare by reference, not value)slotProps.chipaccepts a function(option, index) => ChipPropsfor per-value customization (colors, avatars, variants)Spacetoggles overflow popup,Escapecloses it, overflow chip auto-focuses when cell is focusedgroupingValueGetterserializes the array to a sorted comma-joined key so rows sharing the same set of values cluster into one group regardless of order. Override with an explicitgroupingValueGetterto customize (e.g. order-sensitive grouping). Empty arrays returnnulland remain ungrouped at rootsizeis exposed (counts rows with non-empty array). Numeric aggregations are filtered out because they have no unambiguous meaning over arrays. Footer aggregation cell renders viaGridFooterCellto match other column types; grouped-row aggregation rendersformattedValue+Nchip recomputes in real time while the user drags the column separator (throttled, with asymmetric hysteresis to prevent flicker at chip-fit boundaries) — not only on drag-stop. Drag-tick width is read from thecolumnResizeevent (params.width), so cells make no DOM reads per tick; sub-pixelgetBoundingClientRectruns on commit onlyConfiguration
valueOptionsArray<string | number | { value, label }>or(params) => ArraygetOptionLabel(option) => stringgetOptionValue(option) => string | numberseparatorstring(default:',')Customization
Chips can be customized per-value via
slotProps.chip, which supports a callback signature(option, index) => ChipProps. This lets you assign different colors, avatars, or variants based on the option value:See the Custom multi-select chips recipe demo for a full example.
For Reviewers
Architecture: Pro tier via preprocessor
The column type is registered in
x-data-grid-provia auseGridMultiSelectPreProcessorshook that plugs into thehydrateColumnspipe processor. This applies multiSelect defaults (renderers, operators, sort comparator) to any column withtype: 'multiSelect'. The preprocessor uses the user's original column props (not the already-merged state) to avoid string-type defaults leaking in. It also respectshasBeenResizedto preserve manual column widths after resize.Types (
GridMultiSelectColDef) remain inx-data-gridfor type safety — consumers define columns against the community types even when using Pro features.Cell display: measure-once + live resize
GridMultiSelectCellrenders all chips in the DOM but hides overflow chips via CSS class (multiSelectCellChip--hidden). On mount,useLayoutEffectmeasures each chip width viagetBoundingClientRect().width(sub-pixel precision) and caches them in a ref. Visible count is derived viacalculateVisibleCount— a pure function that sums chip widths against container width with strict (no-tolerance) fit. The row gap and+Noverflow chip widths (per digit count) are not hardcoded — they are measured per column byGridMultiSelectMeasurer(see the 2026-05-04 follow-up below) and read fromapiRef.caches.multiSelect; calibrated defaults (gap = 4 px, 32/38/44 px for 1/2/3-digit overflow) act as fallbacks until the first measurement reports.Live resize. The cell subscribes to the
columnResizeevent and commitscontainerWidthvia a 32 ms throttle. During drag, container width is derived fromparams.widthminus a cachedcolumn − containeroffset (cell padding + borders), so the cell makes no DOM reads per tick. After drag, thecolumnWidthprop effect re-measures the container withgetBoundingClientRectfor sub-pixel accuracy. Chip widths are still measured sub-pixel viagetBoundingClientRecton layout.Hysteresis. Natural pointer jitter produces ±1-3 px micro-reversals around a chip-fit boundary, which without smoothing makes
visibleCountflap between N and N+1. The cell applies asymmetric hysteresis — upward transitions (revealing a chip) require 4 px of headroom past the real boundary; downward transitions (hiding a chip) apply immediately so chips never visually overflow the cell edge. State kept inprevVisibleCountRef, committed in auseLayoutEffect.For auto row height, the component detects
rowHasAutoHeightand skips the overflow logic entirely, using CSSflexWrapinstead.For autosizing, hidden chips are temporarily shown (their CSS class is toggled) so the autosize algorithm measures the full content width.
Group rows.
renderMultiSelectCellshort-circuits toformattedValuewhenrowNode.type === 'group'so grouped rows display the joined key as plain text instead of trying to render chips over a string value.Edit cell
GridEditMultiSelectCelluses MUI Autocomplete in multiple mode withdisableCloseOnSelect. The Popper is portaled to the row element and positioned with offset. Enter key handling differs by edit mode:Enterexits edit mode;Ctrl/Cmd+Entercommits the value.Enterdismisses the popup but keeps the cell focused (a secondEnterre-opens it);Ctrl/Cmd+Entercommits.The popup height is density-aware via CSS variable
var(--_rowHeight)derived from grid state, scaling the listbox maxHeight tocalc(var(--_rowHeight) * 3.5).Header filter
Uses a multiple
Select(viabaseSelectwithmultiple) instead of Autocomplete to avoid layout overflow issues with chips in the constrained header filter row.SelectPropswas extended withmultipleandrenderValueto support this.Filter operators
gridMultiSelectOperatorsprovidescontainsanddoesNotContainwith a multi-valuebaseSelect(multiple) input (GridFilterInputMultipleMultiSelect) — the same component reused by the header filter.The two operators are deliberately asymmetric:
contains [A, B]uses OR — matches rows whose array contains A or B (any of the filter values).doesNotContain [A, B]matches rows whose array contains none of the filter values.This pair maps cleanly to user intent for tag-style data ("contains any" / "contains none").
is/isNotoperators were considered but removed to keep the UX simple. Filter values are pre-parsed into aSetso the apply function is one pass over the cell array.Row grouping
Grouping works out of the box because
GRID_MULTI_SELECT_COL_DEFships a defaultgroupingValueGetterthat serializes the cell array to a sorted comma-joined string. BothgroupingValueGetterandrowSpanValueGettershare amultiSelectKeyhelper. Reference-equal arrays with the same members (any order) collapse into a single group; users who want order-sensitivity override the getter explicitly.Two upstream tweaks were needed for the grouping column to render multiSelect keys correctly:
createGroupingColDefskips the original column'svalueFormatterwhen it's a multiSelect (the default formatter expects an array; the group key is a string). Same treatment singleSelect already had.renderMultiSelectCellreturnsformattedValueinstead of<GridMultiSelectCell>when the row is a group row, avoiding the array-shaped chip rendering on a string key.Pro / Premium integration
Gating is applied at runtime by
useGridMultiSelectPreProcessorsonGRID_MULTI_SELECT_COL_DEF. Charts and aggregation panels read these flags post-hydrate from the column lookup; the pivot panel skipstype === 'multiSelect'explicitly (see the 2026-05-04 type-system cleanup follow-up below). Each flag prevents Tags-style columns from appearing where their array values would produce nonsense:groupabletrue(inherited)groupingValueGetterserializes arrays to a sorted comma-joined key. Rows with the same set of values cluster regardless of order; override the getter to customizepivotablefalsechartabletrueavailableAggregationFunctions['size']sizeappears in the column menu; numeric aggs (sum/avg/min/max) are filtered outUsers can opt in or out of any of these by setting them explicitly on their column; defaults apply only when unset. Aggregated cells render via
GridFooterCell(footer position) orparams.formattedValue(grouped rows) so they visually match other column types.A dev-only
warnOncefires whentype: 'multiSelect'is used in the community-tier<DataGrid />(the multiSelect preprocessor is Pro-only, so the column would otherwise silently render as a string column).CSS classes
New classes added to
gridClasses:multiSelectCell,multiSelectCellChip,multiSelectCellChip--hidden,multiSelectCellOverflow,multiSelectCellPopup,multiSelectCellPopperContent.Review follow-ups (2026-04-15, @MBilalShafi)
multiSelectcolumns now opt out of pivoting and charts;sizeis the only exposed aggregation function. (Grouping was initially gated too but has since been re-enabled — see the @cherniavskii follow-up below.)contains [A, B]uses OR semantics: matches rows containing A or B.doesNotContain [A, B]matches rows containing none of the filter values. The asymmetry is intentional — "contains any" / "contains none" map cleanly to user intent for tag-style data.width: max-content+overflow: visible, letting the intrinsic sum of all (now-revealed) chips drive the column width instead of the flex container'swidth: 100%capping it.aria-rowspan > 1now top-align (alignItems+alignContent: flex-start), wrap (flexWrap: wrap), and drop the+Noverflow so all chips stay visible regardless of how the tall spanned cell is scrolled.pastedValueParsernow splits on any of, ; \t \n |(runs collapsed) and matches option value / label case-insensitively. Defaultseparatoralso changed from', '→','for cleaner CSV / copy-paste round-trips.+Nchip disappears at narrow widths (#r3087186067) — fixed by the cell-display rewrite: sub-pixel chip widths viagetBoundingClientRect, sub-pixel container width on commit, strict (no-tolerance) fit incalculateVisibleCount, and 4 px asymmetric hysteresis to absorb both pointer jitter and the integer column width used during active drag.Review follow-ups (2026-04-22, @cherniavskii)
groupingValueGetter, so rows sharing the same set of values (regardless of order) cluster into one group.RowGroupingFullExamplegrouping model updated to['commodity', 'tags']so the multiSelect grouping path is visible in the docs. Users can override with their owngroupingValueGetterto customize (e.g. order-sensitive grouping).createGroupingColDefwas updated to bypass the multiSelectvalueFormatteron grouping keys (mirrors the existing singleSelect bypass);renderMultiSelectCellrendersformattedValuefor group rows instead of trying to render chips over a string key.Review follow-ups (2026-04-29, edit-cell UX refactor)
baseModalslot added to the grid's slot system. The multiSelect edit cell now wraps its popper in a<rootProps.slots.baseModal>(default Material implementation forwards to@mui/material/Modal). Provides scroll-lock + backdrop the same waysingleSelect(MaterialSelect) does, and lets consumers swap it out alongside other base slots.openstate in the edit cell, decoupled fromhasFocus. Previously the popup was tied to grid focus, which meant Escape couldn't dismiss it without losing the cell. Now the cell has its ownopenand:cellEditStop(existing behavior).setOpen(false)(the cell stays focused, value preserved). Click on chips area or pressingEnterreopens. A second Escape on the focused cell bubbles to the grid'scellKeyDownlistener and exits the row — matchingsingleSelect's two-step Escape.apiRef.scrollToIndexeson the focus-gain edge (tracked via a ref) to bring the cell into view via the grid's own scroll API.openresets totrueso Shift+Tabbing back opens the autocomplete by default — matches the "every focus opens the autocomplete" expectation.stopPropagationto "avoid the Modal handling the event". AnonKeyDownCaptureon the popper content runs first (top-down) so we can route Escape throughclosePopupregardless of Autocomplete's own bubble-phase handling.Review follow-ups (2026-04-30, code review pass)
GridMultiSelectCell. The "show filtered value first" reorder didrawArrayValue.indexOf(filterValue)against an array filter value, which always returned-1, so the reorder never fired. Now finds the first cell value matching any of the activecontainsfilter values and hoists it to the front.contains/doesNotContainperformance. Filter values are pre-parsed into aSet; the apply function is one pass over the cell array (O(n) instead of O(n × m)).columnResize, the cell usesparams.widthminus a cachedcolumn − containeroffset; sub-pixelgetBoundingClientRectonly runs on commit. Eliminates per-cell DOM reads on every drag tick.columnResize+columnResizeStoplisteners, tripping the grid's "Possible EventEmitter memory leak detected. 21 columnResizeStop listeners added" warning on any tall grid.useGridMultiSelectPreProcessorsnow hosts a single per-grid drag broadcaster onapiRef.current.caches.multiSelect(subscribeDrag(field, cb)), throttled per field. Cells consume via oneuseEffectand unsubscribe on unmount. Listener count is now O(1) regardless of how many multiSelect cells are visible; per-tick setTimeouts collapse to one timer per actively-resized field. Tests verify listener count stays bounded with 25 visible rows and thatsubscribeDragbroadcasts / cleans up correctly.apiRef.scrollToIndexesnow fires only on the focus-gain edge (tracked viaprevHasFocusRef); a separate effect handles popup-closed-while-focused chip refocus. Avoids redundant scroll calls whenopentoggles while focus is held.optionByValueinGridMultiSelectCellto stop rebuilding the Map (and invalidating downstream memos) on every render.multiSelectKeyhelper shared bygroupingValueGetterandrowSpanValueGetter— previously identical duplicated code.as anyaggregation casts inrenderMultiSelectCellby typing the params withGridAggregationCellMeta.type: 'multiSelect'is used in the community-tier<DataGrid />. The multiSelect preprocessor is Pro-only, so without the warning the column would silently render as a string column with broken array handling.contains; new test for the chip-reorder behavior under an activecontainsfilter.GridMultiSelectChipssubcomponent (delegated fromGridMultiSelectCell); hysteresis now gated to active drag (snaps to exact fit on autosize / mount / prop-width changes); subscription is via the per-grid broadcaster onapiRef.caches.multiSelect.subscribeDrag, notcolumnResizedirectly. Supersedes the "Cell display: measure-once + live resize" architecture text above.Review follow-ups (2026-05-04, font-independent overflow chip widths)
Hardcoded overflow chip widths depend on font/style (#r3180068015) — the
+Nchip widths (32/38/44 px for 1/2/3 digits) and row gap (4 px) were calibrated for the default Material small outlined chip. Under custom fonts, theme slot overrides onMuiDataGrid-MultiSelectChips, or chip variant changes, the algorithm could mis-computevisibleCountand clip chips at the cell edge.Replaced with a per-column measurer (
GridMultiSelectMeasurer) that renders three hidden sample chips (+9/+99/+999) insideMultiSelectChipsRoot(the same styled slot the cells use), measures their widths viagetBoundingClientRect, and reads the row's actualgapfromgetComputedStyle. Metrics are published per field toapiRef.caches.multiSelect(setOverflowMetrics(field, ...)); cells subscribe viasubscribeOverflowMetrics(field, cb)and fall back to the calibrated defaults until the first measurement reports.Lifecycle is header-driven. The measurer is mounted via the column's
renderHeader, composed byuseGridMultiSelectPreProcessorsso the user'srenderHeader(if any) is wrapped — the measurer is preserved regardless of customization. When the column is hidden via the visibility model, the header unmounts → measurer unmounts → field's metrics are cleared from the cache. No grid-root mount, no global gating; pay-for-what-you-use per column.Implementation reuses
useResizeObserverfrom@mui/x-internals(handles the dev-mode RAF wrap that prevents the React "update during render" warning) andfastArrayComparefor the cache's no-op dedupe so subscribed cells don't re-render on equal updates.Review follow-ups (2026-05-04, type-system cleanups, @arminmeh)
singleSelect.getOptionValuereturn type (#r3180059816) — reverted(value: ValueOptions) => string | numberback to=> anyonGridSingleSelectColDef. Narrowing was a breaking change for consumers returning objects/booleans and transforming them throughvalueGetter/valueFormatter/renderCell. JSDoc reverted to@returns {string}to match master.multiSelectnon-existent in CommunityGridColumnTypesRecord(#r3180438412) — convertedGridColumnTypesRecordfrom atypealias to aninterfacethat omitsmultiSelect; Pro augments it via the existingdeclare module '@mui/x-data-grid'block intypeOverloads/modules.tsto addmultiSelect?: GridColTypeDef(same pattern asGridEventLookup/GridColDefPro/ etc., no new mechanism). Dropped the runtime stub fromgetGridDefaultColumnTypes: charts and aggregation read their gates post-hydrate from the column lookup (set byuseGridMultiSelectPreProcessorsonGRID_MULTI_SELECT_COL_DEF); the pivot panel now skipstype === 'multiSelect'directly since pivot pre-reads col-type defaults fromprops.columnsbefore any preprocessor runs and pivot doesn't support array values anyway.