@mui/x-virtualizer 1.0.0-beta.0 → 1.0.0-rc.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,265 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.0.0-rc.0
4
+
5
+ <!-- generated comparing v9.0.0-beta.0..master -->
6
+
7
+ _Apr 7, 2026_
8
+
9
+ We'd like to extend a big thank you to the 18 contributors who made this release possible.
10
+
11
+ Special thanks go out to these community members for their valuable contributions:
12
+ @mixelburg, @sibananda485, @youjin-hong
13
+
14
+ The following team members contributed to this release:
15
+ @aemartos, @alexfauquette, @arminmeh, @brijeshb42, @flaviendelangle, @JCQuintas, @LukasTy, @mapache-salvaje, @MBilalShafi, @michelengelen, @noraleonte, @rita-codes, @romgrk, @siriwatknp, @ZeeshanTamboli
16
+
17
+ ### Data Grid
18
+
19
+ #### `@mui/x-data-grid@9.0.0-rc.0`
20
+
21
+ - [DataGrid] Rename filter panel `Columns` label to singular `Column` (#21935) @youjin-hong
22
+ - [DataGrid] Export `GridColumnUnsortedIconProps` for custom column icon slots (#21658) @mixelburg
23
+ - [DataGrid] Remove `x-virtualizer`'s `virtualScroller` from public API (#21936) @romgrk
24
+ - [DataGrid][virtualizer] Scrolling without render gaps (#21616) @romgrk
25
+
26
+ #### `@mui/x-data-grid-pro@9.0.0-rc.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
27
+
28
+ Same changes as in `@mui/x-data-grid@9.0.0-rc.0`, plus:
29
+
30
+ - [DataGridPro] Improve trigger for nested row reordering (#21642) @MBilalShafi
31
+ - [DataGridPro] Undeprecate `onRowsScrollEnd` prop (#21912) @MBilalShafi
32
+
33
+ #### `@mui/x-data-grid-premium@9.0.0-rc.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
34
+
35
+ Same changes as in `@mui/x-data-grid-pro@9.0.0-rc.0`, plus:
36
+
37
+ - [DataGridPremium] Fix clipboard paste issue in portal (#21931) @sibananda485
38
+
39
+ ### Date and Time Pickers
40
+
41
+ #### Breaking changes
42
+
43
+ - Accessible DOM structure is now the only default. [Read more](https://next.mui.com/x/migration/migration-pickers-v8/#accessible-dom-structure-is-now-the-default)
44
+ - The `PickerDay2` and `DateRangePickerDay2` components were propagated to stable while removing the previous defaults. [Read more](https://next.mui.com/x/migration/migration-pickers-v8/#day-slot)
45
+
46
+ #### `@mui/x-date-pickers@9.0.0-rc.0`
47
+
48
+ - [pickers] Remove `PickersDay` and `DateRangePickerDay` and promote their `2` versions as replacements (#21739) @michelengelen
49
+
50
+ #### `@mui/x-date-pickers-pro@9.0.0-rc.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
51
+
52
+ Same changes as in `@mui/x-date-pickers@9.0.0-rc.0`.
53
+
54
+ ### Charts
55
+
56
+ #### `@mui/x-charts@9.0.0-rc.0.0`
57
+
58
+ - [charts] Make line visibility toggle start from the baseline (#21893) @alexfauquette
59
+ - [charts] Remove the container overflow (#21955) @alexfauquette
60
+ - [charts] Revert `theme.alpha` for non-channel token (#21965) @siriwatknp
61
+
62
+ #### `@mui/x-charts-pro@9.0.0-rc.0.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.0.0-rc.0.0`, plus:
65
+
66
+ - [charts-pro] Zoom slider touch improvements (#21832) @JCQuintas
67
+ - [charts-pro] Add `seriesIds` filter to zoom slider preview (#21933) @JCQuintas
68
+ - [charts-pro] Fix zoom slider preview with discard filter mode (#21883) @JCQuintas
69
+
70
+ #### `@mui/x-charts-premium@9.0.0-rc.0.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
71
+
72
+ Same changes as in `@mui/x-charts-pro@9.0.0-rc.0.0`, plus:
73
+
74
+ - [charts-premium] Add series `valueFormatter` to candlestick chart (#21905) @JCQuintas
75
+ - [charts-premium] Add zoom slider preview support for candlestick charts (#21914) @JCQuintas
76
+ - [charts-premium] Allow color customization in `Candlestick` chart (#21838) @JCQuintas
77
+ - [charts-premium] Support hide/show for OHLC (candlestick) series (#21807) @Copilot
78
+ - [charts-premium] Add `dataset` support to `Candlestick` chart (#21872) @JCQuintas
79
+ - [charts-premium] Add candlestick page to sidebar navigation (#21834) @JCQuintas
80
+
81
+ ### Tree View
82
+
83
+ #### `@mui/x-tree-view@9.0.0-rc.0`
84
+
85
+ Internal changes.
86
+
87
+ #### `@mui/x-tree-view-pro@9.0.0-rc.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
88
+
89
+ Same changes as in `@mui/x-tree-view@9.0.0-rc.0`, plus:
90
+
91
+ - [RichTreeViewPro] Allow to auto-expand lazy loaded items (#21759) @flaviendelangle
92
+
93
+ ### Scheduler
94
+
95
+ #### `@mui/x-scheduler@9.0.0-alpha.0`
96
+
97
+ - [scheduler] Add locale files, adapt l10n scripts, and add localization table to docs (#21870) @rita-codes
98
+ - [scheduler] Add planned features to the docs (#21705) @rita-codes
99
+ - [scheduler] Add scheduler to docs introduction (#21845) @rita-codes
100
+ - [scheduler] Add wide docs to scheduler (#21860) @noraleonte
101
+ - [scheduler] All day event bugfixes (#21884) @noraleonte
102
+ - [scheduler] Autofocus title field (#21947) @noraleonte
103
+ - [scheduler] Change default event creation trigger to single click (#21979) @rita-codes
104
+ - [scheduler] Change order of the views on the view selector (#21904) @rita-codes
105
+ - [scheduler] Disabled border color for the repeat day picker in dark mode (#21987) @rita-codes
106
+ - [scheduler] Drop unused dependency (#21956) @flaviendelangle
107
+ - [scheduler] Fix all-day event shifting to previous day in negative UTC offsets (#21994) @rita-codes
108
+ - [scheduler] Fix dark theme localization demos (#21992) @noraleonte
109
+ - [scheduler] Fix licensing confusion in docs (#21939) @rita-codes
110
+ - [scheduler] Fix preferences menu width shift when toggling options + Improve preferences menu accessibility (#21902) @rita-codes
111
+ - [scheduler] Prepare for the alpha launch (#21859) @rita-codes
112
+ - [scheduler] Sync Base UI internals and apply good practices (#21946) @flaviendelangle
113
+ - [scheduler] Update close modal aria label translation (#21940) @rita-codes
114
+ - [scheduler] Add Spanish (es-ES) locale (#21900) @rita-codes
115
+ - [scheduler] Improve French (fr-FR) locale (#21941) @rita-codes
116
+ - [scheduler] Improve Romanian (ro-RO) locale (#21942) @rita-codes
117
+
118
+ #### `@mui/x-scheduler-premium@9.0.0-alpha.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
119
+
120
+ Same changes as in `@mui/x-scheduler@9.0.0-alpha.0`.
121
+
122
+ ### Codemod
123
+
124
+ #### `@mui/x-codemod@9.0.0-rc.0`
125
+
126
+ Internal changes.
127
+
128
+ ### Docs
129
+
130
+ - [docs] Fix JSDOM → jsdom casing (#21907) @JCQuintas
131
+ - [docs] Remove Joy UI references and dependency (#21937) @siriwatknp
132
+ - [docs] Remove none generated files (#21886) @alexfauquette
133
+ - [docs] Remove unused interactive demo code (#21945) @LukasTy
134
+ - [docs] Revise the Funnel doc (#21677) @mapache-salvaje
135
+ - [docs] Revise the Line chart docs (#21554) @mapache-salvaje
136
+ - [docs] Revise the Radar doc (#21674) @mapache-salvaje
137
+ - [docs] Revise the Sankey doc (#21678) @mapache-salvaje
138
+ - [docs] Revise the Scatter chart docs (#21564) @mapache-salvaje
139
+
140
+ ### Core
141
+
142
+ - [docs-infra] Update to the latest monorepo (#21971) @brijeshb42
143
+ - [internal] Remove checks for `materialVersion >= 6` (#21975) @LukasTy
144
+
145
+ ### Miscellaneous
146
+
147
+ - [core] Bump @mui/material to v9.0.0-beta.1 (#21858) @siriwatknp
148
+ - [core] Update browserslistrc (#21974) @siriwatknp
149
+ - [deps] Bump minimum core packages to 7.3.0 to adopt theme color manipulator (#21892) @siriwatknp
150
+ - [telemetry] Prefer upstream remote over origin for `projectId` (#21882) @aemartos
151
+ - [telemetry] Send `repoHash`, `[x]packageNameHash`, and `rootPathHash` alongside `projectId` (#21896) @aemartos
152
+ - [test] Exclude flaky `DataGrid` argos test (#21977) @MBilalShafi
153
+ - [test] Fix flaky `DataGrid` test (#22000) @arminmeh
154
+ - [test] Remove `componentsProp` test from `describeConformance` (#21897) @ZeeshanTamboli
155
+ - [x-license] Change `orderId` type from `number` to `string` (#21885) @aemartos
156
+
157
+ ## 9.0.0-beta.0
158
+
159
+ <!-- generated comparing v9.0.0-alpha.4..master -->
160
+
161
+ _Mar 27, 2026_
162
+
163
+ We'd like to extend a big thank you to the 10 contributors who made this release possible. Here are some highlights ✨:
164
+
165
+ - 🔊 New Charts voiceover component for improved screen reader support
166
+ - ⌨️ Charts keyboard navigation improvements: axis tooltip now shows when navigating with the keyboard
167
+ - 📊 Charts axes now can be set to automatically resize to fit their content
168
+ - 📝 New `rowCheckbox` slot in Data Grid for easier checkbox column customization
169
+ - ⚡️ `fetchRows()` API in Data Grid Pro now defaults `start` and `end` based on scroll position with lazy loading
170
+ - 🐞 Bugfixes and internal improvements
171
+
172
+ The following team members contributed to this release:
173
+ @aemartos, @alexfauquette, @arminmeh, @cherniavskii, @Janpot, @JCQuintas, @mapache-salvaje, @michelengelen, @noraleonte, @rita-codes
174
+
175
+ ### Data Grid
176
+
177
+ #### `@mui/x-data-grid@9.0.0-beta.0`
178
+
179
+ - [DataGrid] Add `rowCheckbox` slot for easier customization (#21797) @michelengelen
180
+ - [DataGrid] Prevent repeated `hasScrollbar` state updates (#21820) @arminmeh
181
+
182
+ #### `@mui/x-data-grid-pro@9.0.0-beta.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
183
+
184
+ Same changes as in `@mui/x-data-grid@9.0.0-beta.0`, plus:
185
+
186
+ - [DataGridPro] `fetchRows()` API's default `start` and `end` params based on scroll position with lazy loading (#21742) @arminmeh
187
+
188
+ #### `@mui/x-data-grid-premium@9.0.0-beta.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
189
+
190
+ Same changes as in `@mui/x-data-grid-pro@9.0.0-beta.0`.
191
+
192
+ ### Date and Time Pickers
193
+
194
+ #### `@mui/x-date-pickers@9.0.0-beta.0`
195
+
196
+ Internal changes.
197
+
198
+ #### `@mui/x-date-pickers-pro@9.0.0-beta.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
199
+
200
+ Same changes as in `@mui/x-date-pickers@9.0.0-beta.0`.
201
+
202
+ ### Charts
203
+
204
+ #### `@mui/x-charts@9.0.0-beta.0`
205
+
206
+ - [charts] Add `className` prop to Pro chart plot components (#21793) @JCQuintas
207
+ - [charts] Add experimental position-based pointer interaction for line series (#21809) @JCQuintas
208
+ - [charts] Add l10n to the bar accessibility (#21815) @alexfauquette
209
+ - [charts] Add localization for the basic charts (#21822) @alexfauquette
210
+ - [charts] Add voiceover component (#21344) @alexfauquette
211
+ - [charts] Allow axes to automatically resize to content (#21087) @JCQuintas
212
+ - [charts] Document multiple use-cases for references (#21768) @alexfauquette
213
+ - [charts] Remove compatibility layer for React vs native events (#21780) @JCQuintas
214
+ - [charts] Remove deprecated `barLabel` props (#21783) @alexfauquette
215
+ - [charts] Show axis tooltip when navigating with keyboard (#21689) @Copilot
216
+
217
+ #### `@mui/x-charts-pro@9.0.0-beta.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
218
+
219
+ Same changes as in `@mui/x-charts@9.0.0-beta.0`.
220
+
221
+ #### `@mui/x-charts-premium@9.0.0-beta.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
222
+
223
+ Same changes as in `@mui/x-charts-pro@9.0.0-beta.0`.
224
+
225
+ ### Tree View
226
+
227
+ #### `@mui/x-tree-view@9.0.0-alpha.4`
228
+
229
+ Internal changes.
230
+
231
+ #### `@mui/x-tree-view-pro@9.0.0-alpha.4` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
232
+
233
+ Same changes as in `@mui/x-tree-view@9.0.0-alpha.4`.
234
+
235
+ ### Codemod
236
+
237
+ #### `@mui/x-codemod@9.0.0-alpha.4`
238
+
239
+ Internal changes.
240
+
241
+ ### Docs
242
+
243
+ - [docs] Document how to customize voiceover announcement (#21833) @alexfauquette
244
+ - [docs] Remove Discord mention from docs (#21855) @mapache-salvaje
245
+ - [docs] Remove stabilized experimental feature from demo (#21869) @JCQuintas
246
+ - [docs] Update telemetry guide to reflect pseudonymous data collection and license compliance (#21812) @aemartos
247
+ - [docs] Revise the Sparkline doc (#21614) @mapache-salvaje
248
+ - [docs] Revise the Gauge doc (#21673) @mapache-salvaje
249
+ - [docs] Revise the Heatmap doc (#21676) @mapache-salvaje
250
+
251
+ ### Core
252
+
253
+ - [code-infra] Remove unused deps and unify es-toolkit via catalog (#21840) @Janpot
254
+ - [code-infra] Update @mui/internal-bundle-size-checker to canary.68 (#21836) @Janpot
255
+ - [code-infra] Update next (#21837) @Janpot
256
+ - [internal] Remove headless data grid packages (#21843) @cherniavskii
257
+
258
+ ### Miscellaneous
259
+
260
+ - Add @romgrk to CODEOWNERS for `x-virtualizer` and `x-internals` (#21819) @Copilot
261
+ - [x-license] add 2022 plan version (#21814) @aemartos
262
+
3
263
  ## 9.0.0-alpha.4
4
264
 
5
265
  _Mar 19, 2026_
@@ -59,7 +319,7 @@ Same changes as in `@mui/x-date-pickers@9.0.0-alpha.4`.
59
319
  - [charts] Remove deprecated `useMouseTracker()` (#21787) @alexfauquette
60
320
  - [charts] Remove deprecated classes (#21775) @alexfauquette
61
321
  - [charts] Remove deprecated props from PieArcLabel animation (#21789) @alexfauquette
62
- - [charts] Remove get*UtilityClass from public exports (#21769) @JCQuintas
322
+ - [charts] Remove get\*UtilityClass from public exports (#21769) @JCQuintas
63
323
  - [charts] Remove the deprecated `disableHover` property (#21785) @alexfauquette
64
324
  - [charts] Remove the deprecated `message` prop (#21784) @alexfauquette
65
325
  - [charts] Remove deprecated props about voronoi (#21796) @alexfauquette
@@ -207,7 +467,7 @@ Same changes as in `@mui/x-charts-pro@9.0.0-alpha.3`, plus:
207
467
  - Remove deprecated CSS state classes from `treeItemClasses`: `expanded`, `selected`, `focused`, `disabled`, `editable`, `editing` (use `[data-expanded]`, `[data-selected]`, etc.)
208
468
  - The `<RichTreeViewPro />` component has now virtualization enabled by default.
209
469
  - The items used inside the `<RichTreeViewPro />` now have a default height of `32px`.
210
- - The events of the `<RichTreeViewPro />` are now rendered as a flat list instead of a nested tree.
470
+ - The items of the `<RichTreeViewPro />` are now rendered as a flat list instead of a nested tree.
211
471
 
212
472
  #### `@mui/x-tree-view@9.0.0-alpha.3`
213
473
 
@@ -27,6 +27,7 @@ export declare const Dimensions: {
27
27
  rowPositions: (state: BaseState) => number[];
28
28
  columnPositions: (_: any, columns: ColumnWithWidth[]) => number[];
29
29
  needsHorizontalScrollbar: (state: BaseState) => boolean;
30
+ needsVerticalScrollbar: (state: BaseState) => boolean;
30
31
  };
31
32
  };
32
33
  export declare namespace Dimensions {
@@ -27,6 +27,7 @@ export declare const Dimensions: {
27
27
  rowPositions: (state: BaseState) => number[];
28
28
  columnPositions: (_: any, columns: ColumnWithWidth[]) => number[];
29
29
  needsHorizontalScrollbar: (state: BaseState) => boolean;
30
+ needsVerticalScrollbar: (state: BaseState) => boolean;
30
31
  };
31
32
  };
32
33
  export declare namespace Dimensions {
@@ -61,7 +61,8 @@ const selectors = {
61
61
  }
62
62
  return positions;
63
63
  }),
64
- needsHorizontalScrollbar: state => state.dimensions.viewportOuterSize.width > 0 && state.dimensions.columnsTotalWidth > state.dimensions.viewportOuterSize.width
64
+ needsHorizontalScrollbar: state => state.dimensions.viewportInnerSize.width > 0 && state.dimensions.columnsTotalWidth > state.dimensions.viewportInnerSize.width,
65
+ needsVerticalScrollbar: state => state.dimensions.viewportInnerSize.height > 0 && state.dimensions.contentSize.height > state.dimensions.viewportInnerSize.height
65
66
  };
66
67
  const Dimensions = exports.Dimensions = {
67
68
  initialize: initializeState,
@@ -97,6 +98,21 @@ function initializeState(params) {
97
98
  }
98
99
  function useDimensions(store, params, _api) {
99
100
  const isFirstSizing = React.useRef(true);
101
+
102
+ // Vertical scrollbar oscillation detector.
103
+ // Counts consecutive hasScrollY flips that happen with no row-height change.
104
+ // After 2 flips it is certainly a layout feedback loop, so every further flip
105
+ // is forced to false (no scrollbar). The counter resets when row heights change.
106
+ // Only vertical scrollbar can oscillate because column widths are never 'auto'.
107
+ // https://github.com/mui/mui-x/issues/20539
108
+ const scrollYOscillation = React.useRef({
109
+ counter: 0,
110
+ heights: {
111
+ content: 0,
112
+ pinnedTop: 0,
113
+ pinnedBottom: 0
114
+ }
115
+ });
100
116
  const {
101
117
  layout,
102
118
  dimensions: {
@@ -130,6 +146,7 @@ function useDimensions(store, params, _api) {
130
146
  width: columnsTotalWidth,
131
147
  height: (0, _math.roundToDecimalPlaces)(rowsMeta.currentPageTotalHeight, 1)
132
148
  };
149
+ const prevDimensions = store.state.dimensions;
133
150
  let viewportOuterSize;
134
151
  let viewportInnerSize;
135
152
  let hasScrollX = false;
@@ -167,6 +184,36 @@ function useDimensions(store, params, _api) {
167
184
  hasScrollY = content.height + scrollbarSize > container.height;
168
185
  }
169
186
  }
187
+
188
+ // Detect vertical scrollbar oscillation.
189
+ // Track consecutive hasScrollY flips with no row-height change.
190
+ // Once confirmed (≥ 2 flips), force hasScrollY off — the scrollbar is
191
+ // not genuinely needed, it is a layout feedback loop caused by stale
192
+ // rootSize or the horizontal scrollbar's height cascading.
193
+ {
194
+ const osc = scrollYOscillation.current;
195
+ const heightsChanged = rowsMeta.currentPageTotalHeight !== osc.heights.content || rowsMeta.pinnedTopRowsTotalHeight !== osc.heights.pinnedTop || rowsMeta.pinnedBottomRowsTotalHeight !== osc.heights.pinnedBottom;
196
+ if (heightsChanged) {
197
+ osc.counter = 0;
198
+ osc.heights = {
199
+ content: rowsMeta.currentPageTotalHeight,
200
+ pinnedTop: rowsMeta.pinnedTopRowsTotalHeight,
201
+ pinnedBottom: rowsMeta.pinnedBottomRowsTotalHeight
202
+ };
203
+ }
204
+ if (prevDimensions.isReady && hasScrollY !== prevDimensions.hasScrollY) {
205
+ if (!heightsChanged) {
206
+ osc.counter += 1;
207
+ }
208
+ if (osc.counter >= 2) {
209
+ hasScrollY = false;
210
+ // Recompute hasScrollX without the vertical scrollbar's width impact,
211
+ // otherwise the cascade (hasScrollY → narrower viewport → hasScrollX)
212
+ // keeps the horizontal scrollbar/filler alive and the root keeps resizing.
213
+ hasScrollX = hasScrollXIfNoYScrollBar;
214
+ }
215
+ }
216
+ }
170
217
  if (hasScrollY) {
171
218
  viewportInnerSize.width -= scrollbarSize;
172
219
  }
@@ -205,7 +252,6 @@ function useDimensions(store, params, _api) {
205
252
  autoHeight: params.dimensions.autoHeight,
206
253
  minimalContentHeight: params.dimensions.minimalContentHeight
207
254
  };
208
- const prevDimensions = store.state.dimensions;
209
255
  if ((0, _isDeepEqual.isDeepEqual)(prevDimensions, newDimensions)) {
210
256
  return;
211
257
  }
@@ -54,7 +54,8 @@ const selectors = {
54
54
  }
55
55
  return positions;
56
56
  }),
57
- needsHorizontalScrollbar: state => state.dimensions.viewportOuterSize.width > 0 && state.dimensions.columnsTotalWidth > state.dimensions.viewportOuterSize.width
57
+ needsHorizontalScrollbar: state => state.dimensions.viewportInnerSize.width > 0 && state.dimensions.columnsTotalWidth > state.dimensions.viewportInnerSize.width,
58
+ needsVerticalScrollbar: state => state.dimensions.viewportInnerSize.height > 0 && state.dimensions.contentSize.height > state.dimensions.viewportInnerSize.height
58
59
  };
59
60
  export const Dimensions = {
60
61
  initialize: initializeState,
@@ -90,6 +91,21 @@ function initializeState(params) {
90
91
  }
91
92
  function useDimensions(store, params, _api) {
92
93
  const isFirstSizing = React.useRef(true);
94
+
95
+ // Vertical scrollbar oscillation detector.
96
+ // Counts consecutive hasScrollY flips that happen with no row-height change.
97
+ // After 2 flips it is certainly a layout feedback loop, so every further flip
98
+ // is forced to false (no scrollbar). The counter resets when row heights change.
99
+ // Only vertical scrollbar can oscillate because column widths are never 'auto'.
100
+ // https://github.com/mui/mui-x/issues/20539
101
+ const scrollYOscillation = React.useRef({
102
+ counter: 0,
103
+ heights: {
104
+ content: 0,
105
+ pinnedTop: 0,
106
+ pinnedBottom: 0
107
+ }
108
+ });
93
109
  const {
94
110
  layout,
95
111
  dimensions: {
@@ -123,6 +139,7 @@ function useDimensions(store, params, _api) {
123
139
  width: columnsTotalWidth,
124
140
  height: roundToDecimalPlaces(rowsMeta.currentPageTotalHeight, 1)
125
141
  };
142
+ const prevDimensions = store.state.dimensions;
126
143
  let viewportOuterSize;
127
144
  let viewportInnerSize;
128
145
  let hasScrollX = false;
@@ -160,6 +177,36 @@ function useDimensions(store, params, _api) {
160
177
  hasScrollY = content.height + scrollbarSize > container.height;
161
178
  }
162
179
  }
180
+
181
+ // Detect vertical scrollbar oscillation.
182
+ // Track consecutive hasScrollY flips with no row-height change.
183
+ // Once confirmed (≥ 2 flips), force hasScrollY off — the scrollbar is
184
+ // not genuinely needed, it is a layout feedback loop caused by stale
185
+ // rootSize or the horizontal scrollbar's height cascading.
186
+ {
187
+ const osc = scrollYOscillation.current;
188
+ const heightsChanged = rowsMeta.currentPageTotalHeight !== osc.heights.content || rowsMeta.pinnedTopRowsTotalHeight !== osc.heights.pinnedTop || rowsMeta.pinnedBottomRowsTotalHeight !== osc.heights.pinnedBottom;
189
+ if (heightsChanged) {
190
+ osc.counter = 0;
191
+ osc.heights = {
192
+ content: rowsMeta.currentPageTotalHeight,
193
+ pinnedTop: rowsMeta.pinnedTopRowsTotalHeight,
194
+ pinnedBottom: rowsMeta.pinnedBottomRowsTotalHeight
195
+ };
196
+ }
197
+ if (prevDimensions.isReady && hasScrollY !== prevDimensions.hasScrollY) {
198
+ if (!heightsChanged) {
199
+ osc.counter += 1;
200
+ }
201
+ if (osc.counter >= 2) {
202
+ hasScrollY = false;
203
+ // Recompute hasScrollX without the vertical scrollbar's width impact,
204
+ // otherwise the cascade (hasScrollY → narrower viewport → hasScrollX)
205
+ // keeps the horizontal scrollbar/filler alive and the root keeps resizing.
206
+ hasScrollX = hasScrollXIfNoYScrollBar;
207
+ }
208
+ }
209
+ }
163
210
  if (hasScrollY) {
164
211
  viewportInnerSize.width -= scrollbarSize;
165
212
  }
@@ -198,7 +245,6 @@ function useDimensions(store, params, _api) {
198
245
  autoHeight: params.dimensions.autoHeight,
199
246
  minimalContentHeight: params.dimensions.minimalContentHeight
200
247
  };
201
- const prevDimensions = store.state.dimensions;
202
248
  if (isDeepEqual(prevDimensions, newDimensions)) {
203
249
  return;
204
250
  }
@@ -36,6 +36,17 @@ export declare class LayoutDataGrid extends Layout<DataGridElements> {
36
36
  role: string;
37
37
  tabIndex: number | undefined;
38
38
  };
39
+ scrollerContentProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
40
+ style: React.CSSProperties | undefined;
41
+ role: string;
42
+ };
43
+ viewportProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
44
+ style: {
45
+ width: number;
46
+ height: number;
47
+ };
48
+ role: string;
49
+ };
39
50
  contentProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
40
51
  style: React.CSSProperties;
41
52
  role: string;
@@ -45,6 +56,11 @@ export declare class LayoutDataGrid extends Layout<DataGridElements> {
45
56
  transform: string;
46
57
  };
47
58
  };
59
+ containerVerticalProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
60
+ style: {
61
+ transform: string;
62
+ };
63
+ } | undefined;
48
64
  scrollbarHorizontalProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
49
65
  ref: any;
50
66
  scrollPosition: {
@@ -78,6 +94,17 @@ export declare class LayoutDataGridLegacy extends LayoutDataGrid {
78
94
  role: string;
79
95
  tabIndex: number | undefined;
80
96
  };
97
+ getScrollerContentProps: () => {
98
+ style: React.CSSProperties | undefined;
99
+ role: string;
100
+ };
101
+ getViewportProps: () => {
102
+ style: {
103
+ width: number;
104
+ height: number;
105
+ };
106
+ role: string;
107
+ };
81
108
  getContentProps: () => {
82
109
  style: React.CSSProperties;
83
110
  role: string;
@@ -104,6 +131,11 @@ export declare class LayoutDataGridLegacy extends LayoutDataGrid {
104
131
  current: import("../../models/index.mjs").ScrollPosition;
105
132
  };
106
133
  };
134
+ getContainerVerticalProps: () => {
135
+ style: {
136
+ transform: string;
137
+ };
138
+ } | undefined;
107
139
  };
108
140
  }
109
141
  type ListElements = BaseElements;
@@ -36,6 +36,17 @@ export declare class LayoutDataGrid extends Layout<DataGridElements> {
36
36
  role: string;
37
37
  tabIndex: number | undefined;
38
38
  };
39
+ scrollerContentProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
40
+ style: React.CSSProperties | undefined;
41
+ role: string;
42
+ };
43
+ viewportProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
44
+ style: {
45
+ width: number;
46
+ height: number;
47
+ };
48
+ role: string;
49
+ };
39
50
  contentProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
40
51
  style: React.CSSProperties;
41
52
  role: string;
@@ -45,6 +56,11 @@ export declare class LayoutDataGrid extends Layout<DataGridElements> {
45
56
  transform: string;
46
57
  };
47
58
  };
59
+ containerVerticalProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
60
+ style: {
61
+ transform: string;
62
+ };
63
+ } | undefined;
48
64
  scrollbarHorizontalProps: (args_0: Virtualization.State<Layout<AnyElements>> & Dimensions.State) => {
49
65
  ref: any;
50
66
  scrollPosition: {
@@ -78,6 +94,17 @@ export declare class LayoutDataGridLegacy extends LayoutDataGrid {
78
94
  role: string;
79
95
  tabIndex: number | undefined;
80
96
  };
97
+ getScrollerContentProps: () => {
98
+ style: React.CSSProperties | undefined;
99
+ role: string;
100
+ };
101
+ getViewportProps: () => {
102
+ style: {
103
+ width: number;
104
+ height: number;
105
+ };
106
+ role: string;
107
+ };
81
108
  getContentProps: () => {
82
109
  style: React.CSSProperties;
83
110
  role: string;
@@ -104,6 +131,11 @@ export declare class LayoutDataGridLegacy extends LayoutDataGrid {
104
131
  current: import("../../models/index.js").ScrollPosition;
105
132
  };
106
133
  };
134
+ getContainerVerticalProps: () => {
135
+ style: {
136
+ transform: string;
137
+ };
138
+ } | undefined;
107
139
  };
108
140
  }
109
141
  type ListElements = BaseElements;
@@ -64,19 +64,58 @@ class LayoutDataGrid extends Layout {
64
64
  // https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
65
65
  tabIndex: platform.isFirefox ? -1 : undefined
66
66
  })),
67
+ scrollerContentProps: (0, _store.createSelectorMemoized)(_virtualization.Virtualization.selectors.layoutMode, _dimensions.Dimensions.selectors.dimensions, _dimensions.Dimensions.selectors.needsVerticalScrollbar, _dimensions.Dimensions.selectors.needsHorizontalScrollbar, (layoutMode, dimensions, needsVerticalScrollbar, needsHorizontalScrollbar) => {
68
+ let style;
69
+ if (layoutMode === 'controlled') {
70
+ const {
71
+ contentSize,
72
+ scrollbarSize,
73
+ topContainerHeight,
74
+ bottomContainerHeight,
75
+ minimalContentHeight,
76
+ columnsTotalWidth
77
+ } = dimensions;
78
+ const verticalScrollbarSize = needsVerticalScrollbar ? scrollbarSize : 0;
79
+ const horizontalScrollbarSize = needsHorizontalScrollbar ? scrollbarSize : 0;
80
+ const contentHeight = contentSize.height === 0 ? minimalContentHeight : contentSize.height;
81
+ const width = needsHorizontalScrollbar ? verticalScrollbarSize + columnsTotalWidth : 'auto';
82
+ const height = cssAdd(cssAdd(cssAdd(contentHeight, topContainerHeight), bottomContainerHeight), horizontalScrollbarSize);
83
+ style = {
84
+ width,
85
+ height,
86
+ flex: '0 0 auto'
87
+ };
88
+ }
89
+ return {
90
+ style,
91
+ role: 'presentation'
92
+ };
93
+ }),
94
+ viewportProps: (0, _store.createSelectorMemoized)(_dimensions.Dimensions.selectors.dimensions, dimensions => ({
95
+ style: {
96
+ width: dimensions.viewportOuterSize.width,
97
+ height: dimensions.viewportOuterSize.height
98
+ },
99
+ role: 'presentation'
100
+ })),
67
101
  contentProps: (0, _store.createSelectorMemoized)(_dimensions.Dimensions.selectors.contentHeight, _dimensions.Dimensions.selectors.minimalContentHeight, _dimensions.Dimensions.selectors.columnsTotalWidth, _dimensions.Dimensions.selectors.needsHorizontalScrollbar, (contentHeight, minimalContentHeight, columnsTotalWidth, needsHorizontalScrollbar) => ({
68
102
  style: {
69
103
  width: needsHorizontalScrollbar ? columnsTotalWidth : 'auto',
70
- flexBasis: contentHeight === 0 ? minimalContentHeight : contentHeight,
71
- flexShrink: 0
104
+ height: contentHeight === 0 ? minimalContentHeight : contentHeight,
105
+ flex: '0 0 auto'
72
106
  },
73
107
  role: 'presentation'
74
108
  })),
75
- positionerProps: (0, _store.createSelectorMemoized)(_virtualization.Virtualization.selectors.offsetTop, offsetTop => ({
109
+ positionerProps: (0, _store.createSelectorMemoized)(_virtualization.Virtualization.selectors.layoutMode, _virtualization.Virtualization.selectors.offsetTop, _virtualization.Virtualization.selectors.scrollPosition, (layoutMode, offsetTop, scrollPosition) => ({
76
110
  style: {
77
- transform: `translate3d(0, ${offsetTop}px, 0)`
111
+ transform: layoutMode === 'uncontrolled' ? `translate3d(0, ${offsetTop}px, 0)` : `translate3d(${-scrollPosition.current.left}px, ${offsetTop - scrollPosition.current.top}px, 0)`
78
112
  }
79
113
  })),
114
+ containerVerticalProps: (0, _store.createSelectorMemoized)(_virtualization.Virtualization.selectors.layoutMode, _virtualization.Virtualization.selectors.scrollPosition, (layoutMode, scrollPosition) => layoutMode === 'uncontrolled' ? undefined : {
115
+ style: {
116
+ transform: `translate3d(${-scrollPosition.current.left}px, 0, 0)`
117
+ }
118
+ }),
80
119
  scrollbarHorizontalProps: (0, _store.createSelectorMemoized)(_virtualization.Virtualization.selectors.context, _virtualization.Virtualization.selectors.scrollPosition, (context, scrollPosition) => ({
81
120
  ref: context.scrollbarHorizontalRef,
82
121
  scrollPosition
@@ -102,19 +141,25 @@ class LayoutDataGridLegacy extends LayoutDataGrid {
102
141
  super.use(store, _params, _api, layoutParams);
103
142
  const containerProps = store.use(LayoutDataGrid.selectors.containerProps);
104
143
  const scrollerProps = store.use(LayoutDataGrid.selectors.scrollerProps);
144
+ const scrollerContentProps = store.use(LayoutDataGrid.selectors.scrollerContentProps);
145
+ const viewportProps = store.use(LayoutDataGrid.selectors.viewportProps);
105
146
  const contentProps = store.use(LayoutDataGrid.selectors.contentProps);
106
147
  const positionerProps = store.use(LayoutDataGrid.selectors.positionerProps);
107
148
  const scrollbarVerticalProps = store.use(LayoutDataGrid.selectors.scrollbarVerticalProps);
108
149
  const scrollbarHorizontalProps = store.use(LayoutDataGrid.selectors.scrollbarHorizontalProps);
109
150
  const scrollAreaProps = store.use(LayoutDataGrid.selectors.scrollAreaProps);
151
+ const containerVerticalProps = store.use(LayoutDataGrid.selectors.containerVerticalProps);
110
152
  return {
111
153
  getContainerProps: () => containerProps,
112
154
  getScrollerProps: () => scrollerProps,
155
+ getScrollerContentProps: () => scrollerContentProps,
156
+ getViewportProps: () => viewportProps,
113
157
  getContentProps: () => contentProps,
114
158
  getPositionerProps: () => positionerProps,
115
159
  getScrollbarVerticalProps: () => scrollbarVerticalProps,
116
160
  getScrollbarHorizontalProps: () => scrollbarHorizontalProps,
117
- getScrollAreaProps: () => scrollAreaProps
161
+ getScrollAreaProps: () => scrollAreaProps,
162
+ getContainerVerticalProps: () => containerVerticalProps
118
163
  };
119
164
  }
120
165
  }
@@ -144,20 +189,18 @@ class LayoutList extends Layout {
144
189
  // https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
145
190
  tabIndex: platform.isFirefox ? -1 : undefined
146
191
  })),
147
- contentProps: (0, _store.createSelectorMemoized)(_dimensions.Dimensions.selectors.contentHeight, contentHeight => {
148
- return {
149
- style: {
150
- position: 'absolute',
151
- display: 'inline-block',
152
- width: '100%',
153
- height: contentHeight,
154
- top: 0,
155
- left: 0,
156
- zIndex: -1
157
- },
158
- role: 'presentation'
159
- };
160
- }),
192
+ contentProps: (0, _store.createSelectorMemoized)(_dimensions.Dimensions.selectors.contentHeight, contentHeight => ({
193
+ style: {
194
+ position: 'absolute',
195
+ display: 'inline-block',
196
+ width: '100%',
197
+ height: contentHeight,
198
+ top: 0,
199
+ left: 0,
200
+ zIndex: -1
201
+ },
202
+ role: 'presentation'
203
+ })),
161
204
  positionerProps: (0, _store.createSelectorMemoized)(_virtualization.Virtualization.selectors.offsetTop, offsetTop => ({
162
205
  style: {
163
206
  height: offsetTop
@@ -216,4 +259,19 @@ function useScrollbarRefCallback(scrollerRef, refSetter, scrollProperty) {
216
259
  scrollbar.removeEventListener('scroll', onScrollbarScroll);
217
260
  };
218
261
  });
262
+ }
263
+ function cssAdd(a, b) {
264
+ if (typeof a === 'number' && typeof b === 'number') {
265
+ return a + b;
266
+ }
267
+ return `calc(${valueToCSSString(a)} + ${valueToCSSString(b)})`;
268
+ }
269
+ function valueToCSSString(value) {
270
+ if (typeof value === 'string') {
271
+ return value;
272
+ }
273
+ if (typeof value === 'undefined') {
274
+ return '0';
275
+ }
276
+ return `${value}px`;
219
277
  }
@@ -25,7 +25,7 @@ export class Layout {
25
25
  }
26
26
  }
27
27
  export class LayoutDataGrid extends Layout {
28
- static elements = (() => ['scroller', 'container', 'content', 'positioner', 'scrollbarVertical', 'scrollbarHorizontal'])();
28
+ static elements = ['scroller', 'container', 'content', 'positioner', 'scrollbarVertical', 'scrollbarHorizontal'];
29
29
  use(store, _params, _api, layoutParams) {
30
30
  const {
31
31
  scrollerRef,
@@ -40,7 +40,7 @@ export class LayoutDataGrid extends Layout {
40
40
  scrollbarHorizontalRef
41
41
  };
42
42
  }
43
- static selectors = (() => ({
43
+ static selectors = {
44
44
  containerProps: createSelectorMemoized(Virtualization.selectors.context, context => ({
45
45
  ref: context.containerRef
46
46
  })),
@@ -57,19 +57,58 @@ export class LayoutDataGrid extends Layout {
57
57
  // https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
58
58
  tabIndex: platform.isFirefox ? -1 : undefined
59
59
  })),
60
+ scrollerContentProps: createSelectorMemoized(Virtualization.selectors.layoutMode, Dimensions.selectors.dimensions, Dimensions.selectors.needsVerticalScrollbar, Dimensions.selectors.needsHorizontalScrollbar, (layoutMode, dimensions, needsVerticalScrollbar, needsHorizontalScrollbar) => {
61
+ let style;
62
+ if (layoutMode === 'controlled') {
63
+ const {
64
+ contentSize,
65
+ scrollbarSize,
66
+ topContainerHeight,
67
+ bottomContainerHeight,
68
+ minimalContentHeight,
69
+ columnsTotalWidth
70
+ } = dimensions;
71
+ const verticalScrollbarSize = needsVerticalScrollbar ? scrollbarSize : 0;
72
+ const horizontalScrollbarSize = needsHorizontalScrollbar ? scrollbarSize : 0;
73
+ const contentHeight = contentSize.height === 0 ? minimalContentHeight : contentSize.height;
74
+ const width = needsHorizontalScrollbar ? verticalScrollbarSize + columnsTotalWidth : 'auto';
75
+ const height = cssAdd(cssAdd(cssAdd(contentHeight, topContainerHeight), bottomContainerHeight), horizontalScrollbarSize);
76
+ style = {
77
+ width,
78
+ height,
79
+ flex: '0 0 auto'
80
+ };
81
+ }
82
+ return {
83
+ style,
84
+ role: 'presentation'
85
+ };
86
+ }),
87
+ viewportProps: createSelectorMemoized(Dimensions.selectors.dimensions, dimensions => ({
88
+ style: {
89
+ width: dimensions.viewportOuterSize.width,
90
+ height: dimensions.viewportOuterSize.height
91
+ },
92
+ role: 'presentation'
93
+ })),
60
94
  contentProps: createSelectorMemoized(Dimensions.selectors.contentHeight, Dimensions.selectors.minimalContentHeight, Dimensions.selectors.columnsTotalWidth, Dimensions.selectors.needsHorizontalScrollbar, (contentHeight, minimalContentHeight, columnsTotalWidth, needsHorizontalScrollbar) => ({
61
95
  style: {
62
96
  width: needsHorizontalScrollbar ? columnsTotalWidth : 'auto',
63
- flexBasis: contentHeight === 0 ? minimalContentHeight : contentHeight,
64
- flexShrink: 0
97
+ height: contentHeight === 0 ? minimalContentHeight : contentHeight,
98
+ flex: '0 0 auto'
65
99
  },
66
100
  role: 'presentation'
67
101
  })),
68
- positionerProps: createSelectorMemoized(Virtualization.selectors.offsetTop, offsetTop => ({
102
+ positionerProps: createSelectorMemoized(Virtualization.selectors.layoutMode, Virtualization.selectors.offsetTop, Virtualization.selectors.scrollPosition, (layoutMode, offsetTop, scrollPosition) => ({
69
103
  style: {
70
- transform: `translate3d(0, ${offsetTop}px, 0)`
104
+ transform: layoutMode === 'uncontrolled' ? `translate3d(0, ${offsetTop}px, 0)` : `translate3d(${-scrollPosition.current.left}px, ${offsetTop - scrollPosition.current.top}px, 0)`
71
105
  }
72
106
  })),
107
+ containerVerticalProps: createSelectorMemoized(Virtualization.selectors.layoutMode, Virtualization.selectors.scrollPosition, (layoutMode, scrollPosition) => layoutMode === 'uncontrolled' ? undefined : {
108
+ style: {
109
+ transform: `translate3d(${-scrollPosition.current.left}px, 0, 0)`
110
+ }
111
+ }),
73
112
  scrollbarHorizontalProps: createSelectorMemoized(Virtualization.selectors.context, Virtualization.selectors.scrollPosition, (context, scrollPosition) => ({
74
113
  ref: context.scrollbarHorizontalRef,
75
114
  scrollPosition
@@ -81,7 +120,7 @@ export class LayoutDataGrid extends Layout {
81
120
  scrollAreaProps: createSelectorMemoized(Virtualization.selectors.scrollPosition, scrollPosition => ({
82
121
  scrollPosition
83
122
  }))
84
- }))();
123
+ };
85
124
  }
86
125
 
87
126
  // The current virtualizer API is exposed on one of the DataGrid slots, so we need to keep
@@ -94,24 +133,30 @@ export class LayoutDataGridLegacy extends LayoutDataGrid {
94
133
  super.use(store, _params, _api, layoutParams);
95
134
  const containerProps = store.use(LayoutDataGrid.selectors.containerProps);
96
135
  const scrollerProps = store.use(LayoutDataGrid.selectors.scrollerProps);
136
+ const scrollerContentProps = store.use(LayoutDataGrid.selectors.scrollerContentProps);
137
+ const viewportProps = store.use(LayoutDataGrid.selectors.viewportProps);
97
138
  const contentProps = store.use(LayoutDataGrid.selectors.contentProps);
98
139
  const positionerProps = store.use(LayoutDataGrid.selectors.positionerProps);
99
140
  const scrollbarVerticalProps = store.use(LayoutDataGrid.selectors.scrollbarVerticalProps);
100
141
  const scrollbarHorizontalProps = store.use(LayoutDataGrid.selectors.scrollbarHorizontalProps);
101
142
  const scrollAreaProps = store.use(LayoutDataGrid.selectors.scrollAreaProps);
143
+ const containerVerticalProps = store.use(LayoutDataGrid.selectors.containerVerticalProps);
102
144
  return {
103
145
  getContainerProps: () => containerProps,
104
146
  getScrollerProps: () => scrollerProps,
147
+ getScrollerContentProps: () => scrollerContentProps,
148
+ getViewportProps: () => viewportProps,
105
149
  getContentProps: () => contentProps,
106
150
  getPositionerProps: () => positionerProps,
107
151
  getScrollbarVerticalProps: () => scrollbarVerticalProps,
108
152
  getScrollbarHorizontalProps: () => scrollbarHorizontalProps,
109
- getScrollAreaProps: () => scrollAreaProps
153
+ getScrollAreaProps: () => scrollAreaProps,
154
+ getContainerVerticalProps: () => containerVerticalProps
110
155
  };
111
156
  }
112
157
  }
113
158
  export class LayoutList extends Layout {
114
- static elements = (() => ['scroller', 'container', 'content', 'positioner'])();
159
+ static elements = ['scroller', 'container', 'content', 'positioner'];
115
160
  use(store, _params, _api, layoutParams) {
116
161
  const {
117
162
  scrollerRef,
@@ -122,7 +167,7 @@ export class LayoutList extends Layout {
122
167
  mergedRef
123
168
  };
124
169
  }
125
- static selectors = (() => ({
170
+ static selectors = {
126
171
  containerProps: createSelectorMemoized(Virtualization.selectors.context, Dimensions.selectors.autoHeight, Dimensions.selectors.needsHorizontalScrollbar, (context, autoHeight, needsHorizontalScrollbar) => ({
127
172
  ref: context.mergedRef,
128
173
  style: {
@@ -135,26 +180,24 @@ export class LayoutList extends Layout {
135
180
  // https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
136
181
  tabIndex: platform.isFirefox ? -1 : undefined
137
182
  })),
138
- contentProps: createSelectorMemoized(Dimensions.selectors.contentHeight, contentHeight => {
139
- return {
140
- style: {
141
- position: 'absolute',
142
- display: 'inline-block',
143
- width: '100%',
144
- height: contentHeight,
145
- top: 0,
146
- left: 0,
147
- zIndex: -1
148
- },
149
- role: 'presentation'
150
- };
151
- }),
183
+ contentProps: createSelectorMemoized(Dimensions.selectors.contentHeight, contentHeight => ({
184
+ style: {
185
+ position: 'absolute',
186
+ display: 'inline-block',
187
+ width: '100%',
188
+ height: contentHeight,
189
+ top: 0,
190
+ left: 0,
191
+ zIndex: -1
192
+ },
193
+ role: 'presentation'
194
+ })),
152
195
  positionerProps: createSelectorMemoized(Virtualization.selectors.offsetTop, offsetTop => ({
153
196
  style: {
154
197
  height: offsetTop
155
198
  }
156
199
  }))
157
- }))();
200
+ };
158
201
  }
159
202
  function useScrollbarRefCallback(scrollerRef, refSetter, scrollProperty) {
160
203
  const isLocked = React.useRef(false);
@@ -206,4 +249,19 @@ function useScrollbarRefCallback(scrollerRef, refSetter, scrollProperty) {
206
249
  scrollbar.removeEventListener('scroll', onScrollbarScroll);
207
250
  };
208
251
  });
252
+ }
253
+ function cssAdd(a, b) {
254
+ if (typeof a === 'number' && typeof b === 'number') {
255
+ return a + b;
256
+ }
257
+ return `calc(${valueToCSSString(a)} + ${valueToCSSString(b)})`;
258
+ }
259
+ function valueToCSSString(value) {
260
+ if (typeof value === 'string') {
261
+ return value;
262
+ }
263
+ if (typeof value === 'undefined') {
264
+ return '0';
265
+ }
266
+ return `${value}px`;
209
267
  }
@@ -15,6 +15,13 @@ export type VirtualizationParams = {
15
15
  /** The column buffer in pixels to render before and after the viewport.
16
16
  * @default 150 */
17
17
  columnBufferPx?: number;
18
+ /**
19
+ * Controls how the container and render zones are positioned:
20
+ * - 'uncontrolled': uses CSS sticky positioning (default)
21
+ * - 'controlled': uses CSS absolute positioning with JS-computed offsets
22
+ * @default 'uncontrolled'
23
+ */
24
+ layoutMode?: 'controlled' | 'uncontrolled';
18
25
  };
19
26
  export type VirtualizationState<K extends string = string> = {
20
27
  enabled: boolean;
@@ -26,6 +33,7 @@ export type VirtualizationState<K extends string = string> = {
26
33
  scrollPosition: {
27
34
  current: ScrollPosition;
28
35
  };
36
+ layoutMode: 'controlled' | 'uncontrolled';
29
37
  };
30
38
  export declare const EMPTY_RENDER_CONTEXT: {
31
39
  firstRowIndex: number;
@@ -46,9 +54,18 @@ export declare const Virtualization: {
46
54
  container: React.RefObject<HTMLElement | null>;
47
55
  } & Record<string, React.RefObject<HTMLElement | null>>>> & Dimensions.State) => number;
48
56
  context: (state: BaseState) => Record<string, any>;
57
+ layoutMode: (state: BaseState) => "controlled" | "uncontrolled";
49
58
  scrollPosition: (state: BaseState) => {
50
59
  current: ScrollPosition;
51
60
  };
61
+ pinnedLeftOffsetSelector: (args_0: Virtualization.State<Layout<{
62
+ scroller: React.RefObject<HTMLElement | null>;
63
+ container: React.RefObject<HTMLElement | null>;
64
+ } & Record<string, React.RefObject<HTMLElement | null>>>> & Dimensions.State) => number;
65
+ pinnedRightOffsetSelector: (args_0: Virtualization.State<Layout<{
66
+ scroller: React.RefObject<HTMLElement | null>;
67
+ container: React.RefObject<HTMLElement | null>;
68
+ } & Record<string, React.RefObject<HTMLElement | null>>>> & Dimensions.State) => number;
52
69
  };
53
70
  };
54
71
  export declare namespace Virtualization {
@@ -83,6 +100,6 @@ declare function useVirtualization(store: Store<BaseState>, params: ParamsWithDe
83
100
  scheduleUpdateRenderContext: () => void;
84
101
  };
85
102
  export declare function areRenderContextsEqual(context1: RenderContext, context2: RenderContext): boolean;
86
- export declare function computeOffsetLeft(columnPositions: number[], renderContext: ColumnsRenderContext, pinnedLeftLength: number): number;
103
+ export declare function computeOffsetLeft(columnPositions: number[], renderContext: ColumnsRenderContext, pinnedLeftLength: number, layoutMode?: VirtualizationState['layoutMode']): number;
87
104
  export declare function roundToDecimalPlaces(value: number, decimals: number): number;
88
105
  export {};
@@ -15,6 +15,13 @@ export type VirtualizationParams = {
15
15
  /** The column buffer in pixels to render before and after the viewport.
16
16
  * @default 150 */
17
17
  columnBufferPx?: number;
18
+ /**
19
+ * Controls how the container and render zones are positioned:
20
+ * - 'uncontrolled': uses CSS sticky positioning (default)
21
+ * - 'controlled': uses CSS absolute positioning with JS-computed offsets
22
+ * @default 'uncontrolled'
23
+ */
24
+ layoutMode?: 'controlled' | 'uncontrolled';
18
25
  };
19
26
  export type VirtualizationState<K extends string = string> = {
20
27
  enabled: boolean;
@@ -26,6 +33,7 @@ export type VirtualizationState<K extends string = string> = {
26
33
  scrollPosition: {
27
34
  current: ScrollPosition;
28
35
  };
36
+ layoutMode: 'controlled' | 'uncontrolled';
29
37
  };
30
38
  export declare const EMPTY_RENDER_CONTEXT: {
31
39
  firstRowIndex: number;
@@ -46,9 +54,18 @@ export declare const Virtualization: {
46
54
  container: React.RefObject<HTMLElement | null>;
47
55
  } & Record<string, React.RefObject<HTMLElement | null>>>> & Dimensions.State) => number;
48
56
  context: (state: BaseState) => Record<string, any>;
57
+ layoutMode: (state: BaseState) => "controlled" | "uncontrolled";
49
58
  scrollPosition: (state: BaseState) => {
50
59
  current: ScrollPosition;
51
60
  };
61
+ pinnedLeftOffsetSelector: (args_0: Virtualization.State<Layout<{
62
+ scroller: React.RefObject<HTMLElement | null>;
63
+ container: React.RefObject<HTMLElement | null>;
64
+ } & Record<string, React.RefObject<HTMLElement | null>>>> & Dimensions.State) => number;
65
+ pinnedRightOffsetSelector: (args_0: Virtualization.State<Layout<{
66
+ scroller: React.RefObject<HTMLElement | null>;
67
+ container: React.RefObject<HTMLElement | null>;
68
+ } & Record<string, React.RefObject<HTMLElement | null>>>> & Dimensions.State) => number;
52
69
  };
53
70
  };
54
71
  export declare namespace Virtualization {
@@ -83,6 +100,6 @@ declare function useVirtualization(store: Store<BaseState>, params: ParamsWithDe
83
100
  scheduleUpdateRenderContext: () => void;
84
101
  };
85
102
  export declare function areRenderContextsEqual(context1: RenderContext, context2: RenderContext): boolean;
86
- export declare function computeOffsetLeft(columnPositions: number[], renderContext: ColumnsRenderContext, pinnedLeftLength: number): number;
103
+ export declare function computeOffsetLeft(columnPositions: number[], renderContext: ColumnsRenderContext, pinnedLeftLength: number, layoutMode?: VirtualizationState['layoutMode']): number;
87
104
  export declare function roundToDecimalPlaces(value: number, decimals: number): number;
88
105
  export {};
@@ -42,14 +42,23 @@ const EMPTY_RENDER_CONTEXT = exports.EMPTY_RENDER_CONTEXT = {
42
42
  };
43
43
  const selectors = (() => {
44
44
  const firstRowIndexSelector = (0, _store.createSelector)(state => state.virtualization.renderContext.firstRowIndex);
45
+ const scrollPositionSelector = (0, _store.createSelector)(state => state.virtualization.scrollPosition);
46
+ const layoutModeSelector = (0, _store.createSelector)(state => state.virtualization.layoutMode);
45
47
  return {
46
48
  store: (0, _store.createSelector)(state => state.virtualization),
47
49
  renderContext: (0, _store.createSelector)(state => state.virtualization.renderContext),
48
50
  enabledForRows: (0, _store.createSelector)(state => state.virtualization.enabledForRows),
49
51
  enabledForColumns: (0, _store.createSelector)(state => state.virtualization.enabledForColumns),
50
- offsetTop: (0, _store.createSelector)(_dimensions.Dimensions.selectors.rowPositions, firstRowIndexSelector, (rowPositions, firstRowIndex) => rowPositions[firstRowIndex] ?? 0),
52
+ offsetTop: (0, _store.createSelector)(layoutModeSelector, _dimensions.Dimensions.selectors.dimensions, _dimensions.Dimensions.selectors.rowPositions, firstRowIndexSelector, (layoutMode, dimensions, rowPositions, firstRowIndex) => {
53
+ return (layoutMode === 'uncontrolled' ? dimensions.topContainerHeight : 0) + (rowPositions[firstRowIndex] ?? 0);
54
+ }),
51
55
  context: (0, _store.createSelector)(state => state.virtualization.context),
52
- scrollPosition: (0, _store.createSelector)(state => state.virtualization.scrollPosition)
56
+ layoutMode: layoutModeSelector,
57
+ scrollPosition: scrollPositionSelector,
58
+ pinnedLeftOffsetSelector: (0, _store.createSelector)(scrollPositionSelector, scrollPosition => scrollPosition.current.left),
59
+ pinnedRightOffsetSelector: (0, _store.createSelector)(scrollPositionSelector, _dimensions.Dimensions.selectors.dimensions, _dimensions.Dimensions.selectors.columnsTotalWidth, _dimensions.Dimensions.selectors.needsVerticalScrollbar, (scrollPosition, dimensions, columnsTotalWidth, needsVerticalScrollbar) => {
60
+ return Math.max(columnsTotalWidth, dimensions.viewportOuterSize.width) - dimensions.viewportOuterSize.width - scrollPosition.current.left + (needsVerticalScrollbar ? dimensions.scrollbarSize : 0);
61
+ })
53
62
  };
54
63
  })();
55
64
  const Virtualization = exports.Virtualization = {
@@ -68,7 +77,8 @@ function initializeState(params) {
68
77
  context: {},
69
78
  scrollPosition: {
70
79
  current: _models.ScrollPosition.EMPTY
71
- }
80
+ },
81
+ layoutMode: params.virtualization.layoutMode ?? 'uncontrolled'
72
82
  }, params.initialState?.virtualization),
73
83
  // FIXME: refactor once the state shape is settled
74
84
  getters: null
@@ -365,7 +375,7 @@ function useVirtualization(store, params, api) {
365
375
  }
366
376
  const isVirtualFocusRow = rowIndexInPage === virtualRowIndex;
367
377
  const isVirtualFocusColumn = focusedVirtualCell?.rowIndex === rowIndex;
368
- const offsetLeft = computeOffsetLeft(columnPositions, currentRenderContext, pinnedColumns.left.length);
378
+ const offsetLeft = computeOffsetLeft(columnPositions, currentRenderContext, pinnedColumns.left.length, store.state.virtualization.layoutMode);
369
379
  const showBottomBorder = isLastVisibleInSection && rowParams.position === 'top';
370
380
  const firstColumnIndex = currentRenderContext.firstColumnIndex;
371
381
  const lastColumnIndex = currentRenderContext.lastColumnIndex;
@@ -735,9 +745,14 @@ function areRenderContextsEqual(context1, context2) {
735
745
  }
736
746
  return context1.firstRowIndex === context2.firstRowIndex && context1.lastRowIndex === context2.lastRowIndex && context1.firstColumnIndex === context2.firstColumnIndex && context1.lastColumnIndex === context2.lastColumnIndex;
737
747
  }
738
- function computeOffsetLeft(columnPositions, renderContext, pinnedLeftLength) {
739
- const left = (columnPositions[renderContext.firstColumnIndex] ?? 0) - (columnPositions[pinnedLeftLength] ?? 0);
740
- return Math.abs(left);
748
+ function computeOffsetLeft(columnPositions, renderContext, pinnedLeftLength, layoutMode = 'uncontrolled') {
749
+ let offset = columnPositions[renderContext.firstColumnIndex] ?? 0;
750
+ /* CSS sticky leaves elements in the normal flow of the DOM, so we
751
+ * don't need to add the offset of the pinned columns. */
752
+ if (layoutMode === 'uncontrolled') {
753
+ offset -= columnPositions[pinnedLeftLength] ?? 0;
754
+ }
755
+ return Math.abs(offset);
741
756
  }
742
757
  function bufferForDirection(isRtl, direction, rowBufferPx, columnBufferPx, verticalBuffer, horizontalBuffer) {
743
758
  if (isRtl) {
@@ -33,14 +33,23 @@ export const EMPTY_RENDER_CONTEXT = {
33
33
  };
34
34
  const selectors = (() => {
35
35
  const firstRowIndexSelector = createSelector(state => state.virtualization.renderContext.firstRowIndex);
36
+ const scrollPositionSelector = createSelector(state => state.virtualization.scrollPosition);
37
+ const layoutModeSelector = createSelector(state => state.virtualization.layoutMode);
36
38
  return {
37
39
  store: createSelector(state => state.virtualization),
38
40
  renderContext: createSelector(state => state.virtualization.renderContext),
39
41
  enabledForRows: createSelector(state => state.virtualization.enabledForRows),
40
42
  enabledForColumns: createSelector(state => state.virtualization.enabledForColumns),
41
- offsetTop: createSelector(Dimensions.selectors.rowPositions, firstRowIndexSelector, (rowPositions, firstRowIndex) => rowPositions[firstRowIndex] ?? 0),
43
+ offsetTop: createSelector(layoutModeSelector, Dimensions.selectors.dimensions, Dimensions.selectors.rowPositions, firstRowIndexSelector, (layoutMode, dimensions, rowPositions, firstRowIndex) => {
44
+ return (layoutMode === 'uncontrolled' ? dimensions.topContainerHeight : 0) + (rowPositions[firstRowIndex] ?? 0);
45
+ }),
42
46
  context: createSelector(state => state.virtualization.context),
43
- scrollPosition: createSelector(state => state.virtualization.scrollPosition)
47
+ layoutMode: layoutModeSelector,
48
+ scrollPosition: scrollPositionSelector,
49
+ pinnedLeftOffsetSelector: createSelector(scrollPositionSelector, scrollPosition => scrollPosition.current.left),
50
+ pinnedRightOffsetSelector: createSelector(scrollPositionSelector, Dimensions.selectors.dimensions, Dimensions.selectors.columnsTotalWidth, Dimensions.selectors.needsVerticalScrollbar, (scrollPosition, dimensions, columnsTotalWidth, needsVerticalScrollbar) => {
51
+ return Math.max(columnsTotalWidth, dimensions.viewportOuterSize.width) - dimensions.viewportOuterSize.width - scrollPosition.current.left + (needsVerticalScrollbar ? dimensions.scrollbarSize : 0);
52
+ })
44
53
  };
45
54
  })();
46
55
  export const Virtualization = {
@@ -59,7 +68,8 @@ function initializeState(params) {
59
68
  context: {},
60
69
  scrollPosition: {
61
70
  current: ScrollPosition.EMPTY
62
- }
71
+ },
72
+ layoutMode: params.virtualization.layoutMode ?? 'uncontrolled'
63
73
  }, params.initialState?.virtualization),
64
74
  // FIXME: refactor once the state shape is settled
65
75
  getters: null
@@ -356,7 +366,7 @@ function useVirtualization(store, params, api) {
356
366
  }
357
367
  const isVirtualFocusRow = rowIndexInPage === virtualRowIndex;
358
368
  const isVirtualFocusColumn = focusedVirtualCell?.rowIndex === rowIndex;
359
- const offsetLeft = computeOffsetLeft(columnPositions, currentRenderContext, pinnedColumns.left.length);
369
+ const offsetLeft = computeOffsetLeft(columnPositions, currentRenderContext, pinnedColumns.left.length, store.state.virtualization.layoutMode);
360
370
  const showBottomBorder = isLastVisibleInSection && rowParams.position === 'top';
361
371
  const firstColumnIndex = currentRenderContext.firstColumnIndex;
362
372
  const lastColumnIndex = currentRenderContext.lastColumnIndex;
@@ -726,9 +736,14 @@ export function areRenderContextsEqual(context1, context2) {
726
736
  }
727
737
  return context1.firstRowIndex === context2.firstRowIndex && context1.lastRowIndex === context2.lastRowIndex && context1.firstColumnIndex === context2.firstColumnIndex && context1.lastColumnIndex === context2.lastColumnIndex;
728
738
  }
729
- export function computeOffsetLeft(columnPositions, renderContext, pinnedLeftLength) {
730
- const left = (columnPositions[renderContext.firstColumnIndex] ?? 0) - (columnPositions[pinnedLeftLength] ?? 0);
731
- return Math.abs(left);
739
+ export function computeOffsetLeft(columnPositions, renderContext, pinnedLeftLength, layoutMode = 'uncontrolled') {
740
+ let offset = columnPositions[renderContext.firstColumnIndex] ?? 0;
741
+ /* CSS sticky leaves elements in the normal flow of the DOM, so we
742
+ * don't need to add the offset of the pinned columns. */
743
+ if (layoutMode === 'uncontrolled') {
744
+ offset -= columnPositions[pinnedLeftLength] ?? 0;
745
+ }
746
+ return Math.abs(offset);
732
747
  }
733
748
  function bufferForDirection(isRtl, direction, rowBufferPx, columnBufferPx, verticalBuffer, horizontalBuffer) {
734
749
  if (isRtl) {
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-virtualizer v1.0.0-beta.0
2
+ * @mui/x-virtualizer v1.0.0-rc.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-virtualizer v1.0.0-beta.0
2
+ * @mui/x-virtualizer v1.0.0-rc.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-virtualizer",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-rc.0",
4
4
  "author": "MUI Team",
5
5
  "description": "MUI virtualization library",
6
6
  "license": "MIT",
@@ -28,8 +28,8 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@babel/runtime": "^7.28.6",
31
- "@mui/utils": "^7.3.7",
32
- "@mui/x-internals": "9.0.0-alpha.4"
31
+ "@mui/utils": "9.0.0-beta.1",
32
+ "@mui/x-internals": "9.0.0-rc.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^17.0.0 || ^18.0.0 || ^19.0.0",