@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.
- package/CHANGELOG.md +10645 -0
- package/README.md +3 -0
- package/esm/features/colspan.d.ts +28 -0
- package/esm/features/colspan.js +88 -0
- package/esm/features/dimensions.d.ts +41 -0
- package/esm/features/dimensions.js +465 -0
- package/esm/features/index.d.ts +5 -0
- package/esm/features/index.js +5 -0
- package/esm/features/keyboard.d.ts +16 -0
- package/esm/features/keyboard.js +35 -0
- package/esm/features/rowspan.d.ts +25 -0
- package/esm/features/rowspan.js +36 -0
- package/esm/features/virtualization.d.ts +103 -0
- package/esm/features/virtualization.js +844 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js +9 -0
- package/esm/models/colspan.d.ts +13 -0
- package/esm/models/colspan.js +1 -0
- package/esm/models/core.d.ts +72 -0
- package/esm/models/core.js +43 -0
- package/esm/models/dimensions.d.ts +132 -0
- package/esm/models/dimensions.js +1 -0
- package/esm/models/index.d.ts +4 -0
- package/esm/models/index.js +4 -0
- package/esm/models/rowspan.d.ts +17 -0
- package/esm/models/rowspan.js +1 -0
- package/esm/package.json +1 -0
- package/esm/useVirtualizer.d.ts +193 -0
- package/esm/useVirtualizer.js +26 -0
- package/features/colspan.d.ts +28 -0
- package/features/colspan.js +94 -0
- package/features/dimensions.d.ts +41 -0
- package/features/dimensions.js +472 -0
- package/features/index.d.ts +5 -0
- package/features/index.js +60 -0
- package/features/keyboard.d.ts +16 -0
- package/features/keyboard.js +40 -0
- package/features/rowspan.d.ts +25 -0
- package/features/rowspan.js +42 -0
- package/features/virtualization.d.ts +103 -0
- package/features/virtualization.js +853 -0
- package/index.d.ts +2 -0
- package/index.js +34 -0
- package/models/colspan.d.ts +13 -0
- package/models/colspan.js +5 -0
- package/models/core.d.ts +72 -0
- package/models/core.js +49 -0
- package/models/dimensions.d.ts +132 -0
- package/models/dimensions.js +5 -0
- package/models/index.d.ts +4 -0
- package/models/index.js +49 -0
- package/models/rowspan.d.ts +17 -0
- package/models/rowspan.js +5 -0
- package/package.json +67 -0
- package/useVirtualizer.d.ts +193 -0
- package/useVirtualizer.js +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Store } from '@mui/x-internals/store';
|
|
2
|
+
import type { integer } from '@mui/x-internals/types';
|
|
3
|
+
import type { BaseState, VirtualizerParams } from "../useVirtualizer.js";
|
|
4
|
+
import type { ColumnWithWidth, RowId } from "../models/index.js";
|
|
5
|
+
import type { CellColSpanInfo } from "../models/colspan.js";
|
|
6
|
+
import { Virtualization } from "./virtualization.js";
|
|
7
|
+
type ColumnIndex = number;
|
|
8
|
+
type ColspanMap = Map<RowId, Record<ColumnIndex, CellColSpanInfo>>;
|
|
9
|
+
export declare const Colspan: {
|
|
10
|
+
initialize: typeof initializeState;
|
|
11
|
+
use: typeof useColspan;
|
|
12
|
+
selectors: {};
|
|
13
|
+
};
|
|
14
|
+
export declare namespace Colspan {
|
|
15
|
+
type State = {
|
|
16
|
+
colspanMap: ColspanMap;
|
|
17
|
+
};
|
|
18
|
+
type API = ReturnType<typeof useColspan>;
|
|
19
|
+
}
|
|
20
|
+
declare function initializeState(_params: VirtualizerParams): {
|
|
21
|
+
colspanMap: Map<any, any>;
|
|
22
|
+
};
|
|
23
|
+
declare function useColspan(store: Store<BaseState & Colspan.State>, params: VirtualizerParams, api: Virtualization.API): {
|
|
24
|
+
resetColSpan: () => void;
|
|
25
|
+
getCellColSpanInfo: (rowId: RowId, columnIndex: integer) => CellColSpanInfo | undefined;
|
|
26
|
+
calculateColSpan: (rowId: RowId, minFirstColumn: integer, maxLastColumn: integer, columns: ColumnWithWidth[]) => void;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import useEventCallback from '@mui/utils/useEventCallback';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable import/export, @typescript-eslint/no-redeclare */
|
|
4
|
+
|
|
5
|
+
const selectors = {};
|
|
6
|
+
export const Colspan = {
|
|
7
|
+
initialize: initializeState,
|
|
8
|
+
use: useColspan,
|
|
9
|
+
selectors
|
|
10
|
+
};
|
|
11
|
+
function initializeState(_params) {
|
|
12
|
+
return {
|
|
13
|
+
colspanMap: new Map()
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function useColspan(store, params, api) {
|
|
17
|
+
const resetColSpan = () => {
|
|
18
|
+
store.state.colspanMap = new Map();
|
|
19
|
+
};
|
|
20
|
+
const getCellColSpanInfo = (rowId, columnIndex) => {
|
|
21
|
+
return store.state.colspanMap.get(rowId)?.[columnIndex];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Calculate `colSpan` for each cell in the row
|
|
25
|
+
const calculateColSpan = useEventCallback((rowId, minFirstColumn, maxLastColumn, columns) => {
|
|
26
|
+
for (let i = minFirstColumn; i < maxLastColumn; i += 1) {
|
|
27
|
+
const cellProps = calculateCellColSpan(store.state.colspanMap, i, rowId, minFirstColumn, maxLastColumn, columns, params.getColspan);
|
|
28
|
+
if (cellProps.colSpan > 1) {
|
|
29
|
+
i += cellProps.colSpan - 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
api.calculateColSpan = calculateColSpan;
|
|
34
|
+
return {
|
|
35
|
+
resetColSpan,
|
|
36
|
+
getCellColSpanInfo,
|
|
37
|
+
calculateColSpan
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function calculateCellColSpan(lookup, columnIndex, rowId, minFirstColumnIndex, maxLastColumnIndex, columns, getColspan) {
|
|
41
|
+
const columnsLength = columns.length;
|
|
42
|
+
const column = columns[columnIndex];
|
|
43
|
+
const colSpan = getColspan(rowId, column, columnIndex);
|
|
44
|
+
if (!colSpan || colSpan === 1) {
|
|
45
|
+
setCellColSpanInfo(lookup, rowId, columnIndex, {
|
|
46
|
+
spannedByColSpan: false,
|
|
47
|
+
cellProps: {
|
|
48
|
+
colSpan: 1,
|
|
49
|
+
width: column.computedWidth
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
colSpan: 1
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let width = column.computedWidth;
|
|
57
|
+
for (let j = 1; j < colSpan; j += 1) {
|
|
58
|
+
const nextColumnIndex = columnIndex + j;
|
|
59
|
+
// Cells should be spanned only within their column section (left-pinned, right-pinned and unpinned).
|
|
60
|
+
if (nextColumnIndex >= minFirstColumnIndex && nextColumnIndex < maxLastColumnIndex) {
|
|
61
|
+
const nextColumn = columns[nextColumnIndex];
|
|
62
|
+
width += nextColumn.computedWidth;
|
|
63
|
+
setCellColSpanInfo(lookup, rowId, columnIndex + j, {
|
|
64
|
+
spannedByColSpan: true,
|
|
65
|
+
rightVisibleCellIndex: Math.min(columnIndex + colSpan, columnsLength - 1),
|
|
66
|
+
leftVisibleCellIndex: columnIndex
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
setCellColSpanInfo(lookup, rowId, columnIndex, {
|
|
70
|
+
spannedByColSpan: false,
|
|
71
|
+
cellProps: {
|
|
72
|
+
colSpan,
|
|
73
|
+
width
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
colSpan
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function setCellColSpanInfo(colspanMap, rowId, columnIndex, cellColSpanInfo) {
|
|
82
|
+
let columnInfo = colspanMap.get(rowId);
|
|
83
|
+
if (!columnInfo) {
|
|
84
|
+
columnInfo = {};
|
|
85
|
+
colspanMap.set(rowId, columnInfo);
|
|
86
|
+
}
|
|
87
|
+
columnInfo[columnIndex] = cellColSpanInfo;
|
|
88
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Store } from '@mui/x-internals/store';
|
|
2
|
+
import { ColumnWithWidth, DimensionsState, RowId, RowsMetaState, Size } from "../models/index.js";
|
|
3
|
+
import type { BaseState, VirtualizerParams } from "../useVirtualizer.js";
|
|
4
|
+
export declare const Dimensions: {
|
|
5
|
+
initialize: typeof initializeState;
|
|
6
|
+
use: typeof useDimensions;
|
|
7
|
+
selectors: {
|
|
8
|
+
rootSize: (state: BaseState) => Size;
|
|
9
|
+
dimensions: (state: BaseState) => DimensionsState;
|
|
10
|
+
rowHeight: (state: BaseState) => number;
|
|
11
|
+
rowsMeta: (state: BaseState) => RowsMetaState;
|
|
12
|
+
columnPositions: (_: any, columns: ColumnWithWidth[]) => number[];
|
|
13
|
+
needsHorizontalScrollbar: (state: BaseState) => boolean;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export declare namespace Dimensions {
|
|
17
|
+
type State = {
|
|
18
|
+
rootSize: Size;
|
|
19
|
+
dimensions: DimensionsState;
|
|
20
|
+
rowsMeta: RowsMetaState;
|
|
21
|
+
rowHeights: Map<any, any>;
|
|
22
|
+
};
|
|
23
|
+
type API = ReturnType<typeof useDimensions>;
|
|
24
|
+
}
|
|
25
|
+
declare function initializeState(params: VirtualizerParams): Dimensions.State;
|
|
26
|
+
declare function useDimensions(store: Store<BaseState>, params: VirtualizerParams, _api: {}): {
|
|
27
|
+
updateDimensions: () => void;
|
|
28
|
+
debouncedUpdateDimensions: ((() => void) & import("@mui/x-internals/throttle/throttle").Cancelable) | undefined;
|
|
29
|
+
rowsMeta: {
|
|
30
|
+
getRowHeight: (rowId: RowId) => any;
|
|
31
|
+
setLastMeasuredRowIndex: (index: number) => void;
|
|
32
|
+
storeRowHeightMeasurement: (id: RowId, height: number) => void;
|
|
33
|
+
hydrateRowsMeta: () => void;
|
|
34
|
+
observeRowHeight: (element: Element, rowId: RowId) => () => void | undefined;
|
|
35
|
+
rowHasAutoHeight: (id: RowId) => any;
|
|
36
|
+
getRowHeightEntry: (rowId: RowId) => any;
|
|
37
|
+
getLastMeasuredRowIndex: () => number;
|
|
38
|
+
resetRowHeights: () => void;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import _extends from "@babel/runtime/helpers/esm/extends";
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import ownerDocument from '@mui/utils/ownerDocument';
|
|
6
|
+
import useLazyRef from '@mui/utils/useLazyRef';
|
|
7
|
+
import useLayoutEffect from '@mui/utils/useEnhancedEffect';
|
|
8
|
+
import useEventCallback from '@mui/utils/useEventCallback';
|
|
9
|
+
import { throttle } from '@mui/x-internals/throttle';
|
|
10
|
+
import { isDeepEqual } from '@mui/x-internals/isDeepEqual';
|
|
11
|
+
import { roundToDecimalPlaces } from '@mui/x-internals/math';
|
|
12
|
+
import { useStore, useStoreEffect, createSelectorMemoized } from '@mui/x-internals/store';
|
|
13
|
+
import { Size } from "../models/index.js";
|
|
14
|
+
/* eslint-disable import/export, @typescript-eslint/no-redeclare */
|
|
15
|
+
/* eslint-disable no-underscore-dangle */
|
|
16
|
+
|
|
17
|
+
const EMPTY_DIMENSIONS = {
|
|
18
|
+
isReady: false,
|
|
19
|
+
root: Size.EMPTY,
|
|
20
|
+
viewportOuterSize: Size.EMPTY,
|
|
21
|
+
viewportInnerSize: Size.EMPTY,
|
|
22
|
+
contentSize: Size.EMPTY,
|
|
23
|
+
minimumSize: Size.EMPTY,
|
|
24
|
+
hasScrollX: false,
|
|
25
|
+
hasScrollY: false,
|
|
26
|
+
scrollbarSize: 0,
|
|
27
|
+
headerHeight: 0,
|
|
28
|
+
groupHeaderHeight: 0,
|
|
29
|
+
headerFilterHeight: 0,
|
|
30
|
+
rowWidth: 0,
|
|
31
|
+
rowHeight: 0,
|
|
32
|
+
columnsTotalWidth: 0,
|
|
33
|
+
leftPinnedWidth: 0,
|
|
34
|
+
rightPinnedWidth: 0,
|
|
35
|
+
headersTotalHeight: 0,
|
|
36
|
+
topContainerHeight: 0,
|
|
37
|
+
bottomContainerHeight: 0
|
|
38
|
+
};
|
|
39
|
+
const selectors = {
|
|
40
|
+
rootSize: state => state.rootSize,
|
|
41
|
+
dimensions: state => state.dimensions,
|
|
42
|
+
rowHeight: state => state.dimensions.rowHeight,
|
|
43
|
+
rowsMeta: state => state.rowsMeta,
|
|
44
|
+
columnPositions: createSelectorMemoized((_, columns) => {
|
|
45
|
+
const positions = [];
|
|
46
|
+
let currentPosition = 0;
|
|
47
|
+
for (let i = 0; i < columns.length; i += 1) {
|
|
48
|
+
positions.push(currentPosition);
|
|
49
|
+
currentPosition += columns[i].computedWidth;
|
|
50
|
+
}
|
|
51
|
+
return positions;
|
|
52
|
+
}),
|
|
53
|
+
needsHorizontalScrollbar: state => state.dimensions.viewportOuterSize.width > 0 && state.dimensions.columnsTotalWidth > state.dimensions.viewportOuterSize.width
|
|
54
|
+
};
|
|
55
|
+
export const Dimensions = {
|
|
56
|
+
initialize: initializeState,
|
|
57
|
+
use: useDimensions,
|
|
58
|
+
selectors
|
|
59
|
+
};
|
|
60
|
+
function initializeState(params) {
|
|
61
|
+
const dimensions = _extends({}, EMPTY_DIMENSIONS, params.dimensions);
|
|
62
|
+
const {
|
|
63
|
+
rowCount
|
|
64
|
+
} = params;
|
|
65
|
+
const {
|
|
66
|
+
rowHeight
|
|
67
|
+
} = dimensions;
|
|
68
|
+
const rowsMeta = {
|
|
69
|
+
currentPageTotalHeight: rowCount * rowHeight,
|
|
70
|
+
positions: Array.from({
|
|
71
|
+
length: rowCount
|
|
72
|
+
}, (_, i) => i * rowHeight),
|
|
73
|
+
pinnedTopRowsTotalHeight: 0,
|
|
74
|
+
pinnedBottomRowsTotalHeight: 0
|
|
75
|
+
};
|
|
76
|
+
const rowHeights = new Map();
|
|
77
|
+
return {
|
|
78
|
+
rootSize: Size.EMPTY,
|
|
79
|
+
dimensions,
|
|
80
|
+
rowsMeta,
|
|
81
|
+
rowHeights
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function useDimensions(store, params, _api) {
|
|
85
|
+
const isFirstSizing = React.useRef(true);
|
|
86
|
+
const {
|
|
87
|
+
refs,
|
|
88
|
+
dimensions: {
|
|
89
|
+
rowHeight,
|
|
90
|
+
headerHeight,
|
|
91
|
+
columnsTotalWidth,
|
|
92
|
+
groupHeaderHeight,
|
|
93
|
+
headerFilterHeight,
|
|
94
|
+
headersTotalHeight,
|
|
95
|
+
leftPinnedWidth,
|
|
96
|
+
rightPinnedWidth
|
|
97
|
+
}
|
|
98
|
+
} = params;
|
|
99
|
+
const updateDimensions = React.useCallback(() => {
|
|
100
|
+
if (isFirstSizing.current) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const rootSize = selectors.rootSize(store.state);
|
|
104
|
+
const rowsMeta = selectors.rowsMeta(store.state);
|
|
105
|
+
|
|
106
|
+
// All the floating point dimensions should be rounded to .1 decimal places to avoid subpixel rendering issues
|
|
107
|
+
// https://github.com/mui/mui-x/issues/9550#issuecomment-1619020477
|
|
108
|
+
// https://github.com/mui/mui-x/issues/15721
|
|
109
|
+
const scrollbarSize = measureScrollbarSize(params.refs.container.current, params.scrollbarSize);
|
|
110
|
+
const topContainerHeight = headersTotalHeight + rowsMeta.pinnedTopRowsTotalHeight;
|
|
111
|
+
const bottomContainerHeight = rowsMeta.pinnedBottomRowsTotalHeight;
|
|
112
|
+
const contentSize = {
|
|
113
|
+
width: columnsTotalWidth,
|
|
114
|
+
height: roundToDecimalPlaces(rowsMeta.currentPageTotalHeight, 1)
|
|
115
|
+
};
|
|
116
|
+
let viewportOuterSize;
|
|
117
|
+
let viewportInnerSize;
|
|
118
|
+
let hasScrollX = false;
|
|
119
|
+
let hasScrollY = false;
|
|
120
|
+
if (params.autoHeight) {
|
|
121
|
+
hasScrollY = false;
|
|
122
|
+
hasScrollX = Math.round(columnsTotalWidth) > Math.round(rootSize.width);
|
|
123
|
+
viewportOuterSize = {
|
|
124
|
+
width: rootSize.width,
|
|
125
|
+
height: topContainerHeight + bottomContainerHeight + contentSize.height
|
|
126
|
+
};
|
|
127
|
+
viewportInnerSize = {
|
|
128
|
+
width: Math.max(0, viewportOuterSize.width - (hasScrollY ? scrollbarSize : 0)),
|
|
129
|
+
height: Math.max(0, viewportOuterSize.height - (hasScrollX ? scrollbarSize : 0))
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
viewportOuterSize = {
|
|
133
|
+
width: rootSize.width,
|
|
134
|
+
height: rootSize.height
|
|
135
|
+
};
|
|
136
|
+
viewportInnerSize = {
|
|
137
|
+
width: Math.max(0, viewportOuterSize.width),
|
|
138
|
+
height: Math.max(0, viewportOuterSize.height - topContainerHeight - bottomContainerHeight)
|
|
139
|
+
};
|
|
140
|
+
const content = contentSize;
|
|
141
|
+
const container = viewportInnerSize;
|
|
142
|
+
const hasScrollXIfNoYScrollBar = content.width > container.width;
|
|
143
|
+
const hasScrollYIfNoXScrollBar = content.height > container.height;
|
|
144
|
+
if (hasScrollXIfNoYScrollBar || hasScrollYIfNoXScrollBar) {
|
|
145
|
+
hasScrollY = hasScrollYIfNoXScrollBar;
|
|
146
|
+
hasScrollX = content.width + (hasScrollY ? scrollbarSize : 0) > container.width;
|
|
147
|
+
|
|
148
|
+
// We recalculate the scroll y to consider the size of the x scrollbar.
|
|
149
|
+
if (hasScrollX) {
|
|
150
|
+
hasScrollY = content.height + scrollbarSize > container.height;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (hasScrollY) {
|
|
154
|
+
viewportInnerSize.width -= scrollbarSize;
|
|
155
|
+
}
|
|
156
|
+
if (hasScrollX) {
|
|
157
|
+
viewportInnerSize.height -= scrollbarSize;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const rowWidth = Math.max(viewportOuterSize.width, columnsTotalWidth + (hasScrollY ? scrollbarSize : 0));
|
|
161
|
+
const minimumSize = {
|
|
162
|
+
width: columnsTotalWidth,
|
|
163
|
+
height: topContainerHeight + contentSize.height + bottomContainerHeight
|
|
164
|
+
};
|
|
165
|
+
const newDimensions = {
|
|
166
|
+
isReady: true,
|
|
167
|
+
root: rootSize,
|
|
168
|
+
viewportOuterSize,
|
|
169
|
+
viewportInnerSize,
|
|
170
|
+
contentSize,
|
|
171
|
+
minimumSize,
|
|
172
|
+
hasScrollX,
|
|
173
|
+
hasScrollY,
|
|
174
|
+
scrollbarSize,
|
|
175
|
+
headerHeight,
|
|
176
|
+
groupHeaderHeight,
|
|
177
|
+
headerFilterHeight,
|
|
178
|
+
rowWidth,
|
|
179
|
+
rowHeight,
|
|
180
|
+
columnsTotalWidth,
|
|
181
|
+
leftPinnedWidth,
|
|
182
|
+
rightPinnedWidth,
|
|
183
|
+
headersTotalHeight,
|
|
184
|
+
topContainerHeight,
|
|
185
|
+
bottomContainerHeight
|
|
186
|
+
};
|
|
187
|
+
const prevDimensions = store.state.dimensions;
|
|
188
|
+
if (isDeepEqual(prevDimensions, newDimensions)) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
store.update({
|
|
192
|
+
dimensions: newDimensions
|
|
193
|
+
});
|
|
194
|
+
}, [store, params.refs.container, params.scrollbarSize, params.autoHeight, rowHeight, headerHeight, groupHeaderHeight, headerFilterHeight, columnsTotalWidth, headersTotalHeight, leftPinnedWidth, rightPinnedWidth]);
|
|
195
|
+
const {
|
|
196
|
+
resizeThrottleMs,
|
|
197
|
+
onResize
|
|
198
|
+
} = params;
|
|
199
|
+
const updateDimensionCallback = useEventCallback(updateDimensions);
|
|
200
|
+
const debouncedUpdateDimensions = React.useMemo(() => resizeThrottleMs > 0 ? throttle(() => {
|
|
201
|
+
updateDimensionCallback();
|
|
202
|
+
onResize?.(store.state.rootSize);
|
|
203
|
+
}, resizeThrottleMs) : undefined, [resizeThrottleMs, onResize, store, updateDimensionCallback]);
|
|
204
|
+
React.useEffect(() => debouncedUpdateDimensions?.clear, [debouncedUpdateDimensions]);
|
|
205
|
+
useLayoutEffect(() => observeRootNode(refs.container.current, store), [refs, store]);
|
|
206
|
+
useLayoutEffect(updateDimensions, [updateDimensions]);
|
|
207
|
+
useStoreEffect(store, selectors.rootSize, (_, size) => {
|
|
208
|
+
params.onResize?.(size);
|
|
209
|
+
if (isFirstSizing.current || !debouncedUpdateDimensions) {
|
|
210
|
+
// We want to initialize the grid dimensions as soon as possible to avoid flickering
|
|
211
|
+
isFirstSizing.current = false;
|
|
212
|
+
updateDimensions();
|
|
213
|
+
} else {
|
|
214
|
+
debouncedUpdateDimensions();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
const rowsMeta = useRowsMeta(store, params, updateDimensions);
|
|
218
|
+
return {
|
|
219
|
+
updateDimensions,
|
|
220
|
+
debouncedUpdateDimensions,
|
|
221
|
+
rowsMeta
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function useRowsMeta(store, params, updateDimensions) {
|
|
225
|
+
const heightCache = store.state.rowHeights;
|
|
226
|
+
const {
|
|
227
|
+
rows,
|
|
228
|
+
getRowHeight: getRowHeightProp,
|
|
229
|
+
getRowSpacing,
|
|
230
|
+
getEstimatedRowHeight
|
|
231
|
+
} = params;
|
|
232
|
+
const lastMeasuredRowIndex = React.useRef(-1);
|
|
233
|
+
const hasRowWithAutoHeight = React.useRef(false);
|
|
234
|
+
const isHeightMetaValid = React.useRef(false);
|
|
235
|
+
const pinnedRows = params.pinnedRows;
|
|
236
|
+
const rowHeight = useStore(store, selectors.rowHeight);
|
|
237
|
+
const getRowHeightEntry = useEventCallback(rowId => {
|
|
238
|
+
let entry = heightCache.get(rowId);
|
|
239
|
+
if (entry === undefined) {
|
|
240
|
+
entry = {
|
|
241
|
+
content: store.state.dimensions.rowHeight,
|
|
242
|
+
spacingTop: 0,
|
|
243
|
+
spacingBottom: 0,
|
|
244
|
+
detail: 0,
|
|
245
|
+
autoHeight: false,
|
|
246
|
+
needsFirstMeasurement: true
|
|
247
|
+
};
|
|
248
|
+
heightCache.set(rowId, entry);
|
|
249
|
+
}
|
|
250
|
+
return entry;
|
|
251
|
+
});
|
|
252
|
+
const {
|
|
253
|
+
rowIdToIndexMap,
|
|
254
|
+
applyRowHeight
|
|
255
|
+
} = params;
|
|
256
|
+
const processHeightEntry = React.useCallback(row => {
|
|
257
|
+
// HACK: rowHeight trails behind the most up-to-date value just enough to
|
|
258
|
+
// mess the initial rowsMeta hydration :/
|
|
259
|
+
eslintUseValue(rowHeight);
|
|
260
|
+
const dimensions = selectors.dimensions(store.state);
|
|
261
|
+
const baseRowHeight = dimensions.rowHeight;
|
|
262
|
+
const entry = getRowHeightEntry(row.id);
|
|
263
|
+
if (!getRowHeightProp) {
|
|
264
|
+
entry.content = baseRowHeight;
|
|
265
|
+
entry.needsFirstMeasurement = false;
|
|
266
|
+
} else {
|
|
267
|
+
const rowHeightFromUser = getRowHeightProp(row);
|
|
268
|
+
if (rowHeightFromUser === 'auto') {
|
|
269
|
+
if (entry.needsFirstMeasurement) {
|
|
270
|
+
const estimatedRowHeight = getEstimatedRowHeight ? getEstimatedRowHeight(row) : baseRowHeight;
|
|
271
|
+
|
|
272
|
+
// If the row was not measured yet use the estimated row height
|
|
273
|
+
entry.content = estimatedRowHeight ?? baseRowHeight;
|
|
274
|
+
}
|
|
275
|
+
hasRowWithAutoHeight.current = true;
|
|
276
|
+
entry.autoHeight = true;
|
|
277
|
+
} else {
|
|
278
|
+
// Default back to base rowHeight if getRowHeight returns null value.
|
|
279
|
+
entry.content = rowHeightFromUser ?? dimensions.rowHeight;
|
|
280
|
+
entry.needsFirstMeasurement = false;
|
|
281
|
+
entry.autoHeight = false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (getRowSpacing) {
|
|
285
|
+
const indexRelativeToCurrentPage = rowIdToIndexMap.get(row.id) ?? -1;
|
|
286
|
+
const spacing = getRowSpacing(row, {
|
|
287
|
+
isFirstVisible: indexRelativeToCurrentPage === 0,
|
|
288
|
+
isLastVisible: indexRelativeToCurrentPage === rows.length - 1,
|
|
289
|
+
indexRelativeToCurrentPage
|
|
290
|
+
});
|
|
291
|
+
entry.spacingTop = spacing.top ?? 0;
|
|
292
|
+
entry.spacingBottom = spacing.bottom ?? 0;
|
|
293
|
+
} else {
|
|
294
|
+
entry.spacingTop = 0;
|
|
295
|
+
entry.spacingBottom = 0;
|
|
296
|
+
}
|
|
297
|
+
applyRowHeight?.(entry, row);
|
|
298
|
+
return entry;
|
|
299
|
+
}, [store, rows, getRowHeightProp, getRowHeightEntry, getEstimatedRowHeight, rowHeight, getRowSpacing, rowIdToIndexMap, applyRowHeight]);
|
|
300
|
+
const hydrateRowsMeta = React.useCallback(() => {
|
|
301
|
+
hasRowWithAutoHeight.current = false;
|
|
302
|
+
const pinnedTopRowsTotalHeight = pinnedRows.top.reduce((acc, row) => {
|
|
303
|
+
const entry = processHeightEntry(row);
|
|
304
|
+
return acc + entry.content + entry.spacingTop + entry.spacingBottom + entry.detail;
|
|
305
|
+
}, 0);
|
|
306
|
+
const pinnedBottomRowsTotalHeight = pinnedRows.bottom.reduce((acc, row) => {
|
|
307
|
+
const entry = processHeightEntry(row);
|
|
308
|
+
return acc + entry.content + entry.spacingTop + entry.spacingBottom + entry.detail;
|
|
309
|
+
}, 0);
|
|
310
|
+
const positions = [];
|
|
311
|
+
const currentPageTotalHeight = rows.reduce((acc, row) => {
|
|
312
|
+
positions.push(acc);
|
|
313
|
+
const entry = processHeightEntry(row);
|
|
314
|
+
const total = entry.content + entry.spacingTop + entry.spacingBottom + entry.detail;
|
|
315
|
+
return acc + total;
|
|
316
|
+
}, 0);
|
|
317
|
+
if (!hasRowWithAutoHeight.current) {
|
|
318
|
+
// No row has height=auto, so all rows are already measured
|
|
319
|
+
lastMeasuredRowIndex.current = Infinity;
|
|
320
|
+
}
|
|
321
|
+
const didHeightsChange = pinnedTopRowsTotalHeight !== store.state.rowsMeta.pinnedTopRowsTotalHeight || pinnedBottomRowsTotalHeight !== store.state.rowsMeta.pinnedBottomRowsTotalHeight || currentPageTotalHeight !== store.state.rowsMeta.currentPageTotalHeight;
|
|
322
|
+
const rowsMeta = {
|
|
323
|
+
currentPageTotalHeight,
|
|
324
|
+
positions,
|
|
325
|
+
pinnedTopRowsTotalHeight,
|
|
326
|
+
pinnedBottomRowsTotalHeight
|
|
327
|
+
};
|
|
328
|
+
store.set('rowsMeta', rowsMeta);
|
|
329
|
+
if (didHeightsChange) {
|
|
330
|
+
updateDimensions();
|
|
331
|
+
}
|
|
332
|
+
isHeightMetaValid.current = true;
|
|
333
|
+
}, [store, pinnedRows, rows, processHeightEntry, updateDimensions]);
|
|
334
|
+
const hydrateRowsMetaLatest = useEventCallback(hydrateRowsMeta);
|
|
335
|
+
const getRowHeight = rowId => {
|
|
336
|
+
return heightCache.get(rowId)?.content ?? selectors.rowHeight(store.state);
|
|
337
|
+
};
|
|
338
|
+
const storeRowHeightMeasurement = (id, height) => {
|
|
339
|
+
const entry = getRowHeightEntry(id);
|
|
340
|
+
const didChange = entry.content !== height;
|
|
341
|
+
entry.needsFirstMeasurement = false;
|
|
342
|
+
entry.content = height;
|
|
343
|
+
isHeightMetaValid.current &&= !didChange;
|
|
344
|
+
};
|
|
345
|
+
const rowHasAutoHeight = id => {
|
|
346
|
+
return heightCache.get(id)?.autoHeight ?? false;
|
|
347
|
+
};
|
|
348
|
+
const getLastMeasuredRowIndex = () => {
|
|
349
|
+
return lastMeasuredRowIndex.current;
|
|
350
|
+
};
|
|
351
|
+
const setLastMeasuredRowIndex = index => {
|
|
352
|
+
if (hasRowWithAutoHeight.current && index > lastMeasuredRowIndex.current) {
|
|
353
|
+
lastMeasuredRowIndex.current = index;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const resetRowHeights = () => {
|
|
357
|
+
heightCache.clear();
|
|
358
|
+
hydrateRowsMeta();
|
|
359
|
+
};
|
|
360
|
+
const resizeObserver = useLazyRef(() => typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(entries => {
|
|
361
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
362
|
+
const entry = entries[i];
|
|
363
|
+
const height = entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0].blockSize : entry.contentRect.height;
|
|
364
|
+
const rowId = entry.target.__mui_id;
|
|
365
|
+
const focusedVirtualRowId = params.focusedVirtualCell()?.id;
|
|
366
|
+
if (focusedVirtualRowId === rowId && height === 0) {
|
|
367
|
+
// Focused virtual row has 0 height.
|
|
368
|
+
// We don't want to store it to avoid scroll jumping.
|
|
369
|
+
// https://github.com/mui/mui-x/issues/14726
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
storeRowHeightMeasurement(rowId, height);
|
|
373
|
+
}
|
|
374
|
+
if (!isHeightMetaValid.current) {
|
|
375
|
+
// Avoids "ResizeObserver loop completed with undelivered notifications" error
|
|
376
|
+
requestAnimationFrame(() => {
|
|
377
|
+
hydrateRowsMetaLatest();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
})).current;
|
|
381
|
+
const observeRowHeight = (element, rowId) => {
|
|
382
|
+
element.__mui_id = rowId;
|
|
383
|
+
resizeObserver?.observe(element);
|
|
384
|
+
return () => resizeObserver?.unobserve(element);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// The effect is used to build the rows meta data - currentPageTotalHeight and positions.
|
|
388
|
+
// Because of variable row height this is needed for the virtualization
|
|
389
|
+
useLayoutEffect(() => {
|
|
390
|
+
hydrateRowsMeta();
|
|
391
|
+
}, [hydrateRowsMeta]);
|
|
392
|
+
return {
|
|
393
|
+
getRowHeight,
|
|
394
|
+
setLastMeasuredRowIndex,
|
|
395
|
+
storeRowHeightMeasurement,
|
|
396
|
+
hydrateRowsMeta,
|
|
397
|
+
observeRowHeight,
|
|
398
|
+
rowHasAutoHeight,
|
|
399
|
+
getRowHeightEntry,
|
|
400
|
+
getLastMeasuredRowIndex,
|
|
401
|
+
resetRowHeights
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function observeRootNode(node, store) {
|
|
405
|
+
if (!node) {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
const bounds = node.getBoundingClientRect();
|
|
409
|
+
const initialSize = {
|
|
410
|
+
width: roundToDecimalPlaces(bounds.width, 1),
|
|
411
|
+
height: roundToDecimalPlaces(bounds.height, 1)
|
|
412
|
+
};
|
|
413
|
+
if (store.state.rootSize === Size.EMPTY || !Size.equals(initialSize, store.state.rootSize)) {
|
|
414
|
+
store.update({
|
|
415
|
+
rootSize: initialSize
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
422
|
+
if (!entry) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const rootSize = {
|
|
426
|
+
width: roundToDecimalPlaces(entry.contentRect.width, 1),
|
|
427
|
+
height: roundToDecimalPlaces(entry.contentRect.height, 1)
|
|
428
|
+
};
|
|
429
|
+
if (!Size.equals(rootSize, store.state.rootSize)) {
|
|
430
|
+
store.update({
|
|
431
|
+
rootSize
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
observer.observe(node);
|
|
436
|
+
return () => {
|
|
437
|
+
observer.disconnect();
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const scrollbarSizeCache = new WeakMap();
|
|
441
|
+
function measureScrollbarSize(element, scrollbarSize) {
|
|
442
|
+
if (scrollbarSize !== undefined) {
|
|
443
|
+
return scrollbarSize;
|
|
444
|
+
}
|
|
445
|
+
if (element === null) {
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
const cachedSize = scrollbarSizeCache.get(element);
|
|
449
|
+
if (cachedSize !== undefined) {
|
|
450
|
+
return cachedSize;
|
|
451
|
+
}
|
|
452
|
+
const doc = ownerDocument(element);
|
|
453
|
+
const scrollDiv = doc.createElement('div');
|
|
454
|
+
scrollDiv.style.width = '99px';
|
|
455
|
+
scrollDiv.style.height = '99px';
|
|
456
|
+
scrollDiv.style.position = 'absolute';
|
|
457
|
+
scrollDiv.style.overflow = 'scroll';
|
|
458
|
+
scrollDiv.className = 'scrollDiv';
|
|
459
|
+
element.appendChild(scrollDiv);
|
|
460
|
+
const size = scrollDiv.offsetWidth - scrollDiv.clientWidth;
|
|
461
|
+
element.removeChild(scrollDiv);
|
|
462
|
+
scrollbarSizeCache.set(element, size);
|
|
463
|
+
return size;
|
|
464
|
+
}
|
|
465
|
+
function eslintUseValue(_) {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Store } from '@mui/x-internals/store';
|
|
2
|
+
import type { BaseState, VirtualizerParams } from "../useVirtualizer.js";
|
|
3
|
+
export declare const Keyboard: {
|
|
4
|
+
initialize: typeof initializeState;
|
|
5
|
+
use: typeof useKeyboard;
|
|
6
|
+
selectors: {};
|
|
7
|
+
};
|
|
8
|
+
export declare namespace Keyboard {
|
|
9
|
+
type State = {};
|
|
10
|
+
type API = ReturnType<typeof useKeyboard>;
|
|
11
|
+
}
|
|
12
|
+
declare function initializeState(_params: VirtualizerParams): Keyboard.State;
|
|
13
|
+
declare function useKeyboard(store: Store<BaseState & Keyboard.State>, params: VirtualizerParams, _api: {}): {
|
|
14
|
+
getViewportPageSize: () => number;
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Dimensions } from "./dimensions.js";
|
|
2
|
+
import { Virtualization } from "./virtualization.js";
|
|
3
|
+
|
|
4
|
+
/* eslint-disable import/export, @typescript-eslint/no-redeclare */
|
|
5
|
+
|
|
6
|
+
const selectors = {};
|
|
7
|
+
export const Keyboard = {
|
|
8
|
+
initialize: initializeState,
|
|
9
|
+
use: useKeyboard,
|
|
10
|
+
selectors
|
|
11
|
+
};
|
|
12
|
+
function initializeState(_params) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
function useKeyboard(store, params, _api) {
|
|
16
|
+
const getViewportPageSize = () => {
|
|
17
|
+
const dimensions = Dimensions.selectors.dimensions(store.state);
|
|
18
|
+
if (!dimensions.isReady) {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// TODO: Use a combination of scrollTop, dimensions.viewportInnerSize.height and rowsMeta.possitions
|
|
23
|
+
// to find out the maximum number of rows that can fit in the visible part of the grid
|
|
24
|
+
if (params.getRowHeight) {
|
|
25
|
+
const renderContext = Virtualization.selectors.renderContext(store.state);
|
|
26
|
+
const viewportPageSize = renderContext.lastRowIndex - renderContext.firstRowIndex;
|
|
27
|
+
return Math.min(viewportPageSize - 1, params.rows.length);
|
|
28
|
+
}
|
|
29
|
+
const maximumPageSizeWithoutScrollBar = Math.floor(dimensions.viewportInnerSize.height / dimensions.rowHeight);
|
|
30
|
+
return Math.min(maximumPageSizeWithoutScrollBar, params.rows.length);
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
getViewportPageSize
|
|
34
|
+
};
|
|
35
|
+
}
|