@mui/x-data-grid 7.29.11 → 7.29.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,70 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## 7.29.13
7
+
8
+ _Apr 27, 2026_
9
+
10
+ We'd like to extend a big thank you to the 4 contributors who made this release possible. Here are some highlights ✨:
11
+
12
+ - 🐞 Bugfixes
13
+
14
+ The following are all team members who have contributed to this release:
15
+ @arminmeh, @dav-is, @LukasTy, @michelengelen
16
+
17
+ ### Data Grid
18
+
19
+ #### `@mui/x-data-grid@7.29.13`
20
+
21
+ - [DataGrid] Prevent repeated `hasScrollbar` state updates (#21849) @arminmeh
22
+
23
+ #### `@mui/x-data-grid-pro@7.29.13` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link "Pro plan")
24
+
25
+ Same changes as in `@mui/x-data-grid@7.29.13`.
26
+
27
+ #### `@mui/x-data-grid-premium@7.29.13` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link "Premium plan")
28
+
29
+ Same changes as in `@mui/x-data-grid-pro@7.29.13`.
30
+
31
+ ### Docs
32
+
33
+ - [docs] Add `v9` as root path link and move `v8` under subpath (#22037) @LukasTy
34
+ - [docs] Add check for auto-generated group rows in `renderCountry` (#22143) @michelengelen
35
+ - [docs] Remove obsolete v7 deprecation warning for `dayOfWeekFormatter` (@LukasTy) (#22121)
36
+
37
+ ### Miscellaneous
38
+
39
+ - [docs-infra] Set `SEARCH_INDEX` Env for v7 (#21876) @dav-is
40
+
41
+ ## 7.29.12
42
+
43
+ _Nov 26, 2025_
44
+
45
+ We'd like to extend a big thank you to the 2 contributors who made this release possible. Here are some highlights ✨:
46
+
47
+ - 🐞 Bugfixes
48
+
49
+ Special thanks go out to the community members for their valuable contributions:
50
+ @m2mathew
51
+
52
+ Following are all team members who have contributed to this release:
53
+ @arminmeh
54
+
55
+ ### Data Grid
56
+
57
+ #### `@mui/x-data-grid@7.29.12`
58
+
59
+ - [DataGrid] Avoid automatic scroll back to the focused element after it leaves the viewport (#20417) @arminmeh
60
+ - [DataGrid] Fix missing rows in the print export window (#20157) @m2mathew
61
+
62
+ #### `@mui/x-data-grid-pro@7.29.12` [![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-data-grid@7.29.12`.
65
+
66
+ #### `@mui/x-data-grid-premium@7.29.12` [![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-data-grid-pro@7.29.12`.
69
+
6
70
  ## 7.29.11
7
71
 
8
72
  _Nov 19, 2025_
@@ -2,6 +2,7 @@ import _extends from "@babel/runtime/helpers/esm/extends";
2
2
  import * as React from 'react';
3
3
  import { unstable_useId as useId, unstable_composeClasses as composeClasses } from '@mui/utils';
4
4
  import { useRtl } from '@mui/system/RtlProvider';
5
+ import { doesSupportPreventScroll } from "../../utils/doesSupportPreventScroll.js";
5
6
  import { getDataGridUtilityClass } from "../../constants/gridClasses.js";
6
7
  import { useGridRootProps } from "../../hooks/utils/useGridRootProps.js";
7
8
  import { gridColumnGroupsLookupSelector } from "../../hooks/features/columnGrouping/gridColumnGroupsSelector.js";
@@ -86,7 +87,18 @@ function GridColumnGroupHeader(props) {
86
87
  if (hasFocus) {
87
88
  const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
88
89
  const elementToFocus = focusableElement || headerCellRef.current;
89
- elementToFocus?.focus();
90
+ if (!elementToFocus) {
91
+ return;
92
+ }
93
+ if (doesSupportPreventScroll()) {
94
+ elementToFocus.focus({
95
+ preventScroll: true
96
+ });
97
+ } else {
98
+ const scrollPosition = apiRef.current.getScrollPosition();
99
+ elementToFocus.focus();
100
+ apiRef.current.scroll(scrollPosition);
101
+ }
90
102
  }
91
103
  }, [apiRef, hasFocus]);
92
104
  const publish = React.useCallback(eventName => event => {
@@ -5,6 +5,7 @@ import clsx from 'clsx';
5
5
  import { unstable_composeClasses as composeClasses, unstable_useId as useId } from '@mui/utils';
6
6
  import { fastMemo } from '@mui/x-internals/fastMemo';
7
7
  import { useRtl } from '@mui/system/RtlProvider';
8
+ import { doesSupportPreventScroll } from "../../utils/doesSupportPreventScroll.js";
8
9
  import { useGridPrivateApiContext } from "../../hooks/utils/useGridPrivateApiContext.js";
9
10
  import { ColumnHeaderMenuIcon } from "./ColumnHeaderMenuIcon.js";
10
11
  import { GridColumnHeaderMenu } from "../menu/columnMenu/GridColumnHeaderMenu.js";
@@ -160,9 +161,17 @@ function GridColumnHeaderItem(props) {
160
161
  if (hasFocus && !columnMenuState.open) {
161
162
  const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
162
163
  const elementToFocus = focusableElement || headerCellRef.current;
163
- elementToFocus?.focus();
164
- if (apiRef.current.columnHeadersContainerRef?.current) {
165
- apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0;
164
+ if (!elementToFocus) {
165
+ return;
166
+ }
167
+ if (doesSupportPreventScroll()) {
168
+ elementToFocus.focus({
169
+ preventScroll: true
170
+ });
171
+ } else {
172
+ const scrollPosition = apiRef.current.getScrollPosition();
173
+ elementToFocus.focus();
174
+ apiRef.current.scroll(scrollPosition);
166
175
  }
167
176
  }
168
177
  }, [apiRef, hasFocus]);
@@ -5,7 +5,6 @@ import * as React from 'react';
5
5
  import clsx from 'clsx';
6
6
  import { unstable_useForkRef as useForkRef } from '@mui/utils';
7
7
  import { forwardRef } from '@mui/x-internals/forwardRef';
8
- import { useGridPrivateApiContext } from "../../hooks/utils/useGridPrivateApiContext.js";
9
8
  import { GridColumnHeaderTitle } from "./GridColumnHeaderTitle.js";
10
9
  import { GridColumnHeaderSeparator } from "./GridColumnHeaderSeparator.js";
11
10
  import { useGridRootProps } from "../../hooks/utils/useGridRootProps.js";
@@ -17,7 +16,6 @@ const GridGenericColumnHeaderItem = forwardRef(function GridGenericColumnHeaderI
17
16
  height,
18
17
  isResizing,
19
18
  sortDirection,
20
- hasFocus,
21
19
  tabIndex,
22
20
  separatorSide,
23
21
  isDraggable,
@@ -35,7 +33,6 @@ const GridGenericColumnHeaderItem = forwardRef(function GridGenericColumnHeaderI
35
33
  style
36
34
  } = props,
37
35
  other = _objectWithoutPropertiesLoose(props, _excluded);
38
- const apiRef = useGridPrivateApiContext();
39
36
  const rootProps = useGridRootProps();
40
37
  const headerCellRef = React.useRef(null);
41
38
  const handleRef = useForkRef(headerCellRef, ref);
@@ -43,17 +40,6 @@ const GridGenericColumnHeaderItem = forwardRef(function GridGenericColumnHeaderI
43
40
  if (sortDirection != null) {
44
41
  ariaSort = sortDirection === 'asc' ? 'ascending' : 'descending';
45
42
  }
46
- React.useLayoutEffect(() => {
47
- const columnMenuState = apiRef.current.state.columnMenu;
48
- if (hasFocus && !columnMenuState.open) {
49
- const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
50
- const elementToFocus = focusableElement || headerCellRef.current;
51
- elementToFocus?.focus();
52
- if (apiRef.current.columnHeadersContainerRef?.current) {
53
- apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0;
54
- }
55
- }
56
- }, [apiRef, hasFocus]);
57
43
  return /*#__PURE__*/_jsxs("div", _extends({
58
44
  className: clsx(classes.root, headerClassName),
59
45
  style: _extends({}, style, {
@@ -67,6 +67,21 @@ export function useGridDimensions(apiRef, props) {
67
67
  const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector);
68
68
  const columnsTotalWidth = useGridSelector(apiRef, columnsTotalWidthSelector);
69
69
  const isFirstSizing = React.useRef(true);
70
+
71
+ // Vertical scrollbar oscillation detector.
72
+ // Counts consecutive hasScrollY flips that happen with no row-height change.
73
+ // After 2 flips it is certainly a layout feedback loop, so every further flip
74
+ // is forced to false (no scrollbar). The counter resets when row heights change.
75
+ // Only vertical scrollbar can oscillate because column widths are never 'auto'.
76
+ // https://github.com/mui/mui-x/issues/20539
77
+ const scrollYOscillation = React.useRef({
78
+ counter: 0,
79
+ heights: {
80
+ content: 0,
81
+ pinnedTop: 0,
82
+ pinnedBottom: 0
83
+ }
84
+ });
70
85
  const {
71
86
  rowHeight,
72
87
  headerHeight,
@@ -134,6 +149,7 @@ export function useGridDimensions(apiRef, props) {
134
149
  width: nonPinnedColumnsTotalWidth,
135
150
  height: roundToDecimalPlaces(rowsMeta.currentPageTotalHeight, 1)
136
151
  };
152
+ const prevDimensions = apiRef.current.state.dimensions;
137
153
  let viewportOuterSize;
138
154
  let viewportInnerSize;
139
155
  let hasScrollX = false;
@@ -171,6 +187,36 @@ export function useGridDimensions(apiRef, props) {
171
187
  hasScrollY = content.height + scrollbarSize > container.height;
172
188
  }
173
189
  }
190
+
191
+ // Detect vertical scrollbar oscillation.
192
+ // Track consecutive hasScrollY flips with no row-height change.
193
+ // Once confirmed (≥ 2 flips), force hasScrollY off — the scrollbar is
194
+ // not genuinely needed, it is a layout feedback loop caused by stale
195
+ // rootSize or the horizontal scrollbar's height cascading.
196
+ {
197
+ const osc = scrollYOscillation.current;
198
+ const heightsChanged = rowsMeta.currentPageTotalHeight !== osc.heights.content || rowsMeta.pinnedTopRowsTotalHeight !== osc.heights.pinnedTop || rowsMeta.pinnedBottomRowsTotalHeight !== osc.heights.pinnedBottom;
199
+ if (heightsChanged) {
200
+ osc.counter = 0;
201
+ osc.heights = {
202
+ content: rowsMeta.currentPageTotalHeight,
203
+ pinnedTop: rowsMeta.pinnedTopRowsTotalHeight,
204
+ pinnedBottom: rowsMeta.pinnedBottomRowsTotalHeight
205
+ };
206
+ }
207
+ if (prevDimensions.isReady && hasScrollY !== prevDimensions.hasScrollY) {
208
+ if (!heightsChanged) {
209
+ osc.counter += 1;
210
+ }
211
+ if (osc.counter >= 2) {
212
+ hasScrollY = false;
213
+ // Recompute hasScrollX without the vertical scrollbar's width impact,
214
+ // otherwise the cascade (hasScrollY → narrower viewport → hasScrollX)
215
+ // keeps the horizontal scrollbar/filler alive and the root keeps resizing.
216
+ hasScrollX = hasScrollXIfNoYScrollBar;
217
+ }
218
+ }
219
+ }
174
220
  if (hasScrollY) {
175
221
  viewportInnerSize.width -= scrollbarSize;
176
222
  }
@@ -205,7 +251,6 @@ export function useGridDimensions(apiRef, props) {
205
251
  topContainerHeight,
206
252
  bottomContainerHeight
207
253
  };
208
- const prevDimensions = apiRef.current.state.dimensions;
209
254
  if (isDeepEqual(prevDimensions, newDimensions)) {
210
255
  return;
211
256
  }
@@ -201,7 +201,7 @@ export const useGridPrintExport = (apiRef, props) => {
201
201
  // Revert grid to previous state
202
202
  apiRef.current.restoreState(previousGridState.current || {});
203
203
  if (!previousGridState.current?.columns?.columnVisibilityModel) {
204
- // if the apiRef.current.exportState(); did not exported the column visibility, we update it
204
+ // if the apiRef.current.exportState(); did not export the column visibility, we update it
205
205
  apiRef.current.setColumnVisibilityModel(previousColumnVisibility.current);
206
206
  }
207
207
  apiRef.current.setState(state => _extends({}, state, {
@@ -238,12 +238,7 @@ export const useGridPrintExport = (apiRef, props) => {
238
238
  }));
239
239
  }
240
240
  previousVirtualizationState.current = apiRef.current.state.virtualization;
241
- apiRef.current.setState(state => _extends({}, state, {
242
- virtualization: _extends({}, state.virtualization, {
243
- enabled: false,
244
- enabledForColumns: false
245
- })
246
- }));
241
+ apiRef.current.unstable_setVirtualization(false);
247
242
  await updateGridColumnsForPrint(options?.fields, options?.allColumns, options?.includeCheckboxes);
248
243
  updateGridRowsForPrint(options?.getRowsToExport ?? defaultGetRowsToExport);
249
244
  await raf(); // wait for the state changes to take action
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v7.29.11
2
+ * @mui/x-data-grid v7.29.13
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -2,6 +2,7 @@ import _extends from "@babel/runtime/helpers/esm/extends";
2
2
  import * as React from 'react';
3
3
  import { unstable_useId as useId, unstable_composeClasses as composeClasses } from '@mui/utils';
4
4
  import { useRtl } from '@mui/system/RtlProvider';
5
+ import { doesSupportPreventScroll } from "../../utils/doesSupportPreventScroll.js";
5
6
  import { getDataGridUtilityClass } from "../../constants/gridClasses.js";
6
7
  import { useGridRootProps } from "../../hooks/utils/useGridRootProps.js";
7
8
  import { gridColumnGroupsLookupSelector } from "../../hooks/features/columnGrouping/gridColumnGroupsSelector.js";
@@ -86,7 +87,18 @@ function GridColumnGroupHeader(props) {
86
87
  if (hasFocus) {
87
88
  const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
88
89
  const elementToFocus = focusableElement || headerCellRef.current;
89
- elementToFocus?.focus();
90
+ if (!elementToFocus) {
91
+ return;
92
+ }
93
+ if (doesSupportPreventScroll()) {
94
+ elementToFocus.focus({
95
+ preventScroll: true
96
+ });
97
+ } else {
98
+ const scrollPosition = apiRef.current.getScrollPosition();
99
+ elementToFocus.focus();
100
+ apiRef.current.scroll(scrollPosition);
101
+ }
90
102
  }
91
103
  }, [apiRef, hasFocus]);
92
104
  const publish = React.useCallback(eventName => event => {
@@ -5,6 +5,7 @@ import clsx from 'clsx';
5
5
  import { unstable_composeClasses as composeClasses, unstable_useId as useId } from '@mui/utils';
6
6
  import { fastMemo } from '@mui/x-internals/fastMemo';
7
7
  import { useRtl } from '@mui/system/RtlProvider';
8
+ import { doesSupportPreventScroll } from "../../utils/doesSupportPreventScroll.js";
8
9
  import { useGridPrivateApiContext } from "../../hooks/utils/useGridPrivateApiContext.js";
9
10
  import { ColumnHeaderMenuIcon } from "./ColumnHeaderMenuIcon.js";
10
11
  import { GridColumnHeaderMenu } from "../menu/columnMenu/GridColumnHeaderMenu.js";
@@ -160,9 +161,17 @@ function GridColumnHeaderItem(props) {
160
161
  if (hasFocus && !columnMenuState.open) {
161
162
  const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
162
163
  const elementToFocus = focusableElement || headerCellRef.current;
163
- elementToFocus?.focus();
164
- if (apiRef.current.columnHeadersContainerRef?.current) {
165
- apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0;
164
+ if (!elementToFocus) {
165
+ return;
166
+ }
167
+ if (doesSupportPreventScroll()) {
168
+ elementToFocus.focus({
169
+ preventScroll: true
170
+ });
171
+ } else {
172
+ const scrollPosition = apiRef.current.getScrollPosition();
173
+ elementToFocus.focus();
174
+ apiRef.current.scroll(scrollPosition);
166
175
  }
167
176
  }
168
177
  }, [apiRef, hasFocus]);
@@ -5,7 +5,6 @@ import * as React from 'react';
5
5
  import clsx from 'clsx';
6
6
  import { unstable_useForkRef as useForkRef } from '@mui/utils';
7
7
  import { forwardRef } from '@mui/x-internals/forwardRef';
8
- import { useGridPrivateApiContext } from "../../hooks/utils/useGridPrivateApiContext.js";
9
8
  import { GridColumnHeaderTitle } from "./GridColumnHeaderTitle.js";
10
9
  import { GridColumnHeaderSeparator } from "./GridColumnHeaderSeparator.js";
11
10
  import { useGridRootProps } from "../../hooks/utils/useGridRootProps.js";
@@ -17,7 +16,6 @@ const GridGenericColumnHeaderItem = forwardRef(function GridGenericColumnHeaderI
17
16
  height,
18
17
  isResizing,
19
18
  sortDirection,
20
- hasFocus,
21
19
  tabIndex,
22
20
  separatorSide,
23
21
  isDraggable,
@@ -35,7 +33,6 @@ const GridGenericColumnHeaderItem = forwardRef(function GridGenericColumnHeaderI
35
33
  style
36
34
  } = props,
37
35
  other = _objectWithoutPropertiesLoose(props, _excluded);
38
- const apiRef = useGridPrivateApiContext();
39
36
  const rootProps = useGridRootProps();
40
37
  const headerCellRef = React.useRef(null);
41
38
  const handleRef = useForkRef(headerCellRef, ref);
@@ -43,17 +40,6 @@ const GridGenericColumnHeaderItem = forwardRef(function GridGenericColumnHeaderI
43
40
  if (sortDirection != null) {
44
41
  ariaSort = sortDirection === 'asc' ? 'ascending' : 'descending';
45
42
  }
46
- React.useLayoutEffect(() => {
47
- const columnMenuState = apiRef.current.state.columnMenu;
48
- if (hasFocus && !columnMenuState.open) {
49
- const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
50
- const elementToFocus = focusableElement || headerCellRef.current;
51
- elementToFocus?.focus();
52
- if (apiRef.current.columnHeadersContainerRef?.current) {
53
- apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0;
54
- }
55
- }
56
- }, [apiRef, hasFocus]);
57
43
  return /*#__PURE__*/_jsxs("div", _extends({
58
44
  className: clsx(classes.root, headerClassName),
59
45
  style: _extends({}, style, {
@@ -67,6 +67,21 @@ export function useGridDimensions(apiRef, props) {
67
67
  const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector);
68
68
  const columnsTotalWidth = useGridSelector(apiRef, columnsTotalWidthSelector);
69
69
  const isFirstSizing = React.useRef(true);
70
+
71
+ // Vertical scrollbar oscillation detector.
72
+ // Counts consecutive hasScrollY flips that happen with no row-height change.
73
+ // After 2 flips it is certainly a layout feedback loop, so every further flip
74
+ // is forced to false (no scrollbar). The counter resets when row heights change.
75
+ // Only vertical scrollbar can oscillate because column widths are never 'auto'.
76
+ // https://github.com/mui/mui-x/issues/20539
77
+ const scrollYOscillation = React.useRef({
78
+ counter: 0,
79
+ heights: {
80
+ content: 0,
81
+ pinnedTop: 0,
82
+ pinnedBottom: 0
83
+ }
84
+ });
70
85
  const {
71
86
  rowHeight,
72
87
  headerHeight,
@@ -134,6 +149,7 @@ export function useGridDimensions(apiRef, props) {
134
149
  width: nonPinnedColumnsTotalWidth,
135
150
  height: roundToDecimalPlaces(rowsMeta.currentPageTotalHeight, 1)
136
151
  };
152
+ const prevDimensions = apiRef.current.state.dimensions;
137
153
  let viewportOuterSize;
138
154
  let viewportInnerSize;
139
155
  let hasScrollX = false;
@@ -171,6 +187,36 @@ export function useGridDimensions(apiRef, props) {
171
187
  hasScrollY = content.height + scrollbarSize > container.height;
172
188
  }
173
189
  }
190
+
191
+ // Detect vertical scrollbar oscillation.
192
+ // Track consecutive hasScrollY flips with no row-height change.
193
+ // Once confirmed (≥ 2 flips), force hasScrollY off — the scrollbar is
194
+ // not genuinely needed, it is a layout feedback loop caused by stale
195
+ // rootSize or the horizontal scrollbar's height cascading.
196
+ {
197
+ const osc = scrollYOscillation.current;
198
+ const heightsChanged = rowsMeta.currentPageTotalHeight !== osc.heights.content || rowsMeta.pinnedTopRowsTotalHeight !== osc.heights.pinnedTop || rowsMeta.pinnedBottomRowsTotalHeight !== osc.heights.pinnedBottom;
199
+ if (heightsChanged) {
200
+ osc.counter = 0;
201
+ osc.heights = {
202
+ content: rowsMeta.currentPageTotalHeight,
203
+ pinnedTop: rowsMeta.pinnedTopRowsTotalHeight,
204
+ pinnedBottom: rowsMeta.pinnedBottomRowsTotalHeight
205
+ };
206
+ }
207
+ if (prevDimensions.isReady && hasScrollY !== prevDimensions.hasScrollY) {
208
+ if (!heightsChanged) {
209
+ osc.counter += 1;
210
+ }
211
+ if (osc.counter >= 2) {
212
+ hasScrollY = false;
213
+ // Recompute hasScrollX without the vertical scrollbar's width impact,
214
+ // otherwise the cascade (hasScrollY → narrower viewport → hasScrollX)
215
+ // keeps the horizontal scrollbar/filler alive and the root keeps resizing.
216
+ hasScrollX = hasScrollXIfNoYScrollBar;
217
+ }
218
+ }
219
+ }
174
220
  if (hasScrollY) {
175
221
  viewportInnerSize.width -= scrollbarSize;
176
222
  }
@@ -205,7 +251,6 @@ export function useGridDimensions(apiRef, props) {
205
251
  topContainerHeight,
206
252
  bottomContainerHeight
207
253
  };
208
- const prevDimensions = apiRef.current.state.dimensions;
209
254
  if (isDeepEqual(prevDimensions, newDimensions)) {
210
255
  return;
211
256
  }
@@ -201,7 +201,7 @@ export const useGridPrintExport = (apiRef, props) => {
201
201
  // Revert grid to previous state
202
202
  apiRef.current.restoreState(previousGridState.current || {});
203
203
  if (!previousGridState.current?.columns?.columnVisibilityModel) {
204
- // if the apiRef.current.exportState(); did not exported the column visibility, we update it
204
+ // if the apiRef.current.exportState(); did not export the column visibility, we update it
205
205
  apiRef.current.setColumnVisibilityModel(previousColumnVisibility.current);
206
206
  }
207
207
  apiRef.current.setState(state => _extends({}, state, {
@@ -238,12 +238,7 @@ export const useGridPrintExport = (apiRef, props) => {
238
238
  }));
239
239
  }
240
240
  previousVirtualizationState.current = apiRef.current.state.virtualization;
241
- apiRef.current.setState(state => _extends({}, state, {
242
- virtualization: _extends({}, state.virtualization, {
243
- enabled: false,
244
- enabledForColumns: false
245
- })
246
- }));
241
+ apiRef.current.unstable_setVirtualization(false);
247
242
  await updateGridColumnsForPrint(options?.fields, options?.allColumns, options?.includeCheckboxes);
248
243
  updateGridRowsForPrint(options?.getRowsToExport ?? defaultGetRowsToExport);
249
244
  await raf(); // wait for the state changes to take action
package/modern/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v7.29.11
2
+ * @mui/x-data-grid v7.29.13
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -10,6 +10,7 @@ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")
10
10
  var React = _interopRequireWildcard(require("react"));
11
11
  var _utils = require("@mui/utils");
12
12
  var _RtlProvider = require("@mui/system/RtlProvider");
13
+ var _doesSupportPreventScroll = require("../../utils/doesSupportPreventScroll");
13
14
  var _gridClasses = require("../../constants/gridClasses");
14
15
  var _useGridRootProps = require("../../hooks/utils/useGridRootProps");
15
16
  var _gridColumnGroupsSelector = require("../../hooks/features/columnGrouping/gridColumnGroupsSelector");
@@ -94,7 +95,18 @@ function GridColumnGroupHeader(props) {
94
95
  if (hasFocus) {
95
96
  const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
96
97
  const elementToFocus = focusableElement || headerCellRef.current;
97
- elementToFocus?.focus();
98
+ if (!elementToFocus) {
99
+ return;
100
+ }
101
+ if ((0, _doesSupportPreventScroll.doesSupportPreventScroll)()) {
102
+ elementToFocus.focus({
103
+ preventScroll: true
104
+ });
105
+ } else {
106
+ const scrollPosition = apiRef.current.getScrollPosition();
107
+ elementToFocus.focus();
108
+ apiRef.current.scroll(scrollPosition);
109
+ }
98
110
  }
99
111
  }, [apiRef, hasFocus]);
100
112
  const publish = React.useCallback(eventName => event => {
@@ -13,6 +13,7 @@ var _clsx = _interopRequireDefault(require("clsx"));
13
13
  var _utils = require("@mui/utils");
14
14
  var _fastMemo = require("@mui/x-internals/fastMemo");
15
15
  var _RtlProvider = require("@mui/system/RtlProvider");
16
+ var _doesSupportPreventScroll = require("../../utils/doesSupportPreventScroll");
16
17
  var _useGridPrivateApiContext = require("../../hooks/utils/useGridPrivateApiContext");
17
18
  var _ColumnHeaderMenuIcon = require("./ColumnHeaderMenuIcon");
18
19
  var _GridColumnHeaderMenu = require("../menu/columnMenu/GridColumnHeaderMenu");
@@ -168,9 +169,17 @@ function GridColumnHeaderItem(props) {
168
169
  if (hasFocus && !columnMenuState.open) {
169
170
  const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
170
171
  const elementToFocus = focusableElement || headerCellRef.current;
171
- elementToFocus?.focus();
172
- if (apiRef.current.columnHeadersContainerRef?.current) {
173
- apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0;
172
+ if (!elementToFocus) {
173
+ return;
174
+ }
175
+ if ((0, _doesSupportPreventScroll.doesSupportPreventScroll)()) {
176
+ elementToFocus.focus({
177
+ preventScroll: true
178
+ });
179
+ } else {
180
+ const scrollPosition = apiRef.current.getScrollPosition();
181
+ elementToFocus.focus();
182
+ apiRef.current.scroll(scrollPosition);
174
183
  }
175
184
  }
176
185
  }, [apiRef, hasFocus]);
@@ -12,7 +12,6 @@ var React = _interopRequireWildcard(require("react"));
12
12
  var _clsx = _interopRequireDefault(require("clsx"));
13
13
  var _utils = require("@mui/utils");
14
14
  var _forwardRef = require("@mui/x-internals/forwardRef");
15
- var _useGridPrivateApiContext = require("../../hooks/utils/useGridPrivateApiContext");
16
15
  var _GridColumnHeaderTitle = require("./GridColumnHeaderTitle");
17
16
  var _GridColumnHeaderSeparator = require("./GridColumnHeaderSeparator");
18
17
  var _useGridRootProps = require("../../hooks/utils/useGridRootProps");
@@ -25,7 +24,6 @@ const GridGenericColumnHeaderItem = exports.GridGenericColumnHeaderItem = (0, _f
25
24
  height,
26
25
  isResizing,
27
26
  sortDirection,
28
- hasFocus,
29
27
  tabIndex,
30
28
  separatorSide,
31
29
  isDraggable,
@@ -43,7 +41,6 @@ const GridGenericColumnHeaderItem = exports.GridGenericColumnHeaderItem = (0, _f
43
41
  style
44
42
  } = props,
45
43
  other = (0, _objectWithoutPropertiesLoose2.default)(props, _excluded);
46
- const apiRef = (0, _useGridPrivateApiContext.useGridPrivateApiContext)();
47
44
  const rootProps = (0, _useGridRootProps.useGridRootProps)();
48
45
  const headerCellRef = React.useRef(null);
49
46
  const handleRef = (0, _utils.unstable_useForkRef)(headerCellRef, ref);
@@ -51,17 +48,6 @@ const GridGenericColumnHeaderItem = exports.GridGenericColumnHeaderItem = (0, _f
51
48
  if (sortDirection != null) {
52
49
  ariaSort = sortDirection === 'asc' ? 'ascending' : 'descending';
53
50
  }
54
- React.useLayoutEffect(() => {
55
- const columnMenuState = apiRef.current.state.columnMenu;
56
- if (hasFocus && !columnMenuState.open) {
57
- const focusableElement = headerCellRef.current.querySelector('[tabindex="0"]');
58
- const elementToFocus = focusableElement || headerCellRef.current;
59
- elementToFocus?.focus();
60
- if (apiRef.current.columnHeadersContainerRef?.current) {
61
- apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0;
62
- }
63
- }
64
- }, [apiRef, hasFocus]);
65
51
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", (0, _extends2.default)({
66
52
  className: (0, _clsx.default)(classes.root, headerClassName),
67
53
  style: (0, _extends2.default)({}, style, {
@@ -77,6 +77,21 @@ function useGridDimensions(apiRef, props) {
77
77
  const densityFactor = (0, _utils2.useGridSelector)(apiRef, _density.gridDensityFactorSelector);
78
78
  const columnsTotalWidth = (0, _utils2.useGridSelector)(apiRef, columnsTotalWidthSelector);
79
79
  const isFirstSizing = React.useRef(true);
80
+
81
+ // Vertical scrollbar oscillation detector.
82
+ // Counts consecutive hasScrollY flips that happen with no row-height change.
83
+ // After 2 flips it is certainly a layout feedback loop, so every further flip
84
+ // is forced to false (no scrollbar). The counter resets when row heights change.
85
+ // Only vertical scrollbar can oscillate because column widths are never 'auto'.
86
+ // https://github.com/mui/mui-x/issues/20539
87
+ const scrollYOscillation = React.useRef({
88
+ counter: 0,
89
+ heights: {
90
+ content: 0,
91
+ pinnedTop: 0,
92
+ pinnedBottom: 0
93
+ }
94
+ });
80
95
  const {
81
96
  rowHeight,
82
97
  headerHeight,
@@ -144,6 +159,7 @@ function useGridDimensions(apiRef, props) {
144
159
  width: nonPinnedColumnsTotalWidth,
145
160
  height: (0, _roundToDecimalPlaces.roundToDecimalPlaces)(rowsMeta.currentPageTotalHeight, 1)
146
161
  };
162
+ const prevDimensions = apiRef.current.state.dimensions;
147
163
  let viewportOuterSize;
148
164
  let viewportInnerSize;
149
165
  let hasScrollX = false;
@@ -181,6 +197,36 @@ function useGridDimensions(apiRef, props) {
181
197
  hasScrollY = content.height + scrollbarSize > container.height;
182
198
  }
183
199
  }
200
+
201
+ // Detect vertical scrollbar oscillation.
202
+ // Track consecutive hasScrollY flips with no row-height change.
203
+ // Once confirmed (≥ 2 flips), force hasScrollY off — the scrollbar is
204
+ // not genuinely needed, it is a layout feedback loop caused by stale
205
+ // rootSize or the horizontal scrollbar's height cascading.
206
+ {
207
+ const osc = scrollYOscillation.current;
208
+ const heightsChanged = rowsMeta.currentPageTotalHeight !== osc.heights.content || rowsMeta.pinnedTopRowsTotalHeight !== osc.heights.pinnedTop || rowsMeta.pinnedBottomRowsTotalHeight !== osc.heights.pinnedBottom;
209
+ if (heightsChanged) {
210
+ osc.counter = 0;
211
+ osc.heights = {
212
+ content: rowsMeta.currentPageTotalHeight,
213
+ pinnedTop: rowsMeta.pinnedTopRowsTotalHeight,
214
+ pinnedBottom: rowsMeta.pinnedBottomRowsTotalHeight
215
+ };
216
+ }
217
+ if (prevDimensions.isReady && hasScrollY !== prevDimensions.hasScrollY) {
218
+ if (!heightsChanged) {
219
+ osc.counter += 1;
220
+ }
221
+ if (osc.counter >= 2) {
222
+ hasScrollY = false;
223
+ // Recompute hasScrollX without the vertical scrollbar's width impact,
224
+ // otherwise the cascade (hasScrollY → narrower viewport → hasScrollX)
225
+ // keeps the horizontal scrollbar/filler alive and the root keeps resizing.
226
+ hasScrollX = hasScrollXIfNoYScrollBar;
227
+ }
228
+ }
229
+ }
184
230
  if (hasScrollY) {
185
231
  viewportInnerSize.width -= scrollbarSize;
186
232
  }
@@ -215,7 +261,6 @@ function useGridDimensions(apiRef, props) {
215
261
  topContainerHeight,
216
262
  bottomContainerHeight
217
263
  };
218
- const prevDimensions = apiRef.current.state.dimensions;
219
264
  if ((0, _utils3.isDeepEqual)(prevDimensions, newDimensions)) {
220
265
  return;
221
266
  }
@@ -209,7 +209,7 @@ const useGridPrintExport = (apiRef, props) => {
209
209
  // Revert grid to previous state
210
210
  apiRef.current.restoreState(previousGridState.current || {});
211
211
  if (!previousGridState.current?.columns?.columnVisibilityModel) {
212
- // if the apiRef.current.exportState(); did not exported the column visibility, we update it
212
+ // if the apiRef.current.exportState(); did not export the column visibility, we update it
213
213
  apiRef.current.setColumnVisibilityModel(previousColumnVisibility.current);
214
214
  }
215
215
  apiRef.current.setState(state => (0, _extends2.default)({}, state, {
@@ -246,12 +246,7 @@ const useGridPrintExport = (apiRef, props) => {
246
246
  }));
247
247
  }
248
248
  previousVirtualizationState.current = apiRef.current.state.virtualization;
249
- apiRef.current.setState(state => (0, _extends2.default)({}, state, {
250
- virtualization: (0, _extends2.default)({}, state.virtualization, {
251
- enabled: false,
252
- enabledForColumns: false
253
- })
254
- }));
249
+ apiRef.current.unstable_setVirtualization(false);
255
250
  await updateGridColumnsForPrint(options?.fields, options?.allColumns, options?.includeCheckboxes);
256
251
  updateGridRowsForPrint(options?.getRowsToExport ?? _utils2.defaultGetRowsToExport);
257
252
  await raf(); // wait for the state changes to take action
package/node/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v7.29.11
2
+ * @mui/x-data-grid v7.29.13
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": "7.29.11",
3
+ "version": "7.29.13",
4
4
  "description": "The Community plan edition of the Data Grid components (MUI X).",
5
5
  "author": "MUI Team",
6
6
  "main": "./node/index.js",