@mui/x-virtualizer 0.1.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +10645 -0
  2. package/README.md +3 -0
  3. package/esm/features/colspan.d.ts +28 -0
  4. package/esm/features/colspan.js +88 -0
  5. package/esm/features/dimensions.d.ts +41 -0
  6. package/esm/features/dimensions.js +465 -0
  7. package/esm/features/index.d.ts +5 -0
  8. package/esm/features/index.js +5 -0
  9. package/esm/features/keyboard.d.ts +16 -0
  10. package/esm/features/keyboard.js +35 -0
  11. package/esm/features/rowspan.d.ts +25 -0
  12. package/esm/features/rowspan.js +36 -0
  13. package/esm/features/virtualization.d.ts +103 -0
  14. package/esm/features/virtualization.js +844 -0
  15. package/esm/index.d.ts +2 -0
  16. package/esm/index.js +9 -0
  17. package/esm/models/colspan.d.ts +13 -0
  18. package/esm/models/colspan.js +1 -0
  19. package/esm/models/core.d.ts +72 -0
  20. package/esm/models/core.js +43 -0
  21. package/esm/models/dimensions.d.ts +132 -0
  22. package/esm/models/dimensions.js +1 -0
  23. package/esm/models/index.d.ts +4 -0
  24. package/esm/models/index.js +4 -0
  25. package/esm/models/rowspan.d.ts +17 -0
  26. package/esm/models/rowspan.js +1 -0
  27. package/esm/package.json +1 -0
  28. package/esm/useVirtualizer.d.ts +193 -0
  29. package/esm/useVirtualizer.js +26 -0
  30. package/features/colspan.d.ts +28 -0
  31. package/features/colspan.js +94 -0
  32. package/features/dimensions.d.ts +41 -0
  33. package/features/dimensions.js +472 -0
  34. package/features/index.d.ts +5 -0
  35. package/features/index.js +60 -0
  36. package/features/keyboard.d.ts +16 -0
  37. package/features/keyboard.js +40 -0
  38. package/features/rowspan.d.ts +25 -0
  39. package/features/rowspan.js +42 -0
  40. package/features/virtualization.d.ts +103 -0
  41. package/features/virtualization.js +853 -0
  42. package/index.d.ts +2 -0
  43. package/index.js +34 -0
  44. package/models/colspan.d.ts +13 -0
  45. package/models/colspan.js +5 -0
  46. package/models/core.d.ts +72 -0
  47. package/models/core.js +49 -0
  48. package/models/dimensions.d.ts +132 -0
  49. package/models/dimensions.js +5 -0
  50. package/models/index.d.ts +4 -0
  51. package/models/index.js +49 -0
  52. package/models/rowspan.d.ts +17 -0
  53. package/models/rowspan.js +5 -0
  54. package/package.json +67 -0
  55. package/useVirtualizer.d.ts +193 -0
  56. package/useVirtualizer.js +33 -0
@@ -0,0 +1,844 @@
1
+ 'use client';
2
+
3
+ import _extends from "@babel/runtime/helpers/esm/extends";
4
+ import * as React from 'react';
5
+ import * as ReactDOM from 'react-dom';
6
+ import useLazyRef from '@mui/utils/useLazyRef';
7
+ import useTimeout from '@mui/utils/useTimeout';
8
+ import useEventCallback from '@mui/utils/useEventCallback';
9
+ import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
10
+ import * as platform from '@mui/x-internals/platform';
11
+ import { useRunOnce } from '@mui/x-internals/useRunOnce';
12
+ import { useFirstRender } from '@mui/x-internals/useFirstRender';
13
+ import { createSelector, useStore, useStoreEffect } from '@mui/x-internals/store';
14
+ import { Dimensions } from "./dimensions.js";
15
+ import { ScrollDirection } from "../models/index.js";
16
+
17
+ /* eslint-disable import/export, @typescript-eslint/no-redeclare */
18
+
19
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
20
+ const MINIMUM_COLUMN_WIDTH = 50;
21
+ const EMPTY_SCROLL_POSITION = {
22
+ top: 0,
23
+ left: 0
24
+ };
25
+ const EMPTY_DETAIL_PANELS = Object.freeze(new Map());
26
+ export const EMPTY_RENDER_CONTEXT = {
27
+ firstRowIndex: 0,
28
+ lastRowIndex: 0,
29
+ firstColumnIndex: 0,
30
+ lastColumnIndex: 0
31
+ };
32
+ const selectors = {
33
+ renderContext: createSelector(state => state.virtualization.renderContext),
34
+ enabledForRows: createSelector(state => state.virtualization.enabledForRows),
35
+ enabledForColumns: createSelector(state => state.virtualization.enabledForColumns)
36
+ };
37
+ export const Virtualization = {
38
+ initialize: initializeState,
39
+ use: useVirtualization,
40
+ selectors
41
+ };
42
+ function initializeState(params) {
43
+ const state = {
44
+ virtualization: _extends({
45
+ enabled: !platform.isJSDOM,
46
+ enabledForRows: !platform.isJSDOM,
47
+ enabledForColumns: !platform.isJSDOM,
48
+ renderContext: EMPTY_RENDER_CONTEXT
49
+ }, params.initialState?.virtualization),
50
+ // FIXME: refactor once the state shape is settled
51
+ getters: null
52
+ };
53
+ return state;
54
+ }
55
+
56
+ /** APIs to override for colspan/rowspan */
57
+
58
+ function useVirtualization(store, params, api) {
59
+ const {
60
+ initialState,
61
+ isRtl,
62
+ rows,
63
+ range,
64
+ columns,
65
+ pinnedRows,
66
+ pinnedColumns,
67
+ refs,
68
+ hasColSpan,
69
+ dimensions: {
70
+ rowHeight,
71
+ columnsTotalWidth
72
+ },
73
+ contentHeight,
74
+ minimalContentHeight,
75
+ autoHeight,
76
+ onWheel,
77
+ onTouchMove,
78
+ onRenderContextChange,
79
+ onScrollChange,
80
+ rowBufferPx,
81
+ columnBufferPx,
82
+ scrollReset,
83
+ renderRow,
84
+ renderInfiniteLoadingTrigger
85
+ } = params;
86
+ const needsHorizontalScrollbar = useStore(store, Dimensions.selectors.needsHorizontalScrollbar);
87
+ const hasBottomPinnedRows = pinnedRows.bottom.length > 0;
88
+ const [panels, setPanels] = React.useState(EMPTY_DETAIL_PANELS);
89
+ const isRenderContextReady = React.useRef(false);
90
+ const renderContext = useStore(store, selectors.renderContext);
91
+ const enabledForRows = useStore(store, selectors.enabledForRows);
92
+ const enabledForColumns = useStore(store, selectors.enabledForColumns);
93
+
94
+ /*
95
+ * Scroll context logic
96
+ * ====================
97
+ * We only render the cells contained in the `renderContext`. However, when the user starts scrolling the grid
98
+ * in a direction, we want to render as many cells as possible in that direction, as to avoid presenting white
99
+ * areas if the user scrolls too fast/far and the viewport ends up in a region we haven't rendered yet. To render
100
+ * more cells, we store some offsets to add to the viewport in `scrollCache.buffer`. Those offsets make the render
101
+ * context wider in the direction the user is going, but also makes the buffer around the viewport `0` for the
102
+ * dimension (horizontal or vertical) in which the user is not scrolling. So if the normal viewport is 8 columns
103
+ * wide, with a 1 column buffer (10 columns total), then we want it to be exactly 8 columns wide during vertical
104
+ * scroll.
105
+ * However, we don't want the rows in the old context to re-render from e.g. 10 columns to 8 columns, because that's
106
+ * work that's not necessary. Thus we store the context at the start of the scroll in `frozenContext`, and the rows
107
+ * that are part of this old context will keep their same render context as to avoid re-rendering.
108
+ */
109
+ const scrollPosition = React.useRef(initialState?.scroll ?? EMPTY_SCROLL_POSITION);
110
+ const ignoreNextScrollEvent = React.useRef(false);
111
+ const previousContextScrollPosition = React.useRef(EMPTY_SCROLL_POSITION);
112
+ const previousRowContext = React.useRef(EMPTY_RENDER_CONTEXT);
113
+ const scrollTimeout = useTimeout();
114
+ const frozenContext = React.useRef(undefined);
115
+ const scrollCache = useLazyRef(() => createScrollCache(isRtl, rowBufferPx, columnBufferPx, rowHeight * 15, MINIMUM_COLUMN_WIDTH * 6)).current;
116
+ const updateRenderContext = React.useCallback(nextRenderContext => {
117
+ if (areRenderContextsEqual(nextRenderContext, store.state.virtualization.renderContext)) {
118
+ return;
119
+ }
120
+ const didRowsIntervalChange = nextRenderContext.firstRowIndex !== previousRowContext.current.firstRowIndex || nextRenderContext.lastRowIndex !== previousRowContext.current.lastRowIndex;
121
+ store.set('virtualization', _extends({}, store.state.virtualization, {
122
+ renderContext: nextRenderContext
123
+ }));
124
+
125
+ // The lazy-loading hook is listening to `renderedRowsIntervalChange`,
126
+ // but only does something if we already have a render context, because
127
+ // otherwise we would call an update directly on mount
128
+ const isReady = Dimensions.selectors.dimensions(store.state).isReady;
129
+ if (isReady && didRowsIntervalChange) {
130
+ previousRowContext.current = nextRenderContext;
131
+ onRenderContextChange?.(nextRenderContext);
132
+ }
133
+ previousContextScrollPosition.current = scrollPosition.current;
134
+ }, [store, onRenderContextChange]);
135
+ const triggerUpdateRenderContext = useEventCallback(() => {
136
+ const scroller = refs.scroller.current;
137
+ if (!scroller) {
138
+ return undefined;
139
+ }
140
+ const dimensions = Dimensions.selectors.dimensions(store.state);
141
+ const maxScrollTop = Math.ceil(dimensions.minimumSize.height - dimensions.viewportOuterSize.height);
142
+ const maxScrollLeft = Math.ceil(dimensions.minimumSize.width - dimensions.viewportInnerSize.width);
143
+
144
+ // Clamp the scroll position to the viewport to avoid re-calculating the render context for scroll bounce
145
+ const newScroll = {
146
+ top: clamp(scroller.scrollTop, 0, maxScrollTop),
147
+ left: isRtl ? clamp(scroller.scrollLeft, -maxScrollLeft, 0) : clamp(scroller.scrollLeft, 0, maxScrollLeft)
148
+ };
149
+ const dx = newScroll.left - scrollPosition.current.left;
150
+ const dy = newScroll.top - scrollPosition.current.top;
151
+ const isScrolling = dx !== 0 || dy !== 0;
152
+ scrollPosition.current = newScroll;
153
+ const direction = isScrolling ? directionForDelta(dx, dy) : ScrollDirection.NONE;
154
+
155
+ // Since previous render, we have scrolled...
156
+ const rowScroll = Math.abs(scrollPosition.current.top - previousContextScrollPosition.current.top);
157
+ const columnScroll = Math.abs(scrollPosition.current.left - previousContextScrollPosition.current.left);
158
+
159
+ // PERF: use the computed minimum column width instead of a static one
160
+ const didCrossThreshold = rowScroll >= rowHeight || columnScroll >= MINIMUM_COLUMN_WIDTH;
161
+ const didChangeDirection = scrollCache.direction !== direction;
162
+ const shouldUpdate = didCrossThreshold || didChangeDirection;
163
+ if (!shouldUpdate) {
164
+ return renderContext;
165
+ }
166
+
167
+ // Render a new context
168
+
169
+ if (didChangeDirection) {
170
+ switch (direction) {
171
+ case ScrollDirection.NONE:
172
+ case ScrollDirection.LEFT:
173
+ case ScrollDirection.RIGHT:
174
+ frozenContext.current = undefined;
175
+ break;
176
+ default:
177
+ frozenContext.current = renderContext;
178
+ break;
179
+ }
180
+ }
181
+ scrollCache.direction = direction;
182
+ scrollCache.buffer = bufferForDirection(isRtl, direction, rowBufferPx, columnBufferPx, rowHeight * 15, MINIMUM_COLUMN_WIDTH * 6);
183
+ const inputs = inputsSelector(store, params, api, enabledForRows, enabledForColumns);
184
+ const nextRenderContext = computeRenderContext(inputs, scrollPosition.current, scrollCache);
185
+ if (!areRenderContextsEqual(nextRenderContext, renderContext)) {
186
+ // Prevents batching render context changes
187
+ ReactDOM.flushSync(() => {
188
+ updateRenderContext(nextRenderContext);
189
+ });
190
+ scrollTimeout.start(1000, triggerUpdateRenderContext);
191
+ }
192
+ return nextRenderContext;
193
+ });
194
+ const forceUpdateRenderContext = useEventCallback(() => {
195
+ // skip update if dimensions are not ready and virtualization is enabled
196
+ if (!Dimensions.selectors.dimensions(store.state).isReady && (enabledForRows || enabledForColumns)) {
197
+ return;
198
+ }
199
+ const inputs = inputsSelector(store, params, api, enabledForRows, enabledForColumns);
200
+ const nextRenderContext = computeRenderContext(inputs, scrollPosition.current, scrollCache);
201
+ // Reset the frozen context when the render context changes, see the illustration in https://github.com/mui/mui-x/pull/12353
202
+ frozenContext.current = undefined;
203
+ updateRenderContext(nextRenderContext);
204
+ });
205
+ const handleScroll = useEventCallback(() => {
206
+ if (ignoreNextScrollEvent.current) {
207
+ ignoreNextScrollEvent.current = false;
208
+ return;
209
+ }
210
+ const nextRenderContext = triggerUpdateRenderContext();
211
+ if (nextRenderContext) {
212
+ onScrollChange?.(scrollPosition.current, nextRenderContext);
213
+ }
214
+ });
215
+
216
+ /**
217
+ * HACK: unstable_rowTree fixes the issue described below, but does it by tightly coupling this
218
+ * section of code to the DataGrid's rowTree model. The `unstable_rowTree` param is a temporary
219
+ * solution to decouple the code.
220
+ */
221
+ const getRows = (
222
+ // eslint-disable-next-line @typescript-eslint/default-param-last
223
+ rowParams = {}, unstable_rowTree) => {
224
+ if (!rowParams.rows && !range) {
225
+ return [];
226
+ }
227
+ let baseRenderContext = renderContext;
228
+ if (rowParams.renderContext) {
229
+ baseRenderContext = rowParams.renderContext;
230
+ baseRenderContext.firstColumnIndex = renderContext.firstColumnIndex;
231
+ baseRenderContext.lastColumnIndex = renderContext.lastColumnIndex;
232
+ }
233
+ const isLastSection = !hasBottomPinnedRows && rowParams.position === undefined || hasBottomPinnedRows && rowParams.position === 'bottom';
234
+ const isPinnedSection = rowParams.position !== undefined;
235
+ let rowIndexOffset;
236
+ switch (rowParams.position) {
237
+ case 'top':
238
+ rowIndexOffset = 0;
239
+ break;
240
+ case 'bottom':
241
+ rowIndexOffset = pinnedRows.top.length + rows.length;
242
+ break;
243
+ case undefined:
244
+ default:
245
+ rowIndexOffset = pinnedRows.top.length;
246
+ break;
247
+ }
248
+ const rowModels = rowParams.rows ?? rows;
249
+ const firstRowToRender = baseRenderContext.firstRowIndex;
250
+ const lastRowToRender = Math.min(baseRenderContext.lastRowIndex, rowModels.length);
251
+ const rowIndexes = rowParams.rows ? createRange(0, rowParams.rows.length) : createRange(firstRowToRender, lastRowToRender);
252
+ let virtualRowIndex = -1;
253
+ const focusedVirtualCell = params.focusedVirtualCell();
254
+ if (!isPinnedSection && focusedVirtualCell) {
255
+ if (focusedVirtualCell.rowIndex < firstRowToRender) {
256
+ rowIndexes.unshift(focusedVirtualCell.rowIndex);
257
+ virtualRowIndex = focusedVirtualCell.rowIndex;
258
+ }
259
+ if (focusedVirtualCell.rowIndex > lastRowToRender) {
260
+ rowIndexes.push(focusedVirtualCell.rowIndex);
261
+ virtualRowIndex = focusedVirtualCell.rowIndex;
262
+ }
263
+ }
264
+ const rowElements = [];
265
+ const columnPositions = Dimensions.selectors.columnPositions(store.state, columns);
266
+ rowIndexes.forEach(rowIndexInPage => {
267
+ const {
268
+ id,
269
+ model
270
+ } = rowModels[rowIndexInPage];
271
+
272
+ // In certain cases, the state might already be updated and `params.rows` (which sets `rowModels`)
273
+ // contains stale data.
274
+ // In that case, skip any further row processing.
275
+ // See:
276
+ // - https://github.com/mui/mui-x/issues/16638
277
+ // - https://github.com/mui/mui-x/issues/17022
278
+ if (!unstable_rowTree[id]) {
279
+ return;
280
+ }
281
+ const rowIndex = (range?.firstRowIndex || 0) + rowIndexOffset + rowIndexInPage;
282
+
283
+ // NOTE: This is an expensive feature, the colSpan code could be optimized.
284
+ if (hasColSpan) {
285
+ const minFirstColumn = pinnedColumns.left.length;
286
+ const maxLastColumn = columns.length - pinnedColumns.right.length;
287
+ api.calculateColSpan(id, minFirstColumn, maxLastColumn, columns);
288
+ if (pinnedColumns.left.length > 0) {
289
+ api.calculateColSpan(id, 0, pinnedColumns.left.length, columns);
290
+ }
291
+ if (pinnedColumns.right.length > 0) {
292
+ api.calculateColSpan(id, columns.length - pinnedColumns.right.length, columns.length, columns);
293
+ }
294
+ }
295
+ const baseRowHeight = !api.rowsMeta.rowHasAutoHeight(id) ? api.rowsMeta.getRowHeight(id) : 'auto';
296
+ let isFirstVisible = false;
297
+ if (rowParams.position === undefined) {
298
+ isFirstVisible = rowIndexInPage === 0;
299
+ }
300
+ let isLastVisible = false;
301
+ const isLastVisibleInSection = rowIndexInPage === rowModels.length - 1;
302
+ if (isLastSection) {
303
+ if (!isPinnedSection) {
304
+ const lastIndex = rows.length - 1;
305
+ const isLastVisibleRowIndex = rowIndexInPage === lastIndex;
306
+ if (isLastVisibleRowIndex) {
307
+ isLastVisible = true;
308
+ }
309
+ } else {
310
+ isLastVisible = isLastVisibleInSection;
311
+ }
312
+ }
313
+ let currentRenderContext = baseRenderContext;
314
+ if (frozenContext.current && rowIndexInPage >= frozenContext.current.firstRowIndex && rowIndexInPage < frozenContext.current.lastRowIndex) {
315
+ currentRenderContext = frozenContext.current;
316
+ }
317
+ const isVirtualFocusRow = rowIndexInPage === virtualRowIndex;
318
+ const isVirtualFocusColumn = focusedVirtualCell?.rowIndex === rowIndex;
319
+ const offsetLeft = computeOffsetLeft(columnPositions, currentRenderContext, pinnedColumns.left.length);
320
+ const showBottomBorder = isLastVisibleInSection && rowParams.position === 'top';
321
+ const firstColumnIndex = currentRenderContext.firstColumnIndex;
322
+ const lastColumnIndex = currentRenderContext.lastColumnIndex;
323
+ rowElements.push(renderRow({
324
+ id,
325
+ model,
326
+ rowIndex,
327
+ offsetLeft,
328
+ columnsTotalWidth,
329
+ baseRowHeight,
330
+ columns,
331
+ firstColumnIndex,
332
+ lastColumnIndex,
333
+ focusedColumnIndex: isVirtualFocusColumn ? focusedVirtualCell.columnIndex : undefined,
334
+ isFirstVisible,
335
+ isLastVisible,
336
+ isVirtualFocusRow,
337
+ showBottomBorder
338
+ }));
339
+ if (isVirtualFocusRow) {
340
+ return;
341
+ }
342
+ const panel = panels.get(id);
343
+ if (panel) {
344
+ rowElements.push(panel);
345
+ }
346
+ if (rowParams.position === undefined && isLastVisibleInSection) {
347
+ rowElements.push(renderInfiniteLoadingTrigger(id));
348
+ }
349
+ });
350
+ return rowElements;
351
+ };
352
+ const scrollerStyle = React.useMemo(() => ({
353
+ overflowX: !needsHorizontalScrollbar ? 'hidden' : undefined,
354
+ overflowY: autoHeight ? 'hidden' : undefined
355
+ }), [needsHorizontalScrollbar, autoHeight]);
356
+ const contentSize = React.useMemo(() => {
357
+ const size = {
358
+ width: needsHorizontalScrollbar ? columnsTotalWidth : 'auto',
359
+ flexBasis: contentHeight,
360
+ flexShrink: 0
361
+ };
362
+ if (size.flexBasis === 0) {
363
+ size.flexBasis = minimalContentHeight; // Give room to show the overlay when there no rows.
364
+ }
365
+ return size;
366
+ }, [columnsTotalWidth, contentHeight, needsHorizontalScrollbar, minimalContentHeight]);
367
+ const verticalScrollRestoreCallback = React.useRef(null);
368
+ const onContentSizeApplied = React.useCallback(node => {
369
+ if (!node) {
370
+ return;
371
+ }
372
+ verticalScrollRestoreCallback.current?.(columnsTotalWidth, contentHeight);
373
+ }, [columnsTotalWidth, contentHeight]);
374
+ useEnhancedEffect(() => {
375
+ if (!isRenderContextReady.current) {
376
+ return;
377
+ }
378
+ forceUpdateRenderContext();
379
+ }, [enabledForColumns, enabledForRows, forceUpdateRenderContext]);
380
+ useEnhancedEffect(() => {
381
+ if (refs.scroller.current) {
382
+ refs.scroller.current.scrollLeft = 0;
383
+ }
384
+ }, [refs.scroller, scrollReset]);
385
+ useRunOnce(renderContext !== EMPTY_RENDER_CONTEXT, () => {
386
+ onScrollChange?.(scrollPosition.current, renderContext);
387
+ isRenderContextReady.current = true;
388
+ if (initialState?.scroll && refs.scroller.current) {
389
+ const scroller = refs.scroller.current;
390
+ const {
391
+ top,
392
+ left
393
+ } = initialState.scroll;
394
+
395
+ // On initial mount, if we have columns available, we can restore the horizontal scroll immediately, but we need to skip the resulting scroll event, otherwise we would recalculate the render context at position top=0, left=restoredValue, but the initial render context is already calculated based on the initial value of scrollPosition ref.
396
+ const isScrollRestored = {
397
+ top: !(top > 0),
398
+ left: !(left > 0)
399
+ };
400
+ if (!isScrollRestored.left && columnsTotalWidth) {
401
+ scroller.scrollLeft = left;
402
+ ignoreNextScrollEvent.current = true;
403
+ isScrollRestored.left = true;
404
+ }
405
+
406
+ // For the sake of completeness, but I'm not sure if contentHeight is ever available at this point. Maybe when virtualisation is disabled?
407
+ if (!isScrollRestored.top && contentHeight) {
408
+ scroller.scrollTop = top;
409
+ ignoreNextScrollEvent.current = true;
410
+ isScrollRestored.top = true;
411
+ }
412
+
413
+ // To restore the vertical scroll, we need to wait until the rows are available in the DOM (otherwise there's nowhere to scroll), but before paint to avoid reflows
414
+ if (!isScrollRestored.top || !isScrollRestored.left) {
415
+ verticalScrollRestoreCallback.current = (columnsTotalWidthCurrent, contentHeightCurrent) => {
416
+ if (!isScrollRestored.left && columnsTotalWidthCurrent) {
417
+ scroller.scrollLeft = left;
418
+ ignoreNextScrollEvent.current = true;
419
+ isScrollRestored.left = true;
420
+ }
421
+ if (!isScrollRestored.top && contentHeightCurrent) {
422
+ scroller.scrollTop = top;
423
+ ignoreNextScrollEvent.current = true;
424
+ isScrollRestored.top = true;
425
+ }
426
+ if (isScrollRestored.left && isScrollRestored.top) {
427
+ verticalScrollRestoreCallback.current = null;
428
+ }
429
+ };
430
+ }
431
+ }
432
+ });
433
+ useStoreEffect(store, Dimensions.selectors.dimensions, forceUpdateRenderContext);
434
+ const getters = {
435
+ setPanels,
436
+ getRows,
437
+ getContainerProps: () => ({
438
+ ref: params.refs.container
439
+ }),
440
+ getScrollerProps: () => ({
441
+ ref: refs.scroller,
442
+ onScroll: handleScroll,
443
+ onWheel,
444
+ onTouchMove,
445
+ style: scrollerStyle,
446
+ role: 'presentation',
447
+ // `tabIndex` shouldn't be used along role=presentation, but it fixes a Firefox bug
448
+ // https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
449
+ tabIndex: platform.isFirefox ? -1 : undefined
450
+ }),
451
+ getContentProps: () => ({
452
+ style: contentSize,
453
+ role: 'presentation',
454
+ ref: onContentSizeApplied
455
+ }),
456
+ getRenderZoneProps: () => ({
457
+ role: 'rowgroup'
458
+ }),
459
+ getScrollbarVerticalProps: () => ({
460
+ ref: refs.scrollbarVertical,
461
+ scrollPosition
462
+ }),
463
+ getScrollbarHorizontalProps: () => ({
464
+ ref: refs.scrollbarHorizontal,
465
+ scrollPosition
466
+ }),
467
+ getScrollAreaProps: () => ({
468
+ scrollPosition
469
+ })
470
+ };
471
+ useFirstRender(() => {
472
+ store.state = _extends({}, store.state, {
473
+ getters
474
+ });
475
+ });
476
+ React.useEffect(() => {
477
+ store.update(_extends({}, store.state, {
478
+ getters
479
+ }));
480
+ // eslint-disable-next-line react-hooks/exhaustive-deps
481
+ }, Object.values(getters));
482
+
483
+ /* Placeholder API functions for colspan & rowspan to re-implement */
484
+
485
+ const getCellColSpanInfo = () => {
486
+ throw new Error('Unimplemented: colspan feature is required');
487
+ };
488
+ const calculateColSpan = () => {
489
+ throw new Error('Unimplemented: colspan feature is required');
490
+ };
491
+ const getHiddenCellsOrigin = () => {
492
+ throw new Error('Unimplemented: rowspan feature is required');
493
+ };
494
+ return {
495
+ getters,
496
+ useVirtualization: () => useStore(store, state => state),
497
+ setPanels,
498
+ forceUpdateRenderContext,
499
+ getCellColSpanInfo,
500
+ calculateColSpan,
501
+ getHiddenCellsOrigin
502
+ };
503
+ }
504
+ function inputsSelector(store, params, api, enabledForRows, enabledForColumns) {
505
+ const dimensions = Dimensions.selectors.dimensions(store.state);
506
+ const rows = params.rows;
507
+ const range = params.range;
508
+ const columns = params.columns;
509
+ const hiddenCellsOriginMap = api.getHiddenCellsOrigin();
510
+ const lastRowId = params.rows.at(-1)?.id;
511
+ const lastColumn = columns.at(-1);
512
+ return {
513
+ api,
514
+ enabledForRows,
515
+ enabledForColumns,
516
+ autoHeight: params.autoHeight,
517
+ rowBufferPx: params.rowBufferPx,
518
+ columnBufferPx: params.columnBufferPx,
519
+ leftPinnedWidth: dimensions.leftPinnedWidth,
520
+ columnsTotalWidth: dimensions.columnsTotalWidth,
521
+ viewportInnerWidth: dimensions.viewportInnerSize.width,
522
+ viewportInnerHeight: dimensions.viewportInnerSize.height,
523
+ lastRowHeight: lastRowId !== undefined ? api.rowsMeta.getRowHeight(lastRowId) : 0,
524
+ lastColumnWidth: lastColumn?.computedWidth ?? 0,
525
+ rowsMeta: Dimensions.selectors.rowsMeta(store.state),
526
+ columnPositions: Dimensions.selectors.columnPositions(store.state, params.columns),
527
+ rows,
528
+ range,
529
+ pinnedColumns: params.pinnedColumns,
530
+ columns,
531
+ hiddenCellsOriginMap,
532
+ virtualizeColumnsWithAutoRowHeight: params.virtualizeColumnsWithAutoRowHeight
533
+ };
534
+ }
535
+ function computeRenderContext(inputs, scrollPosition, scrollCache) {
536
+ const renderContext = {
537
+ firstRowIndex: 0,
538
+ lastRowIndex: inputs.rows.length,
539
+ firstColumnIndex: 0,
540
+ lastColumnIndex: inputs.columns.length
541
+ };
542
+ const {
543
+ top,
544
+ left
545
+ } = scrollPosition;
546
+ const realLeft = Math.abs(left) + inputs.leftPinnedWidth;
547
+ if (inputs.enabledForRows) {
548
+ // Clamp the value because the search may return an index out of bounds.
549
+ // In the last index, this is not needed because Array.slice doesn't include it.
550
+ let firstRowIndex = Math.min(getNearestIndexToRender(inputs, top, {
551
+ atStart: true,
552
+ lastPosition: inputs.rowsMeta.positions[inputs.rowsMeta.positions.length - 1] + inputs.lastRowHeight
553
+ }), inputs.rowsMeta.positions.length - 1);
554
+
555
+ // If any of the cells in the `firstRowIndex` is hidden due to an extended row span,
556
+ // Make sure the row from where the rowSpan is originated is visible.
557
+ const rowSpanHiddenCellOrigin = inputs.hiddenCellsOriginMap[firstRowIndex];
558
+ if (rowSpanHiddenCellOrigin) {
559
+ const minSpannedRowIndex = Math.min(...Object.values(rowSpanHiddenCellOrigin));
560
+ firstRowIndex = Math.min(firstRowIndex, minSpannedRowIndex);
561
+ }
562
+ const lastRowIndex = inputs.autoHeight ? firstRowIndex + inputs.rows.length : getNearestIndexToRender(inputs, top + inputs.viewportInnerHeight);
563
+ renderContext.firstRowIndex = firstRowIndex;
564
+ renderContext.lastRowIndex = lastRowIndex;
565
+ }
566
+
567
+ // XXX
568
+ // if (inputs.listView) {
569
+ // return {
570
+ // ...renderContext,
571
+ // lastColumnIndex: 1,
572
+ // };
573
+ // }
574
+
575
+ if (inputs.enabledForColumns) {
576
+ let firstColumnIndex = 0;
577
+ let lastColumnIndex = inputs.columnPositions.length;
578
+ let hasRowWithAutoHeight = false;
579
+ const [firstRowToRender, lastRowToRender] = getIndexesToRender({
580
+ firstIndex: renderContext.firstRowIndex,
581
+ lastIndex: renderContext.lastRowIndex,
582
+ minFirstIndex: 0,
583
+ maxLastIndex: inputs.rows.length,
584
+ bufferBefore: scrollCache.buffer.rowBefore,
585
+ bufferAfter: scrollCache.buffer.rowAfter,
586
+ positions: inputs.rowsMeta.positions,
587
+ lastSize: inputs.lastRowHeight
588
+ });
589
+ if (!inputs.virtualizeColumnsWithAutoRowHeight) {
590
+ for (let i = firstRowToRender; i < lastRowToRender && !hasRowWithAutoHeight; i += 1) {
591
+ const row = inputs.rows[i];
592
+ hasRowWithAutoHeight = inputs.api.rowsMeta.rowHasAutoHeight(row.id);
593
+ }
594
+ }
595
+ if (!hasRowWithAutoHeight || inputs.virtualizeColumnsWithAutoRowHeight) {
596
+ firstColumnIndex = binarySearch(realLeft, inputs.columnPositions, {
597
+ atStart: true,
598
+ lastPosition: inputs.columnsTotalWidth
599
+ });
600
+ lastColumnIndex = binarySearch(realLeft + inputs.viewportInnerWidth, inputs.columnPositions);
601
+ }
602
+ renderContext.firstColumnIndex = firstColumnIndex;
603
+ renderContext.lastColumnIndex = lastColumnIndex;
604
+ }
605
+ const actualRenderContext = deriveRenderContext(inputs, renderContext, scrollCache);
606
+ return actualRenderContext;
607
+ }
608
+ function getNearestIndexToRender(inputs, offset, options) {
609
+ const lastMeasuredIndexRelativeToAllRows = inputs.api.rowsMeta.getLastMeasuredRowIndex();
610
+ let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
611
+ if (inputs.range?.lastRowIndex && !allRowsMeasured) {
612
+ // Check if all rows in this page are already measured
613
+ allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= inputs.range.lastRowIndex;
614
+ }
615
+ const lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (inputs.range?.firstRowIndex || 0), 0, inputs.rowsMeta.positions.length);
616
+ if (allRowsMeasured || inputs.rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
617
+ // If all rows were measured (when no row has "auto" as height) or all rows before the offset
618
+ // were measured, then use a binary search because it's faster.
619
+ return binarySearch(offset, inputs.rowsMeta.positions, options);
620
+ }
621
+
622
+ // Otherwise, use an exponential search.
623
+ // If rows have "auto" as height, their positions will be based on estimated heights.
624
+ // In this case, we can skip several steps until we find a position higher than the offset.
625
+ // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
626
+ return exponentialSearch(offset, inputs.rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage, options);
627
+ }
628
+
629
+ /**
630
+ * Accepts as input a raw render context (the area visible in the viewport) and adds
631
+ * computes the actual render context based on pinned elements, buffer dimensions and
632
+ * spanning.
633
+ */
634
+ function deriveRenderContext(inputs, nextRenderContext, scrollCache) {
635
+ const [firstRowToRender, lastRowToRender] = getIndexesToRender({
636
+ firstIndex: nextRenderContext.firstRowIndex,
637
+ lastIndex: nextRenderContext.lastRowIndex,
638
+ minFirstIndex: 0,
639
+ maxLastIndex: inputs.rows.length,
640
+ bufferBefore: scrollCache.buffer.rowBefore,
641
+ bufferAfter: scrollCache.buffer.rowAfter,
642
+ positions: inputs.rowsMeta.positions,
643
+ lastSize: inputs.lastRowHeight
644
+ });
645
+ const [initialFirstColumnToRender, lastColumnToRender] = getIndexesToRender({
646
+ firstIndex: nextRenderContext.firstColumnIndex,
647
+ lastIndex: nextRenderContext.lastColumnIndex,
648
+ minFirstIndex: inputs.pinnedColumns.left.length,
649
+ maxLastIndex: inputs.columns.length - inputs.pinnedColumns.right.length,
650
+ bufferBefore: scrollCache.buffer.columnBefore,
651
+ bufferAfter: scrollCache.buffer.columnAfter,
652
+ positions: inputs.columnPositions,
653
+ lastSize: inputs.lastColumnWidth
654
+ });
655
+ const firstColumnToRender = getFirstNonSpannedColumnToRender({
656
+ api: inputs.api,
657
+ firstColumnToRender: initialFirstColumnToRender,
658
+ firstRowToRender,
659
+ lastRowToRender,
660
+ visibleRows: inputs.rows
661
+ });
662
+ return {
663
+ firstRowIndex: firstRowToRender,
664
+ lastRowIndex: lastRowToRender,
665
+ firstColumnIndex: firstColumnToRender,
666
+ lastColumnIndex: lastColumnToRender
667
+ };
668
+ }
669
+ /**
670
+ * Use binary search to avoid looping through all possible positions.
671
+ * The `options.atStart` provides the possibility to match for the first element that
672
+ * intersects the screen, even if said element's start position is before `offset`. In
673
+ * other words, we search for `offset + width`.
674
+ */
675
+ function binarySearch(offset, positions, options = undefined, sliceStart = 0, sliceEnd = positions.length) {
676
+ if (positions.length <= 0) {
677
+ return -1;
678
+ }
679
+ if (sliceStart >= sliceEnd) {
680
+ return sliceStart;
681
+ }
682
+ const pivot = sliceStart + Math.floor((sliceEnd - sliceStart) / 2);
683
+ const position = positions[pivot];
684
+ let isBefore;
685
+ if (options?.atStart) {
686
+ const width = (pivot === positions.length - 1 ? options.lastPosition : positions[pivot + 1]) - position;
687
+ isBefore = offset - width < position;
688
+ } else {
689
+ isBefore = offset <= position;
690
+ }
691
+ return isBefore ? binarySearch(offset, positions, options, sliceStart, pivot) : binarySearch(offset, positions, options, pivot + 1, sliceEnd);
692
+ }
693
+ function exponentialSearch(offset, positions, index, options = undefined) {
694
+ let interval = 1;
695
+ while (index < positions.length && Math.abs(positions[index]) < offset) {
696
+ index += interval;
697
+ interval *= 2;
698
+ }
699
+ return binarySearch(offset, positions, options, Math.floor(index / 2), Math.min(index, positions.length));
700
+ }
701
+ function getIndexesToRender({
702
+ firstIndex,
703
+ lastIndex,
704
+ bufferBefore,
705
+ bufferAfter,
706
+ minFirstIndex,
707
+ maxLastIndex,
708
+ positions,
709
+ lastSize
710
+ }) {
711
+ const firstPosition = positions[firstIndex] - bufferBefore;
712
+ const lastPosition = positions[lastIndex] + bufferAfter;
713
+ const firstIndexPadded = binarySearch(firstPosition, positions, {
714
+ atStart: true,
715
+ lastPosition: positions[positions.length - 1] + lastSize
716
+ });
717
+ const lastIndexPadded = binarySearch(lastPosition, positions);
718
+ return [clamp(firstIndexPadded, minFirstIndex, maxLastIndex), clamp(lastIndexPadded, minFirstIndex, maxLastIndex)];
719
+ }
720
+ export function areRenderContextsEqual(context1, context2) {
721
+ if (context1 === context2) {
722
+ return true;
723
+ }
724
+ return context1.firstRowIndex === context2.firstRowIndex && context1.lastRowIndex === context2.lastRowIndex && context1.firstColumnIndex === context2.firstColumnIndex && context1.lastColumnIndex === context2.lastColumnIndex;
725
+ }
726
+ export function computeOffsetLeft(columnPositions, renderContext, pinnedLeftLength) {
727
+ const left = (columnPositions[renderContext.firstColumnIndex] ?? 0) - (columnPositions[pinnedLeftLength] ?? 0);
728
+ return Math.abs(left);
729
+ }
730
+ function directionForDelta(dx, dy) {
731
+ if (dx === 0 && dy === 0) {
732
+ return ScrollDirection.NONE;
733
+ }
734
+ /* eslint-disable */
735
+ if (Math.abs(dy) >= Math.abs(dx)) {
736
+ if (dy > 0) {
737
+ return ScrollDirection.DOWN;
738
+ } else {
739
+ return ScrollDirection.UP;
740
+ }
741
+ } else {
742
+ if (dx > 0) {
743
+ return ScrollDirection.RIGHT;
744
+ } else {
745
+ return ScrollDirection.LEFT;
746
+ }
747
+ }
748
+ /* eslint-enable */
749
+ }
750
+ function bufferForDirection(isRtl, direction, rowBufferPx, columnBufferPx, verticalBuffer, horizontalBuffer) {
751
+ if (isRtl) {
752
+ switch (direction) {
753
+ case ScrollDirection.LEFT:
754
+ direction = ScrollDirection.RIGHT;
755
+ break;
756
+ case ScrollDirection.RIGHT:
757
+ direction = ScrollDirection.LEFT;
758
+ break;
759
+ default:
760
+ }
761
+ }
762
+ switch (direction) {
763
+ case ScrollDirection.NONE:
764
+ return {
765
+ rowAfter: rowBufferPx,
766
+ rowBefore: rowBufferPx,
767
+ columnAfter: columnBufferPx,
768
+ columnBefore: columnBufferPx
769
+ };
770
+ case ScrollDirection.LEFT:
771
+ return {
772
+ rowAfter: 0,
773
+ rowBefore: 0,
774
+ columnAfter: 0,
775
+ columnBefore: horizontalBuffer
776
+ };
777
+ case ScrollDirection.RIGHT:
778
+ return {
779
+ rowAfter: 0,
780
+ rowBefore: 0,
781
+ columnAfter: horizontalBuffer,
782
+ columnBefore: 0
783
+ };
784
+ case ScrollDirection.UP:
785
+ return {
786
+ rowAfter: 0,
787
+ rowBefore: verticalBuffer,
788
+ columnAfter: 0,
789
+ columnBefore: 0
790
+ };
791
+ case ScrollDirection.DOWN:
792
+ return {
793
+ rowAfter: verticalBuffer,
794
+ rowBefore: 0,
795
+ columnAfter: 0,
796
+ columnBefore: 0
797
+ };
798
+ default:
799
+ // eslint unable to figure out enum exhaustiveness
800
+ throw new Error('unreachable');
801
+ }
802
+ }
803
+ function createScrollCache(isRtl, rowBufferPx, columnBufferPx, verticalBuffer, horizontalBuffer) {
804
+ return {
805
+ direction: ScrollDirection.NONE,
806
+ buffer: bufferForDirection(isRtl, ScrollDirection.NONE, rowBufferPx, columnBufferPx, verticalBuffer, horizontalBuffer)
807
+ };
808
+ }
809
+ function createRange(from, to) {
810
+ return Array.from({
811
+ length: to - from
812
+ }).map((_, i) => from + i);
813
+ }
814
+ function getFirstNonSpannedColumnToRender({
815
+ api,
816
+ firstColumnToRender,
817
+ firstRowToRender,
818
+ lastRowToRender,
819
+ visibleRows
820
+ }) {
821
+ let firstNonSpannedColumnToRender = firstColumnToRender;
822
+ let foundStableColumn = false;
823
+
824
+ // Keep checking columns until we find one that's not spanned in any visible row
825
+ while (!foundStableColumn && firstNonSpannedColumnToRender >= 0) {
826
+ foundStableColumn = true;
827
+ for (let i = firstRowToRender; i < lastRowToRender; i += 1) {
828
+ const row = visibleRows[i];
829
+ if (row) {
830
+ const rowId = visibleRows[i].id;
831
+ const cellColSpanInfo = api.getCellColSpanInfo(rowId, firstNonSpannedColumnToRender);
832
+ if (cellColSpanInfo && cellColSpanInfo.spannedByColSpan && cellColSpanInfo.leftVisibleCellIndex < firstNonSpannedColumnToRender) {
833
+ firstNonSpannedColumnToRender = cellColSpanInfo.leftVisibleCellIndex;
834
+ foundStableColumn = false;
835
+ break; // Check the new column index against the visible rows, because it might be spanned
836
+ }
837
+ }
838
+ }
839
+ }
840
+ return firstNonSpannedColumnToRender;
841
+ }
842
+ export function roundToDecimalPlaces(value, decimals) {
843
+ return Math.round(value * 10 ** decimals) / 10 ** decimals;
844
+ }