@mui/x-data-grid 9.13.0 → 9.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,154 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.14.0
4
+
5
+ _Sep 17, 2026_
6
+
7
+ We'd like to extend a big thank you to the 16 contributors who made this release possible. Here are some highlights ✨:
8
+
9
+ - 🌍 Add Hebrew (he-IL) locale to the Scheduler
10
+ - 📅 Scheduler time grid now opens at 7 AM by default, configurable with the new `initialScrollTime` prop
11
+ - 🗓️ Auto-scheduling engine for finish-to-start dependencies in the Premium Event Timeline
12
+ - 🧩 New content slots for the Event Timeline event block and resource title cell
13
+ - ♿️ Charts announce the visible zoom range to screen readers
14
+ - 🐞 Bugfixes
15
+ - 📚 Documentation improvements
16
+
17
+ Special thanks go out to these community members for their valuable contributions:
18
+ @eterry1388, @jamesDin, @kareem-ib, @mateuseap, @mustafajw07
19
+
20
+ The following team members contributed to this release:
21
+ @arminmeh, @bernardobelchior, @brijeshb42, @flaviendelangle, @hasdfa, @Janpot, @JCQuintas, @MBilalShafi, @michelengelen, @noraleonte, @rita-codes
22
+
23
+ ### Data Grid
24
+
25
+ #### `@mui/x-data-grid@9.14.0`
26
+
27
+ - [DataGrid] Do not crash on unmount when the document has no body (#23431) @eterry1388
28
+ - [DataGrid] Fix `MissingRowIdError` on cell `mousedown`/`mouseup` after row removal (#23261) @kareem-ib
29
+ - [DataGrid] Fix quick filter dropping a pending search on parser identity change (#23575) @michelengelen
30
+ - [DataGrid] Keep overlays sticky in RTL mode (#23492) @arminmeh
31
+ - [DataGrid] Prevent scroll area state update before mount (#23478) @michelengelen
32
+ - [DataGrid] Remove `'menu'` role from the actions cell (#23437) @michelengelen
33
+
34
+ #### `@mui/x-data-grid-pro@9.14.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
35
+
36
+ Same changes as in `@mui/x-data-grid@9.14.0`, plus:
37
+
38
+ - [DataGridPro] Fix data integrity issue in nested server data (#23518) @MBilalShafi
39
+ - [DataGridPro] Keep parent selection when lazily loaded children arrive (#23421) @michelengelen
40
+
41
+ #### `@mui/x-data-grid-premium@9.14.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
42
+
43
+ Same changes as in `@mui/x-data-grid-pro@9.14.0`.
44
+
45
+ ### Date and Time Pickers
46
+
47
+ #### `@mui/x-date-pickers@9.14.0`
48
+
49
+ Internal changes.
50
+
51
+ #### `@mui/x-date-pickers-pro@9.14.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
52
+
53
+ Same changes as in `@mui/x-date-pickers@9.14.0`.
54
+
55
+ ### Charts
56
+
57
+ #### `@mui/x-charts@9.14.0`
58
+
59
+ - [charts] Announce the visible zoom range to screen readers (#23371) @JCQuintas
60
+ - [charts] Fix Premium chart zoom props and `apiRef` forwarding (#23590) @bernardobelchior
61
+
62
+ #### `@mui/x-charts-pro@9.14.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
63
+
64
+ Same changes as in `@mui/x-charts@9.14.0`.
65
+
66
+ #### `@mui/x-charts-premium@9.14.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
67
+
68
+ Same changes as in `@mui/x-charts-pro@9.14.0`, plus:
69
+
70
+ - [charts-premium] Add the Excel data extraction layer (#23569) @JCQuintas
71
+ - [charts-premium] Forward `apiRef` on `RadialBarChart` and `RadialLineChart` (#23625) @JCQuintas
72
+
73
+ ### Tree View
74
+
75
+ #### `@mui/x-tree-view@9.14.0`
76
+
77
+ - [tree view] Optimize lazy loading store writes for nested children (#23489) @noraleonte
78
+ - [tree view] Prune stale children when replacing an item's children (#23598) @JCQuintas
79
+
80
+ #### `@mui/x-tree-view-pro@9.14.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
81
+
82
+ Same changes as in `@mui/x-tree-view@9.14.0`.
83
+
84
+ ### Scheduler
85
+
86
+ #### Breaking changes
87
+
88
+ - The `dataSource` prop has been removed from the Community Event Calendar components:
89
+ `EventCalendar`, `StandaloneDayView`, `StandaloneWeekView`, `StandaloneMonthView`,
90
+ `StandaloneAgendaView`, `StandaloneCompactDayView`, `StandaloneCompactThreeDayView` and
91
+ `StandaloneCompactWeekView`.
92
+
93
+ Lazy loading has always been Premium-only, and the prop never had any effect on these
94
+ components: it left the calendar in a permanent loading state. To load events through a data
95
+ source, use `EventCalendarPremium` (or the Premium standalone views) and `EventTimelinePremium`,
96
+ where `dataSource` is unchanged.
97
+
98
+ ```diff
99
+ -import { EventCalendar } from '@mui/x-scheduler/event-calendar';
100
+ +import { EventCalendarPremium } from '@mui/x-scheduler-premium/event-calendar-premium';
101
+
102
+ -<EventCalendar dataSource={dataSource} />
103
+ +<EventCalendarPremium dataSource={dataSource} />
104
+ ```
105
+
106
+ #### `@mui/x-scheduler@9.0.0-beta.12`
107
+
108
+ - [l10n] Add Hebrew (he-IL) locale (#23577) @jamesDin
109
+ - [scheduler] Add content slots for the Event Timeline event block and resource title cell (#23557) @rita-codes
110
+ - [scheduler] Fix `onPreferencesChange` never being called on `EventCalendar` (#23568) @rita-codes
111
+ - [scheduler] Fix flaky timeline observer absorb (#23523) @JCQuintas
112
+ - [scheduler] Open the time grid at 7 AM by default and add `initialScrollTime` (#23594) @rita-codes
113
+ - [scheduler] Pin sibling scheduler packages to exact versions (#23574) @rita-codes
114
+ - [scheduler] Read form initialization settings without subscriptions (#23614) @flaviendelangle
115
+ - [scheduler] Remove `dataSource` from the Community components (#23483) @mustafajw07
116
+ - [scheduler] Remove redundant recurrence subscriptions (#23610) @flaviendelangle
117
+ - [scheduler] Remove unused more-events popover count (#23619) @flaviendelangle
118
+ - [scheduler] Remove unused refs (#23616) @flaviendelangle
119
+ - [scheduler] Reuse precomputed timeline duration (#23620) @flaviendelangle
120
+ - [scheduler] Reuse recurrence option labels (#23609) @flaviendelangle
121
+ - [scheduler] Reuse recurrence presets for form drafts (#23615) @flaviendelangle
122
+ - [scheduler] Simplify month-view overflow counting (#23617) @flaviendelangle
123
+ - [scheduler] Use form store state to guard submissions (#23613) @flaviendelangle
124
+
125
+ #### `@mui/x-scheduler-premium@9.0.0-beta.12` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
126
+
127
+ Same changes as in `@mui/x-scheduler@9.0.0-beta.12`, plus:
128
+
129
+ - [scheduler-premium] Dependencies - Arrows and terminals for SS, FF and SF (#23524) @rita-codes
130
+ - [scheduler-premium] Dependencies - Auto-scheduling engine (FS) (#23439) @rita-codes
131
+
132
+ ### Docs
133
+
134
+ - [DataGrid][docs] Fix "Apply filters on demand" demo (#23602) @mateuseap
135
+ - [docs] Add recipe for getting the tree view's root DOM element via `ref` (#22941) @michelengelen
136
+ - [docs][pickers] Mention `Moment.js` maintenance mode (#23601) @mateuseap
137
+
138
+ ### Core
139
+
140
+ - [code-infra] Gate the `DataGridScrollRestoration` screenshot on the horizontal restore (#23527) @JCQuintas
141
+ - [code-infra] Let the `NoColumnsOverlay` demo settle before screenshotting (#23529) @JCQuintas
142
+ - [code-infra] Pin the only-allow version in the preinstall hook (#23551) @Janpot
143
+ - [code-infra] Remove disabled Discord release announcement workflow (#23554) @Janpot
144
+ - [code-infra] Trial: Claude CI flake-fix caller (#23556) @Janpot
145
+
146
+ ### Miscellaneous
147
+
148
+ - [chat] Add feedback form to the docs (#23403) @hasdfa
149
+ - [ci] Grant actions: read to the flake-fix caller (#23560) @Janpot
150
+ - [ci] Update flake-fix caller to mui-public#1849 (#23571) @Janpot
151
+
3
152
  ## 9.13.0
4
153
 
5
154
  <!-- generated comparing v9.12.0..master -->
@@ -27,6 +27,7 @@ var _useTimeout = require("../hooks/utils/useTimeout");
27
27
  var _gridColumnsUtils = require("../hooks/features/columns/gridColumnsUtils");
28
28
  var _createSelector = require("../utils/createSelector");
29
29
  var _gridRowsMetaSelector = require("../hooks/features/rows/gridRowsMetaSelector");
30
+ var _gridRowReorderSelector = require("../hooks/features/rowReorder/gridRowReorderSelector");
30
31
  var _jsxRuntime = require("react/jsx-runtime");
31
32
  const CLIFF = 1;
32
33
  const SLOP = 1.5;
@@ -89,15 +90,27 @@ const offsetSelector = (0, _createSelector.createSelector)(_gridDimensionsSelect
89
90
  // For vertical scroll areas, we don't need horizontal offset
90
91
  return 0;
91
92
  });
93
+
94
+ // The `columnReorder` state is only initialized by the Pro package,
95
+ // so it is read defensively here.
96
+ const gridColumnReorderDragColSelector = (0, _createSelector.createRootSelector)(state => state.columnReorder?.dragCol);
97
+
98
+ // The drag direction is derived from the reorder state instead of the drag events.
99
+ // An event handler subscribed during render can fire for a render pass that never mounts.
100
+ // A `setState` call from such a handler triggers a React warning.
101
+ // See https://github.com/mui/mui-x/issues/23469.
102
+ const gridDragDirectionSelector = (0, _createSelector.createSelector)(gridColumnReorderDragColSelector, _gridRowReorderSelector.gridIsRowDragActiveSelector, (dragCol, isRowDragActive) => {
103
+ if (dragCol) {
104
+ return 'horizontal';
105
+ }
106
+ if (isRowDragActive) {
107
+ return 'vertical';
108
+ }
109
+ return 'none';
110
+ });
92
111
  function GridScrollAreaWrapper(props) {
93
112
  const apiRef = (0, _useGridApiContext.useGridApiContext)();
94
- const [dragDirection, setDragDirection] = React.useState('none');
95
-
96
- // Listen for both column and row drag events
97
- (0, _useGridEvent.useGridEvent)(apiRef, 'columnHeaderDragStart', () => setDragDirection('horizontal'));
98
- (0, _useGridEvent.useGridEvent)(apiRef, 'columnHeaderDragEnd', () => setDragDirection('none'));
99
- (0, _useGridEvent.useGridEvent)(apiRef, 'rowDragStart', () => setDragDirection('vertical'));
100
- (0, _useGridEvent.useGridEvent)(apiRef, 'rowDragEnd', () => setDragDirection('none'));
113
+ const dragDirection = (0, _useGridSelector.useGridSelector)(apiRef, gridDragDirectionSelector);
101
114
  if (dragDirection === 'none') {
102
115
  return null;
103
116
  }
@@ -18,8 +18,9 @@ import { gridDimensionsSelector, gridColumnsTotalWidthSelector } from "../hooks/
18
18
  import { gridDensityFactorSelector } from "../hooks/features/density/densitySelector.mjs";
19
19
  import { useTimeout } from "../hooks/utils/useTimeout.mjs";
20
20
  import { getTotalHeaderHeight } from "../hooks/features/columns/gridColumnsUtils.mjs";
21
- import { createSelector } from "../utils/createSelector.mjs";
21
+ import { createRootSelector, createSelector } from "../utils/createSelector.mjs";
22
22
  import { gridRowsMetaSelector } from "../hooks/features/rows/gridRowsMetaSelector.mjs";
23
+ import { gridIsRowDragActiveSelector } from "../hooks/features/rowReorder/gridRowReorderSelector.mjs";
23
24
  import { jsx as _jsx } from "react/jsx-runtime";
24
25
  const CLIFF = 1;
25
26
  const SLOP = 1.5;
@@ -82,15 +83,27 @@ const offsetSelector = createSelector(gridDimensionsSelector, (dimensions, direc
82
83
  // For vertical scroll areas, we don't need horizontal offset
83
84
  return 0;
84
85
  });
86
+
87
+ // The `columnReorder` state is only initialized by the Pro package,
88
+ // so it is read defensively here.
89
+ const gridColumnReorderDragColSelector = createRootSelector(state => state.columnReorder?.dragCol);
90
+
91
+ // The drag direction is derived from the reorder state instead of the drag events.
92
+ // An event handler subscribed during render can fire for a render pass that never mounts.
93
+ // A `setState` call from such a handler triggers a React warning.
94
+ // See https://github.com/mui/mui-x/issues/23469.
95
+ const gridDragDirectionSelector = createSelector(gridColumnReorderDragColSelector, gridIsRowDragActiveSelector, (dragCol, isRowDragActive) => {
96
+ if (dragCol) {
97
+ return 'horizontal';
98
+ }
99
+ if (isRowDragActive) {
100
+ return 'vertical';
101
+ }
102
+ return 'none';
103
+ });
85
104
  function GridScrollAreaWrapper(props) {
86
105
  const apiRef = useGridApiContext();
87
- const [dragDirection, setDragDirection] = React.useState('none');
88
-
89
- // Listen for both column and row drag events
90
- useGridEvent(apiRef, 'columnHeaderDragStart', () => setDragDirection('horizontal'));
91
- useGridEvent(apiRef, 'columnHeaderDragEnd', () => setDragDirection('none'));
92
- useGridEvent(apiRef, 'rowDragStart', () => setDragDirection('vertical'));
93
- useGridEvent(apiRef, 'rowDragEnd', () => setDragDirection('none'));
106
+ const dragDirection = useGridSelector(apiRef, gridDragDirectionSelector);
94
107
  if (dragDirection === 'none') {
95
108
  return null;
96
109
  }
@@ -21,19 +21,21 @@ var _jsxRuntime = require("react/jsx-runtime");
21
21
  const GridOverlayWrapperRoot = (0, _styles.styled)('div', {
22
22
  name: 'MuiDataGrid',
23
23
  slot: 'OverlayWrapper',
24
- shouldForwardProp: prop => prop !== 'overlayType' && prop !== 'loadingOverlayVariant' && prop !== 'right'
24
+ shouldForwardProp: prop => prop !== 'overlayType' && prop !== 'loadingOverlayVariant'
25
25
  })(({
26
26
  overlayType,
27
- loadingOverlayVariant,
28
- right
27
+ loadingOverlayVariant
29
28
  }) =>
30
29
  // Skeleton overlay should flow with the scroll container and not be sticky
31
30
  loadingOverlayVariant !== 'skeleton' ? {
32
31
  position: 'sticky',
33
32
  // To stay in place while scrolling
34
33
  top: 'var(--DataGrid-topContainerHeight)',
34
+ // Both 0-insets keep the overlay in the viewport in LTR and RTL alike. Sticky insets
35
+ // are independent constraints, and a width: 0 box satisfies both at once, so it
36
+ // clamps at whichever edge the scroll pushes it past.
35
37
  left: 0,
36
- right: `${right}px`,
38
+ right: 0,
37
39
  width: 0,
38
40
  // To stay above the content instead of shifting it down
39
41
  height: 0,
@@ -70,7 +72,6 @@ function GridOverlayWrapper(props) {
70
72
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(GridOverlayWrapperRoot, (0, _extends2.default)({
71
73
  className: classes.root
72
74
  }, props, {
73
- right: dimensions.columnsTotalWidth - dimensions.viewportOuterSize.width,
74
75
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(GridOverlayWrapperInner, (0, _extends2.default)({
75
76
  className: classes.inner,
76
77
  style: {
@@ -13,19 +13,21 @@ import { jsx as _jsx } from "react/jsx-runtime";
13
13
  const GridOverlayWrapperRoot = styled('div', {
14
14
  name: 'MuiDataGrid',
15
15
  slot: 'OverlayWrapper',
16
- shouldForwardProp: prop => prop !== 'overlayType' && prop !== 'loadingOverlayVariant' && prop !== 'right'
16
+ shouldForwardProp: prop => prop !== 'overlayType' && prop !== 'loadingOverlayVariant'
17
17
  })(({
18
18
  overlayType,
19
- loadingOverlayVariant,
20
- right
19
+ loadingOverlayVariant
21
20
  }) =>
22
21
  // Skeleton overlay should flow with the scroll container and not be sticky
23
22
  loadingOverlayVariant !== 'skeleton' ? {
24
23
  position: 'sticky',
25
24
  // To stay in place while scrolling
26
25
  top: 'var(--DataGrid-topContainerHeight)',
26
+ // Both 0-insets keep the overlay in the viewport in LTR and RTL alike. Sticky insets
27
+ // are independent constraints, and a width: 0 box satisfies both at once, so it
28
+ // clamps at whichever edge the scroll pushes it past.
27
29
  left: 0,
28
- right: `${right}px`,
30
+ right: 0,
29
31
  width: 0,
30
32
  // To stay above the content instead of shifting it down
31
33
  height: 0,
@@ -62,7 +64,6 @@ export function GridOverlayWrapper(props) {
62
64
  return /*#__PURE__*/_jsx(GridOverlayWrapperRoot, _extends({
63
65
  className: classes.root
64
66
  }, props, {
65
- right: dimensions.columnsTotalWidth - dimensions.viewportOuterSize.width,
66
67
  children: /*#__PURE__*/_jsx(GridOverlayWrapperInner, _extends({
67
68
  className: classes.inner,
68
69
  style: {
@@ -20,6 +20,7 @@ var _gridClasses = require("../../constants/gridClasses");
20
20
  var _GridMenu = require("../menu/GridMenu");
21
21
  var _useGridRootProps = require("../../hooks/utils/useGridRootProps");
22
22
  var _useGridApiContext = require("../../hooks/utils/useGridApiContext");
23
+ var _domUtils = require("../../utils/domUtils");
23
24
  var _GridActionsCellItem = require("./GridActionsCellItem");
24
25
  var _jsxRuntime = require("react/jsx-runtime");
25
26
  const _excluded = ["api", "colDef", "id", "hasFocus", "isEditable", "field", "value", "formattedValue", "row", "rowNode", "cellMode", "tabIndex", "position", "onMenuOpen", "onMenuClose", "children", "suppressChildrenValidation"];
@@ -140,7 +141,9 @@ If this is intentional, you can suppress this warning by passing the \`suppressC
140
141
  }
141
142
  };
142
143
  const handleRootKeyDown = event => {
143
- if (numberOfButtons <= 1) {
144
+ // The menu renders in a portal, but its key events still bubble through the React tree.
145
+ // They must not move the roving focus between the buttons behind the open menu.
146
+ if (numberOfButtons <= 1 || (0, _domUtils.isEventTargetInPortal)(event)) {
144
147
  return;
145
148
  }
146
149
  const getNewIndex = (index, direction) => {
@@ -170,55 +173,55 @@ If this is intentional, you can suppress this warning by passing the \`suppressC
170
173
  setFocusedButtonIndex(newIndex);
171
174
  }
172
175
  };
173
-
174
- // role="menu" requires at least one child element
175
- const attributes = numberOfButtons > 0 ? {
176
- role: 'menu',
177
- onKeyDown: handleRootKeyDown
178
- } : undefined;
179
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", (0, _extends2.default)({
180
- ref: rootRef,
181
- tabIndex: -1,
182
- className: _gridClasses.gridClasses.actionsCell
183
- }, attributes, other, {
184
- children: [iconButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
185
- key: index,
186
- touchRippleRef: handleTouchRippleRef(index),
187
- onClick: handleButtonClick(index, button.props.onClick),
188
- tabIndex: focusedButtonIndex === index ? tabIndex : -1
189
- })), menuButtons.length > 0 && buttonId && /*#__PURE__*/(0, _jsxRuntime.jsx)(rootProps.slots.baseIconButton, (0, _extends2.default)({
190
- ref: buttonRef,
191
- id: buttonId,
192
- "aria-label": apiRef.current.getLocaleText('actionsCellMore'),
193
- "aria-haspopup": "menu",
194
- "aria-expanded": open,
195
- "aria-controls": open ? menuId : undefined,
196
- role: "menuitem",
197
- size: "small",
198
- onClick: toggleMenu,
199
- touchRippleRef: handleTouchRippleRef(buttonId),
200
- tabIndex: focusedButtonIndex === iconButtons.length ? tabIndex : -1
201
- }, rootProps.slotProps?.baseIconButton, {
202
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(rootProps.slots.moreActionsIcon, {
203
- fontSize: "small"
204
- })
205
- })), menuButtons.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_GridMenu.GridMenu, {
206
- open: open,
207
- target: buttonRef.current,
208
- position: position,
209
- onClose: hideMenu,
210
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(rootProps.slots.baseMenuList, {
211
- id: menuId,
212
- className: _gridClasses.gridClasses.menuList,
213
- "aria-labelledby": buttonId,
214
- autoFocusItem: true,
215
- children: menuButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
216
- key: index,
217
- closeMenu: hideMenu
218
- }))
219
- })
220
- })]
221
- }));
176
+ return (
177
+ /*#__PURE__*/
178
+ // The wrapper carries no role on purpose: the buttons are plain buttons inside the
179
+ // `gridcell`. It only relays the key events bubbling from them.
180
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
181
+ (0, _jsxRuntime.jsxs)("div", (0, _extends2.default)({
182
+ ref: rootRef,
183
+ tabIndex: -1,
184
+ className: _gridClasses.gridClasses.actionsCell,
185
+ onKeyDown: handleRootKeyDown
186
+ }, other, {
187
+ children: [iconButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
188
+ key: index,
189
+ touchRippleRef: handleTouchRippleRef(index),
190
+ onClick: handleButtonClick(index, button.props.onClick),
191
+ tabIndex: focusedButtonIndex === index ? tabIndex : -1
192
+ })), menuButtons.length > 0 && buttonId && /*#__PURE__*/(0, _jsxRuntime.jsx)(rootProps.slots.baseIconButton, (0, _extends2.default)({
193
+ ref: buttonRef,
194
+ id: buttonId,
195
+ "aria-label": apiRef.current.getLocaleText('actionsCellMore'),
196
+ "aria-haspopup": "menu",
197
+ "aria-expanded": open,
198
+ "aria-controls": open ? menuId : undefined,
199
+ size: "small",
200
+ onClick: toggleMenu,
201
+ touchRippleRef: handleTouchRippleRef(buttonId),
202
+ tabIndex: focusedButtonIndex === iconButtons.length ? tabIndex : -1
203
+ }, rootProps.slotProps?.baseIconButton, {
204
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(rootProps.slots.moreActionsIcon, {
205
+ fontSize: "small"
206
+ })
207
+ })), menuButtons.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_GridMenu.GridMenu, {
208
+ open: open,
209
+ target: buttonRef.current,
210
+ position: position,
211
+ onClose: hideMenu,
212
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(rootProps.slots.baseMenuList, {
213
+ id: menuId,
214
+ className: _gridClasses.gridClasses.menuList,
215
+ "aria-labelledby": buttonId,
216
+ autoFocusItem: true,
217
+ children: menuButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
218
+ key: index,
219
+ closeMenu: hideMenu
220
+ }))
221
+ })
222
+ })]
223
+ }))
224
+ );
222
225
  }
223
226
  process.env.NODE_ENV !== "production" ? GridActionsCell.propTypes /* remove-proptypes */ = {
224
227
  // ----------------------------- Warning --------------------------------
@@ -13,6 +13,7 @@ import { gridClasses } from "../../constants/gridClasses.mjs";
13
13
  import { GridMenu } from "../menu/GridMenu.mjs";
14
14
  import { useGridRootProps } from "../../hooks/utils/useGridRootProps.mjs";
15
15
  import { useGridApiContext } from "../../hooks/utils/useGridApiContext.mjs";
16
+ import { isEventTargetInPortal } from "../../utils/domUtils.mjs";
16
17
  import { GridActionsCellItem } from "./GridActionsCellItem.mjs";
17
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
19
  const hasActions = colDef => typeof colDef.getActions === 'function';
@@ -132,7 +133,9 @@ If this is intentional, you can suppress this warning by passing the \`suppressC
132
133
  }
133
134
  };
134
135
  const handleRootKeyDown = event => {
135
- if (numberOfButtons <= 1) {
136
+ // The menu renders in a portal, but its key events still bubble through the React tree.
137
+ // They must not move the roving focus between the buttons behind the open menu.
138
+ if (numberOfButtons <= 1 || isEventTargetInPortal(event)) {
136
139
  return;
137
140
  }
138
141
  const getNewIndex = (index, direction) => {
@@ -162,55 +165,55 @@ If this is intentional, you can suppress this warning by passing the \`suppressC
162
165
  setFocusedButtonIndex(newIndex);
163
166
  }
164
167
  };
165
-
166
- // role="menu" requires at least one child element
167
- const attributes = numberOfButtons > 0 ? {
168
- role: 'menu',
169
- onKeyDown: handleRootKeyDown
170
- } : undefined;
171
- return /*#__PURE__*/_jsxs("div", _extends({
172
- ref: rootRef,
173
- tabIndex: -1,
174
- className: gridClasses.actionsCell
175
- }, attributes, other, {
176
- children: [iconButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
177
- key: index,
178
- touchRippleRef: handleTouchRippleRef(index),
179
- onClick: handleButtonClick(index, button.props.onClick),
180
- tabIndex: focusedButtonIndex === index ? tabIndex : -1
181
- })), menuButtons.length > 0 && buttonId && /*#__PURE__*/_jsx(rootProps.slots.baseIconButton, _extends({
182
- ref: buttonRef,
183
- id: buttonId,
184
- "aria-label": apiRef.current.getLocaleText('actionsCellMore'),
185
- "aria-haspopup": "menu",
186
- "aria-expanded": open,
187
- "aria-controls": open ? menuId : undefined,
188
- role: "menuitem",
189
- size: "small",
190
- onClick: toggleMenu,
191
- touchRippleRef: handleTouchRippleRef(buttonId),
192
- tabIndex: focusedButtonIndex === iconButtons.length ? tabIndex : -1
193
- }, rootProps.slotProps?.baseIconButton, {
194
- children: /*#__PURE__*/_jsx(rootProps.slots.moreActionsIcon, {
195
- fontSize: "small"
196
- })
197
- })), menuButtons.length > 0 && /*#__PURE__*/_jsx(GridMenu, {
198
- open: open,
199
- target: buttonRef.current,
200
- position: position,
201
- onClose: hideMenu,
202
- children: /*#__PURE__*/_jsx(rootProps.slots.baseMenuList, {
203
- id: menuId,
204
- className: gridClasses.menuList,
205
- "aria-labelledby": buttonId,
206
- autoFocusItem: true,
207
- children: menuButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
208
- key: index,
209
- closeMenu: hideMenu
210
- }))
211
- })
212
- })]
213
- }));
168
+ return (
169
+ /*#__PURE__*/
170
+ // The wrapper carries no role on purpose: the buttons are plain buttons inside the
171
+ // `gridcell`. It only relays the key events bubbling from them.
172
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
173
+ _jsxs("div", _extends({
174
+ ref: rootRef,
175
+ tabIndex: -1,
176
+ className: gridClasses.actionsCell,
177
+ onKeyDown: handleRootKeyDown
178
+ }, other, {
179
+ children: [iconButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
180
+ key: index,
181
+ touchRippleRef: handleTouchRippleRef(index),
182
+ onClick: handleButtonClick(index, button.props.onClick),
183
+ tabIndex: focusedButtonIndex === index ? tabIndex : -1
184
+ })), menuButtons.length > 0 && buttonId && /*#__PURE__*/_jsx(rootProps.slots.baseIconButton, _extends({
185
+ ref: buttonRef,
186
+ id: buttonId,
187
+ "aria-label": apiRef.current.getLocaleText('actionsCellMore'),
188
+ "aria-haspopup": "menu",
189
+ "aria-expanded": open,
190
+ "aria-controls": open ? menuId : undefined,
191
+ size: "small",
192
+ onClick: toggleMenu,
193
+ touchRippleRef: handleTouchRippleRef(buttonId),
194
+ tabIndex: focusedButtonIndex === iconButtons.length ? tabIndex : -1
195
+ }, rootProps.slotProps?.baseIconButton, {
196
+ children: /*#__PURE__*/_jsx(rootProps.slots.moreActionsIcon, {
197
+ fontSize: "small"
198
+ })
199
+ })), menuButtons.length > 0 && /*#__PURE__*/_jsx(GridMenu, {
200
+ open: open,
201
+ target: buttonRef.current,
202
+ position: position,
203
+ onClose: hideMenu,
204
+ children: /*#__PURE__*/_jsx(rootProps.slots.baseMenuList, {
205
+ id: menuId,
206
+ className: gridClasses.menuList,
207
+ "aria-labelledby": buttonId,
208
+ autoFocusItem: true,
209
+ children: menuButtons.map((button, index) => /*#__PURE__*/React.cloneElement(button, {
210
+ key: index,
211
+ closeMenu: hideMenu
212
+ }))
213
+ })
214
+ })]
215
+ }))
216
+ );
214
217
  }
215
218
  process.env.NODE_ENV !== "production" ? GridActionsCell.propTypes /* remove-proptypes */ = {
216
219
  // ----------------------------- Warning --------------------------------
@@ -30,7 +30,6 @@ const GridActionsCellItem = exports.GridActionsCellItem = (0, _forwardRef.forwar
30
30
  };
31
31
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(rootProps.slots.baseIconButton, (0, _extends2.default)({
32
32
  size: "small",
33
- role: "menuitem",
34
33
  "aria-label": label
35
34
  }, other, {
36
35
  onClick: handleClick
@@ -23,7 +23,6 @@ const GridActionsCellItem = forwardRef((props, ref) => {
23
23
  };
24
24
  return /*#__PURE__*/_jsx(rootProps.slots.baseIconButton, _extends({
25
25
  size: "small",
26
- role: "menuitem",
27
26
  "aria-label": label
28
27
  }, other, {
29
28
  onClick: handleClick
@@ -177,6 +177,10 @@ const GridCell = (0, _forwardRef.forwardRef)(function GridCell(props, ref) {
177
177
  };
178
178
  const classes = useUtilityClasses(ownerState);
179
179
  const publishMouseUp = React.useCallback(eventName => event => {
180
+ // The row might have been deleted during the click
181
+ if (!apiRef.current.getRow(rowId)) {
182
+ return;
183
+ }
180
184
  const params = apiRef.current.getCellParams(rowId, field || '');
181
185
  apiRef.current.publishEvent(eventName, params, event);
182
186
  if (onMouseUp) {
@@ -184,6 +188,10 @@ const GridCell = (0, _forwardRef.forwardRef)(function GridCell(props, ref) {
184
188
  }
185
189
  }, [apiRef, field, onMouseUp, rowId]);
186
190
  const publishMouseDown = React.useCallback(eventName => event => {
191
+ // The row might have been deleted during the click
192
+ if (!apiRef.current.getRow(rowId)) {
193
+ return;
194
+ }
187
195
  const params = apiRef.current.getCellParams(rowId, field || '');
188
196
  apiRef.current.publishEvent(eventName, params, event);
189
197
  if (onMouseDown) {
@@ -170,6 +170,10 @@ const GridCell = forwardRef(function GridCell(props, ref) {
170
170
  };
171
171
  const classes = useUtilityClasses(ownerState);
172
172
  const publishMouseUp = React.useCallback(eventName => event => {
173
+ // The row might have been deleted during the click
174
+ if (!apiRef.current.getRow(rowId)) {
175
+ return;
176
+ }
173
177
  const params = apiRef.current.getCellParams(rowId, field || '');
174
178
  apiRef.current.publishEvent(eventName, params, event);
175
179
  if (onMouseUp) {
@@ -177,6 +181,10 @@ const GridCell = forwardRef(function GridCell(props, ref) {
177
181
  }
178
182
  }, [apiRef, field, onMouseUp, rowId]);
179
183
  const publishMouseDown = React.useCallback(eventName => event => {
184
+ // The row might have been deleted during the click
185
+ if (!apiRef.current.getRow(rowId)) {
186
+ return;
187
+ }
180
188
  const params = apiRef.current.getCellParams(rowId, field || '');
181
189
  apiRef.current.publishEvent(eventName, params, event);
182
190
  if (onMouseDown) {
@@ -13,6 +13,7 @@ var React = _interopRequireWildcard(require("react"));
13
13
  var _propTypes = _interopRequireDefault(require("prop-types"));
14
14
  var _debounce = _interopRequireDefault(require("@mui/utils/debounce"));
15
15
  var _useEnhancedEffect = _interopRequireDefault(require("@mui/utils/useEnhancedEffect"));
16
+ var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
16
17
  var _useId = _interopRequireDefault(require("@mui/utils/useId"));
17
18
  var _isDeepEqual = require("@mui/x-internals/isDeepEqual");
18
19
  var _useComponentRenderer = require("@mui/x-internals/useComponentRenderer");
@@ -108,11 +109,15 @@ function QuickFilter(props) {
108
109
  previousExpandedValue.current = expandedValue;
109
110
  }
110
111
  }, [expandedValue]);
111
- const setQuickFilterValueDebounced = React.useMemo(() => (0, _debounce.default)(newValue => {
112
+
113
+ // Identity-stable, so that a re-render with a new `parser` does not recreate the debounced
114
+ // function below and clear a pending commit. See https://github.com/mui/mui-x/issues/23572.
115
+ const commitQuickFilterValue = (0, _useEventCallback.default)(newValue => {
112
116
  const newQuickFilterValues = parser(newValue);
113
117
  prevQuickFilterValuesRef.current = newQuickFilterValues;
114
118
  apiRef.current.setQuickFilterValues(newQuickFilterValues);
115
- }, debounceMs), [apiRef, debounceMs, parser]);
119
+ });
120
+ const setQuickFilterValueDebounced = React.useMemo(() => (0, _debounce.default)(commitQuickFilterValue, debounceMs), [commitQuickFilterValue, debounceMs]);
116
121
  React.useEffect(() => setQuickFilterValueDebounced.clear, [setQuickFilterValueDebounced]);
117
122
  const handleValueChange = React.useCallback(event => {
118
123
  const newValue = event.target.value;
@@ -7,6 +7,7 @@ import * as React from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import debounce from '@mui/utils/debounce';
9
9
  import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
10
+ import useEventCallback from '@mui/utils/useEventCallback';
10
11
  import useId from '@mui/utils/useId';
11
12
  import { isDeepEqual } from '@mui/x-internals/isDeepEqual';
12
13
  import { useComponentRenderer } from '@mui/x-internals/useComponentRenderer';
@@ -101,11 +102,15 @@ function QuickFilter(props) {
101
102
  previousExpandedValue.current = expandedValue;
102
103
  }
103
104
  }, [expandedValue]);
104
- const setQuickFilterValueDebounced = React.useMemo(() => debounce(newValue => {
105
+
106
+ // Identity-stable, so that a re-render with a new `parser` does not recreate the debounced
107
+ // function below and clear a pending commit. See https://github.com/mui/mui-x/issues/23572.
108
+ const commitQuickFilterValue = useEventCallback(newValue => {
105
109
  const newQuickFilterValues = parser(newValue);
106
110
  prevQuickFilterValuesRef.current = newQuickFilterValues;
107
111
  apiRef.current.setQuickFilterValues(newQuickFilterValues);
108
- }, debounceMs), [apiRef, debounceMs, parser]);
112
+ });
113
+ const setQuickFilterValueDebounced = React.useMemo(() => debounce(commitQuickFilterValue, debounceMs), [commitQuickFilterValue, debounceMs]);
109
114
  React.useEffect(() => setQuickFilterValueDebounced.clear, [setQuickFilterValueDebounced]);
110
115
  const handleValueChange = React.useCallback(event => {
111
116
  const newValue = event.target.value;
@@ -93,7 +93,10 @@ const Viewport = (0, _styles.styled)('div', {
93
93
  display: 'inline-block',
94
94
  position: 'sticky',
95
95
  top: 0,
96
+ // The viewport spans the scrollport exactly, so both 0-insets resolve to the same
97
+ // offset and it stays pinned in LTR and RTL alike.
96
98
  left: 0,
99
+ right: 0,
97
100
  overflow: 'hidden'
98
101
  }
99
102
  });
@@ -85,7 +85,10 @@ const Viewport = styled('div', {
85
85
  display: 'inline-block',
86
86
  position: 'sticky',
87
87
  top: 0,
88
+ // The viewport spans the scrollport exactly, so both 0-insets resolve to the same
89
+ // offset and it stays pinned in LTR and RTL alike.
88
90
  left: 0,
91
+ right: 0,
89
92
  overflow: 'hidden'
90
93
  }
91
94
  });
@@ -464,7 +464,10 @@ const useGridColumnResize = (apiRef, props) => {
464
464
  });
465
465
  const stopListening = React.useCallback(() => {
466
466
  const doc = (0, _ownerDocument.default)(apiRef.current.rootElementRef.current);
467
- doc.body.style.removeProperty('cursor');
467
+ // Also runs as this effect's unmount cleanup, which can execute against a
468
+ // document that no longer has a body. The cursor is only ever set while a
469
+ // resize is in progress, so there is nothing to clear in that case.
470
+ doc.body?.style.removeProperty('cursor');
468
471
  doc.removeEventListener('mousemove', handleResizeMouseMove);
469
472
  doc.removeEventListener('mouseup', handleResizeMouseUp);
470
473
  doc.removeEventListener('touchmove', handleTouchMove);
@@ -456,7 +456,10 @@ export const useGridColumnResize = (apiRef, props) => {
456
456
  });
457
457
  const stopListening = React.useCallback(() => {
458
458
  const doc = ownerDocument(apiRef.current.rootElementRef.current);
459
- doc.body.style.removeProperty('cursor');
459
+ // Also runs as this effect's unmount cleanup, which can execute against a
460
+ // document that no longer has a body. The cursor is only ever set while a
461
+ // resize is in progress, so there is nothing to clear in that case.
462
+ doc.body?.style.removeProperty('cursor');
460
463
  doc.removeEventListener('mousemove', handleResizeMouseMove);
461
464
  doc.removeEventListener('mouseup', handleResizeMouseUp);
462
465
  doc.removeEventListener('touchmove', handleTouchMove);
@@ -83,7 +83,7 @@ const useGridDataSourceBase = (apiRef, props, options = {}) => {
83
83
  // The selector prunes the incomplete items, but a caller passing its own `filterModel`
84
84
  // overrides it, so prune again on the merged params.
85
85
  fetchParams.filterModel = (0, _gridFilterUtils.removeIncompleteFilterItems)(fetchParams.filterModel, (0, _columns.gridColumnLookupSelector)(apiRef));
86
- if (parentId && parentId !== _gridRowsUtils.GRID_ROOT_GROUP_ID && props.signature !== 'DataGrid') {
86
+ if (parentId != null && parentId !== _gridRowsUtils.GRID_ROOT_GROUP_ID && props.signature !== 'DataGrid') {
87
87
  options.fetchRowChildren?.([parentId], [fetchParams], showChildrenLoading);
88
88
  return;
89
89
  }
@@ -76,7 +76,7 @@ export const useGridDataSourceBase = (apiRef, props, options = {}) => {
76
76
  // The selector prunes the incomplete items, but a caller passing its own `filterModel`
77
77
  // overrides it, so prune again on the merged params.
78
78
  fetchParams.filterModel = removeIncompleteFilterItems(fetchParams.filterModel, gridColumnLookupSelector(apiRef));
79
- if (parentId && parentId !== GRID_ROOT_GROUP_ID && props.signature !== 'DataGrid') {
79
+ if (parentId != null && parentId !== GRID_ROOT_GROUP_ID && props.signature !== 'DataGrid') {
80
80
  options.fetchRowChildren?.([parentId], [fetchParams], showChildrenLoading);
81
81
  return;
82
82
  }
@@ -9,4 +9,4 @@ export declare const rowSelectionStateInitializer: GridStateInitializer<Pick<Dat
9
9
  * @requires useGridFocus (state) - can be after
10
10
  * @requires useGridKeyboardNavigation (`cellKeyDown` event must first be consumed by it)
11
11
  */
12
- export declare const useGridRowSelection: (apiRef: RefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, 'checkboxSelection' | 'rowSelectionModel' | 'onRowSelectionModelChange' | 'disableMultipleRowSelection' | 'disableRowSelectionOnClick' | 'disableRowSelectionExcludeModel' | 'isRowSelectable' | 'checkboxSelectionVisibleOnly' | 'pagination' | 'paginationMode' | 'filterMode' | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' | 'rowSelectionPropagation' | 'signature'>) => void;
12
+ export declare const useGridRowSelection: (apiRef: RefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, 'checkboxSelection' | 'rowSelectionModel' | 'onRowSelectionModelChange' | 'disableMultipleRowSelection' | 'disableRowSelectionOnClick' | 'disableRowSelectionExcludeModel' | 'isRowSelectable' | 'checkboxSelectionVisibleOnly' | 'pagination' | 'paginationMode' | 'filterMode' | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' | 'rowSelectionPropagation' | 'dataSource' | 'signature'>) => void;
@@ -9,4 +9,4 @@ export declare const rowSelectionStateInitializer: GridStateInitializer<Pick<Dat
9
9
  * @requires useGridFocus (state) - can be after
10
10
  * @requires useGridKeyboardNavigation (`cellKeyDown` event must first be consumed by it)
11
11
  */
12
- export declare const useGridRowSelection: (apiRef: RefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, 'checkboxSelection' | 'rowSelectionModel' | 'onRowSelectionModelChange' | 'disableMultipleRowSelection' | 'disableRowSelectionOnClick' | 'disableRowSelectionExcludeModel' | 'isRowSelectable' | 'checkboxSelectionVisibleOnly' | 'pagination' | 'paginationMode' | 'filterMode' | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' | 'rowSelectionPropagation' | 'signature'>) => void;
12
+ export declare const useGridRowSelection: (apiRef: RefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, 'checkboxSelection' | 'rowSelectionModel' | 'onRowSelectionModelChange' | 'disableMultipleRowSelection' | 'disableRowSelectionOnClick' | 'disableRowSelectionExcludeModel' | 'isRowSelectable' | 'checkboxSelectionVisibleOnly' | 'pagination' | 'paginationMode' | 'filterMode' | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' | 'rowSelectionPropagation' | 'dataSource' | 'signature'>) => void;
@@ -17,6 +17,7 @@ var _useGridApiMethod = require("../../utils/useGridApiMethod");
17
17
  var _useGridLogger = require("../../utils/useGridLogger");
18
18
  var _useGridSelector = require("../../utils/useGridSelector");
19
19
  var _gridRowsSelector = require("../rows/gridRowsSelector");
20
+ var _gridRowsUtils = require("../rows/gridRowsUtils");
20
21
  var _gridRowSelectionSelector = require("./gridRowSelectionSelector");
21
22
  var _gridFocusStateSelector = require("../focus/gridFocusStateSelector");
22
23
  var _gridFilterSelector = require("../filter/gridFilterSelector");
@@ -127,6 +128,10 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
127
128
  if (props.rowSelection === false) {
128
129
  return false;
129
130
  }
131
+ const rowNode = (0, _gridRowsSelector.gridRowNodeSelector)(apiRef, id);
132
+ if (rowNode?.type === 'footer' || rowNode?.type === 'pinnedRow' || rowNode?.type === 'skeletonRow') {
133
+ return false;
134
+ }
130
135
 
131
136
  // If `keepNonExistentRowsSelected` is true, we might run in a case where row selectability is checked for a row that does not exist.
132
137
  // Since that row was previously selected (otherwise it would not be checked at this point), we return true.
@@ -136,10 +141,6 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
136
141
  if (propIsRowSelectable && !propIsRowSelectable(apiRef.current.getRowParams(id))) {
137
142
  return false;
138
143
  }
139
- const rowNode = (0, _gridRowsSelector.gridRowNodeSelector)(apiRef, id);
140
- if (rowNode?.type === 'footer' || rowNode?.type === 'pinnedRow') {
141
- return false;
142
- }
143
144
  return true;
144
145
  }, [apiRef, props.rowSelection, props.keepNonExistentRowsSelected, propIsRowSelectable]);
145
146
  const getSelectedRows = React.useCallback(() => (0, _gridRowSelectionSelector.gridRowSelectionIdsSelector)(apiRef), [apiRef]);
@@ -315,6 +316,7 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
315
316
  * EVENTS
316
317
  */
317
318
  const isFirstRender = React.useRef(true);
319
+ const previousRowsLookupRef = React.useRef(null);
318
320
  const removeOutdatedSelection = React.useCallback(() => {
319
321
  if (isFirstRender.current) {
320
322
  return;
@@ -323,6 +325,11 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
323
325
  const rowsLookup = (0, _gridRowsSelector.gridRowsLookupSelector)(apiRef);
324
326
  const rowTree = (0, _gridRowsSelector.gridRowTreeSelector)(apiRef);
325
327
  const filteredRowsLookup = (0, _gridFilterSelector.gridFilteredRowsLookupSelector)(apiRef);
328
+ const previousRowsLookup = previousRowsLookupRef.current;
329
+ previousRowsLookupRef.current = rowsLookup;
330
+ // Read the depth at call time. The event that runs this callback can fire in the same
331
+ // tick as the state update that changes the depth, before the render-scope value updates.
332
+ const isNested = (0, _gridRowsSelector.gridRowMaximumTreeDepthSelector)(apiRef) > 1;
326
333
  const isNonExistent = id => {
327
334
  if (props.filterMode === 'server') {
328
335
  return !rowsLookup[id];
@@ -350,6 +357,32 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
350
357
  return node.children.some(childId => isOrHasSelectableDescendant(childId));
351
358
  };
352
359
  let hasChanged = false;
360
+
361
+ // Rows can load after their parent was selected (server-side lazy loading).
362
+ // They arrive unselected, so the parent re-derivation below would drop the parent.
363
+ // With `rowSelectionPropagation.descendants`, the rows that appeared since the last run
364
+ // inherit the selection state of their closest previously known ancestor instead.
365
+ if (isNested && props.rowSelectionPropagation?.descendants && currentSelection.type === 'include' && currentSelection.ids.size > 0 && previousRowsLookup !== null && previousRowsLookup !== rowsLookup) {
366
+ const isNewRow = id => rowsLookup[id] !== undefined && previousRowsLookup[id] === undefined;
367
+ const inheritsSelection = id => {
368
+ const parentId = rowTree[id]?.parent;
369
+ if (parentId == null || parentId === _gridRowsUtils.GRID_ROOT_GROUP_ID) {
370
+ return false;
371
+ }
372
+ if (selectionManager.has(parentId)) {
373
+ return true;
374
+ }
375
+ // A row that appeared together with its parent reads the state further up the chain.
376
+ return isNewRow(parentId) ? inheritsSelection(parentId) : false;
377
+ };
378
+ const dataRowIds = (0, _gridRowsSelector.gridDataRowIdsSelector)(apiRef);
379
+ for (const id of dataRowIds) {
380
+ if (isNewRow(id) && filteredRowsLookup[id] !== false && inheritsSelection(id) && apiRef.current.isRowSelectable(id)) {
381
+ selectionManager.select(id);
382
+ hasChanged = true;
383
+ }
384
+ }
385
+ }
353
386
  for (const id of currentSelection.ids) {
354
387
  if (isNonExistent(id)) {
355
388
  if (props.keepNonExistentRowsSelected) {
@@ -382,9 +415,10 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
382
415
  // For nested data, on row tree updation (filtering, adding rows, etc.) when the selection is
383
416
  // not empty, we need to re-run scanning of the tree to propagate the selection changes
384
417
  // Example: A parent whose de-selected children are filtered out should now be selected
385
- const shouldReapplyPropagation = isNestedData && props.rowSelectionPropagation?.parents && (newSelectionModel.ids.size > 0 ||
386
- // In case of exclude selection, newSelectionModel.ids.size === 0 means all rows are selected
387
- newSelectionModel.type === 'exclude');
418
+ // With a data source, an `exclude` model is left alone: the grid only knows the loaded
419
+ // subset of the rows, so rewriting the model into an `include` of the known rows would
420
+ // drop the "rows that load later are selected too" semantics of the select-all state.
421
+ const shouldReapplyPropagation = isNested && props.rowSelectionPropagation?.parents && (newSelectionModel.type === 'exclude' ? props.dataSource === undefined : newSelectionModel.ids.size > 0);
388
422
  if (hasChanged || shouldReapplyPropagation) {
389
423
  if (shouldReapplyPropagation) {
390
424
  if (newSelectionModel.type === 'exclude') {
@@ -398,13 +432,20 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
398
432
  }
399
433
  apiRef.current.selectRows(selectedRowIds, true, true);
400
434
  } else {
401
- apiRef.current.selectRows(Array.from(newSelectionModel.ids), true, true);
435
+ // `getPropagatedRowSelectionModel` reads the sibling selection state from the model
436
+ // built above, so the rows selected during this run count for the parent derivation.
437
+ // `selectRows` would read the state from before this run instead.
438
+ const propagatedModel = apiRef.current.getPropagatedRowSelectionModel(newSelectionModel);
439
+ const isSelectionValid = propagatedModel.ids.size < 2 || canHaveMultipleSelection;
440
+ if (isSelectionValid && (propagatedModel.type !== currentSelection.type || propagatedModel.ids.size !== currentSelection.ids.size || !Array.from(propagatedModel.ids).every(id => currentSelection.ids.has(id)))) {
441
+ apiRef.current.setRowSelectionModel(propagatedModel, 'multipleRowsSelection');
442
+ }
402
443
  }
403
444
  } else {
404
445
  apiRef.current.setRowSelectionModel(newSelectionModel, 'multipleRowsSelection');
405
446
  }
406
447
  }
407
- }, [apiRef, isNestedData, props.rowSelectionPropagation?.parents, props.keepNonExistentRowsSelected, props.filterMode, getRowsToBeSelected]);
448
+ }, [apiRef, props.rowSelectionPropagation?.parents, props.rowSelectionPropagation?.descendants, props.keepNonExistentRowsSelected, props.filterMode, props.dataSource, canHaveMultipleSelection, getRowsToBeSelected]);
408
449
  const handleSingleRowSelection = React.useCallback((id, event) => {
409
450
  const hasCtrlKey = event.metaKey || event.ctrlKey;
410
451
 
@@ -9,7 +9,8 @@ import { useGridEvent } from "../../utils/useGridEvent.mjs";
9
9
  import { useGridApiMethod } from "../../utils/useGridApiMethod.mjs";
10
10
  import { useGridLogger } from "../../utils/useGridLogger.mjs";
11
11
  import { useGridSelector } from "../../utils/useGridSelector.mjs";
12
- import { gridRowsLookupSelector, gridRowMaximumTreeDepthSelector, gridRowNodeSelector, gridRowTreeSelector } from "../rows/gridRowsSelector.mjs";
12
+ import { gridDataRowIdsSelector, gridRowsLookupSelector, gridRowMaximumTreeDepthSelector, gridRowNodeSelector, gridRowTreeSelector } from "../rows/gridRowsSelector.mjs";
13
+ import { GRID_ROOT_GROUP_ID } from "../rows/gridRowsUtils.mjs";
13
14
  import { gridRowSelectionManagerSelector, gridRowSelectionStateSelector, gridRowSelectionCountSelector, gridRowSelectionIdsSelector } from "./gridRowSelectionSelector.mjs";
14
15
  import { gridFocusCellSelector } from "../focus/gridFocusStateSelector.mjs";
15
16
  import { gridExpandedSortedRowIdsSelector, gridFilteredRowsLookupSelector, gridFilterModelSelector, gridQuickFilterValuesSelector } from "../filter/gridFilterSelector.mjs";
@@ -119,6 +120,10 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
119
120
  if (props.rowSelection === false) {
120
121
  return false;
121
122
  }
123
+ const rowNode = gridRowNodeSelector(apiRef, id);
124
+ if (rowNode?.type === 'footer' || rowNode?.type === 'pinnedRow' || rowNode?.type === 'skeletonRow') {
125
+ return false;
126
+ }
122
127
 
123
128
  // If `keepNonExistentRowsSelected` is true, we might run in a case where row selectability is checked for a row that does not exist.
124
129
  // Since that row was previously selected (otherwise it would not be checked at this point), we return true.
@@ -128,10 +133,6 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
128
133
  if (propIsRowSelectable && !propIsRowSelectable(apiRef.current.getRowParams(id))) {
129
134
  return false;
130
135
  }
131
- const rowNode = gridRowNodeSelector(apiRef, id);
132
- if (rowNode?.type === 'footer' || rowNode?.type === 'pinnedRow') {
133
- return false;
134
- }
135
136
  return true;
136
137
  }, [apiRef, props.rowSelection, props.keepNonExistentRowsSelected, propIsRowSelectable]);
137
138
  const getSelectedRows = React.useCallback(() => gridRowSelectionIdsSelector(apiRef), [apiRef]);
@@ -307,6 +308,7 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
307
308
  * EVENTS
308
309
  */
309
310
  const isFirstRender = React.useRef(true);
311
+ const previousRowsLookupRef = React.useRef(null);
310
312
  const removeOutdatedSelection = React.useCallback(() => {
311
313
  if (isFirstRender.current) {
312
314
  return;
@@ -315,6 +317,11 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
315
317
  const rowsLookup = gridRowsLookupSelector(apiRef);
316
318
  const rowTree = gridRowTreeSelector(apiRef);
317
319
  const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef);
320
+ const previousRowsLookup = previousRowsLookupRef.current;
321
+ previousRowsLookupRef.current = rowsLookup;
322
+ // Read the depth at call time. The event that runs this callback can fire in the same
323
+ // tick as the state update that changes the depth, before the render-scope value updates.
324
+ const isNested = gridRowMaximumTreeDepthSelector(apiRef) > 1;
318
325
  const isNonExistent = id => {
319
326
  if (props.filterMode === 'server') {
320
327
  return !rowsLookup[id];
@@ -342,6 +349,32 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
342
349
  return node.children.some(childId => isOrHasSelectableDescendant(childId));
343
350
  };
344
351
  let hasChanged = false;
352
+
353
+ // Rows can load after their parent was selected (server-side lazy loading).
354
+ // They arrive unselected, so the parent re-derivation below would drop the parent.
355
+ // With `rowSelectionPropagation.descendants`, the rows that appeared since the last run
356
+ // inherit the selection state of their closest previously known ancestor instead.
357
+ if (isNested && props.rowSelectionPropagation?.descendants && currentSelection.type === 'include' && currentSelection.ids.size > 0 && previousRowsLookup !== null && previousRowsLookup !== rowsLookup) {
358
+ const isNewRow = id => rowsLookup[id] !== undefined && previousRowsLookup[id] === undefined;
359
+ const inheritsSelection = id => {
360
+ const parentId = rowTree[id]?.parent;
361
+ if (parentId == null || parentId === GRID_ROOT_GROUP_ID) {
362
+ return false;
363
+ }
364
+ if (selectionManager.has(parentId)) {
365
+ return true;
366
+ }
367
+ // A row that appeared together with its parent reads the state further up the chain.
368
+ return isNewRow(parentId) ? inheritsSelection(parentId) : false;
369
+ };
370
+ const dataRowIds = gridDataRowIdsSelector(apiRef);
371
+ for (const id of dataRowIds) {
372
+ if (isNewRow(id) && filteredRowsLookup[id] !== false && inheritsSelection(id) && apiRef.current.isRowSelectable(id)) {
373
+ selectionManager.select(id);
374
+ hasChanged = true;
375
+ }
376
+ }
377
+ }
345
378
  for (const id of currentSelection.ids) {
346
379
  if (isNonExistent(id)) {
347
380
  if (props.keepNonExistentRowsSelected) {
@@ -374,9 +407,10 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
374
407
  // For nested data, on row tree updation (filtering, adding rows, etc.) when the selection is
375
408
  // not empty, we need to re-run scanning of the tree to propagate the selection changes
376
409
  // Example: A parent whose de-selected children are filtered out should now be selected
377
- const shouldReapplyPropagation = isNestedData && props.rowSelectionPropagation?.parents && (newSelectionModel.ids.size > 0 ||
378
- // In case of exclude selection, newSelectionModel.ids.size === 0 means all rows are selected
379
- newSelectionModel.type === 'exclude');
410
+ // With a data source, an `exclude` model is left alone: the grid only knows the loaded
411
+ // subset of the rows, so rewriting the model into an `include` of the known rows would
412
+ // drop the "rows that load later are selected too" semantics of the select-all state.
413
+ const shouldReapplyPropagation = isNested && props.rowSelectionPropagation?.parents && (newSelectionModel.type === 'exclude' ? props.dataSource === undefined : newSelectionModel.ids.size > 0);
380
414
  if (hasChanged || shouldReapplyPropagation) {
381
415
  if (shouldReapplyPropagation) {
382
416
  if (newSelectionModel.type === 'exclude') {
@@ -390,13 +424,20 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
390
424
  }
391
425
  apiRef.current.selectRows(selectedRowIds, true, true);
392
426
  } else {
393
- apiRef.current.selectRows(Array.from(newSelectionModel.ids), true, true);
427
+ // `getPropagatedRowSelectionModel` reads the sibling selection state from the model
428
+ // built above, so the rows selected during this run count for the parent derivation.
429
+ // `selectRows` would read the state from before this run instead.
430
+ const propagatedModel = apiRef.current.getPropagatedRowSelectionModel(newSelectionModel);
431
+ const isSelectionValid = propagatedModel.ids.size < 2 || canHaveMultipleSelection;
432
+ if (isSelectionValid && (propagatedModel.type !== currentSelection.type || propagatedModel.ids.size !== currentSelection.ids.size || !Array.from(propagatedModel.ids).every(id => currentSelection.ids.has(id)))) {
433
+ apiRef.current.setRowSelectionModel(propagatedModel, 'multipleRowsSelection');
434
+ }
394
435
  }
395
436
  } else {
396
437
  apiRef.current.setRowSelectionModel(newSelectionModel, 'multipleRowsSelection');
397
438
  }
398
439
  }
399
- }, [apiRef, isNestedData, props.rowSelectionPropagation?.parents, props.keepNonExistentRowsSelected, props.filterMode, getRowsToBeSelected]);
440
+ }, [apiRef, props.rowSelectionPropagation?.parents, props.rowSelectionPropagation?.descendants, props.keepNonExistentRowsSelected, props.filterMode, props.dataSource, canHaveMultipleSelection, getRowsToBeSelected]);
400
441
  const handleSingleRowSelection = React.useCallback((id, event) => {
401
442
  const hasCtrlKey = event.metaKey || event.ctrlKey;
402
443
 
@@ -67,6 +67,11 @@ const checkboxPropsSelector = exports.checkboxPropsSelector = (0, _createSelecto
67
67
  return;
68
68
  }
69
69
  const node = rowTree[itemToTraverseId];
70
+ // Skeleton rows are placeholders for rows that are not loaded yet.
71
+ // They have no selection state, so they do not affect the parent checkbox state.
72
+ if (node?.type === 'skeletonRow') {
73
+ return;
74
+ }
70
75
  if (node?.type === 'group') {
71
76
  node.children.forEach(traverseDescendants);
72
77
  }
@@ -60,6 +60,11 @@ export const checkboxPropsSelector = createSelector(gridColumnDefinitionsSelecto
60
60
  return;
61
61
  }
62
62
  const node = rowTree[itemToTraverseId];
63
+ // Skeleton rows are placeholders for rows that are not loaded yet.
64
+ // They have no selection state, so they do not affect the parent checkbox state.
65
+ if (node?.type === 'skeletonRow') {
66
+ return;
67
+ }
63
68
  if (node?.type === 'group') {
64
69
  node.children.forEach(traverseDescendants);
65
70
  }
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v9.13.0
2
+ * @mui/x-data-grid v9.14.0
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
package/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v9.13.0
2
+ * @mui/x-data-grid v9.14.0
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/x-data-grid",
3
- "version": "9.13.0",
3
+ "version": "9.14.0",
4
4
  "author": "MUI Team",
5
5
  "description": "The Community plan edition of the MUI X Data Grid components.",
6
6
  "license": "MIT",
@@ -40,11 +40,11 @@
40
40
  "@babel/runtime": "^7.29.7",
41
41
  "@base-ui/utils": "^0.3.2",
42
42
  "@mui/utils": "^9.4.0",
43
- "@mui/x-internals": "^9.13.0",
44
- "@mui/x-virtualizer": "0.7.1",
43
+ "@mui/x-internals": "^9.14.0",
44
+ "@mui/x-virtualizer": "0.7.2",
45
45
  "clsx": "^2.1.1",
46
46
  "prop-types": "^15.8.1",
47
- "use-sync-external-store": "^1.6.0"
47
+ "use-sync-external-store": "^1.7.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@emotion/react": "^11.9.0",