UNPKG

@mui/x-data-grid

Version:

The Community plan edition of the Data Grid components (MUI X).

315 lines (313 loc) 14.6 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import { unstable_ownerDocument as ownerDocument, unstable_useEnhancedEffect as useEnhancedEffect, unstable_useEventCallback as useEventCallback, unstable_ownerWindow as ownerWindow } from '@mui/utils'; import { throttle } from '@mui/x-internals/throttle'; import { useGridApiOptionHandler } from "../../utils/useGridApiEventHandler.js"; import { useGridApiMethod } from "../../utils/useGridApiMethod.js"; import { createSelector } from "../../../utils/createSelector.js"; import { useGridLogger } from "../../utils/useGridLogger.js"; import { gridColumnPositionsSelector, gridVisibleColumnDefinitionsSelector, gridVisiblePinnedColumnDefinitionsSelector } from "../columns/index.js"; import { gridDimensionsSelector } from "./gridDimensionsSelectors.js"; import { gridDensityFactorSelector } from "../density/index.js"; import { gridRenderContextSelector } from "../virtualization/index.js"; import { useGridSelector } from "../../utils/index.js"; import { getVisibleRows } from "../../utils/useGridVisibleRows.js"; import { gridRowsMetaSelector } from "../rows/gridRowsMetaSelector.js"; import { getValidRowHeight, rowHeightWarning } from "../rows/gridRowsUtils.js"; import { getTotalHeaderHeight } from "../columns/gridColumnsUtils.js"; import { DATA_GRID_PROPS_DEFAULT_VALUES } from "../../../constants/dataGridPropsDefaultValues.js"; import { roundToDecimalPlaces } from "../../../utils/roundToDecimalPlaces.js"; import { isJSDOM } from "../../../utils/isJSDOM.js"; import { isDeepEqual } from "../../../utils/utils.js"; const EMPTY_SIZE = { width: 0, height: 0 }; const EMPTY_DIMENSIONS = { isReady: false, root: EMPTY_SIZE, viewportOuterSize: EMPTY_SIZE, viewportInnerSize: EMPTY_SIZE, contentSize: EMPTY_SIZE, minimumSize: EMPTY_SIZE, hasScrollX: false, hasScrollY: false, scrollbarSize: 0, headerHeight: 0, groupHeaderHeight: 0, headerFilterHeight: 0, rowWidth: 0, rowHeight: 0, columnsTotalWidth: 0, leftPinnedWidth: 0, rightPinnedWidth: 0, headersTotalHeight: 0, topContainerHeight: 0, bottomContainerHeight: 0 }; export const dimensionsStateInitializer = (state, props, apiRef) => { const dimensions = EMPTY_DIMENSIONS; const density = gridDensityFactorSelector(apiRef); return _extends({}, state, { dimensions: _extends({}, dimensions, getStaticDimensions(props, apiRef, density, gridVisiblePinnedColumnDefinitionsSelector(apiRef))) }); }; const columnsTotalWidthSelector = createSelector(gridVisibleColumnDefinitionsSelector, gridColumnPositionsSelector, (visibleColumns, positions) => { const colCount = visibleColumns.length; if (colCount === 0) { return 0; } return roundToDecimalPlaces(positions[colCount - 1] + visibleColumns[colCount - 1].computedWidth, 1); }); export function useGridDimensions(apiRef, props) { const logger = useGridLogger(apiRef, 'useResizeContainer'); const errorShown = React.useRef(false); const rootDimensionsRef = React.useRef(EMPTY_SIZE); const pinnedColumns = useGridSelector(apiRef, gridVisiblePinnedColumnDefinitionsSelector); const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector); const columnsTotalWidth = useGridSelector(apiRef, columnsTotalWidthSelector); const isFirstSizing = React.useRef(true); const { rowHeight, headerHeight, groupHeaderHeight, headerFilterHeight, headersTotalHeight, leftPinnedWidth, rightPinnedWidth } = getStaticDimensions(props, apiRef, densityFactor, pinnedColumns); const previousSize = React.useRef(undefined); const getRootDimensions = React.useCallback(() => gridDimensionsSelector(apiRef.current.state), [apiRef]); const setDimensions = React.useCallback(dimensions => { apiRef.current.setState(state => _extends({}, state, { dimensions })); if (apiRef.current.rootElementRef.current) { setCSSVariables(apiRef.current.rootElementRef.current, gridDimensionsSelector(apiRef.current.state)); } }, [apiRef]); const resize = React.useCallback(() => { const element = apiRef.current.mainElementRef.current; if (!element) { return; } const computedStyle = ownerWindow(element).getComputedStyle(element); const newSize = { width: parseFloat(computedStyle.width) || 0, height: parseFloat(computedStyle.height) || 0 }; if (!previousSize.current || !areElementSizesEqual(previousSize.current, newSize)) { apiRef.current.publishEvent('resize', newSize); previousSize.current = newSize; } }, [apiRef]); const getViewportPageSize = React.useCallback(() => { const dimensions = gridDimensionsSelector(apiRef.current.state); if (!dimensions.isReady) { return 0; } const currentPage = getVisibleRows(apiRef); // TODO: Use a combination of scrollTop, dimensions.viewportInnerSize.height and rowsMeta.possitions // to find out the maximum number of rows that can fit in the visible part of the grid if (props.getRowHeight) { const renderContext = gridRenderContextSelector(apiRef); const viewportPageSize = renderContext.lastRowIndex - renderContext.firstRowIndex; return Math.min(viewportPageSize - 1, currentPage.rows.length); } const maximumPageSizeWithoutScrollBar = Math.floor(dimensions.viewportInnerSize.height / rowHeight); return Math.min(maximumPageSizeWithoutScrollBar, currentPage.rows.length); }, [apiRef, props.getRowHeight, rowHeight]); const updateDimensions = React.useCallback(() => { if (isFirstSizing.current) { return; } // All the floating point dimensions should be rounded to .1 decimal places to avoid subpixel rendering issues // https://github.com/mui/mui-x/issues/9550#issuecomment-1619020477 // https://github.com/mui/mui-x/issues/15721 const scrollbarSize = measureScrollbarSize(apiRef.current.mainElementRef.current, props.scrollbarSize); const rowsMeta = gridRowsMetaSelector(apiRef.current.state); const topContainerHeight = headersTotalHeight + rowsMeta.pinnedTopRowsTotalHeight; const bottomContainerHeight = rowsMeta.pinnedBottomRowsTotalHeight; const nonPinnedColumnsTotalWidth = columnsTotalWidth - leftPinnedWidth - rightPinnedWidth; const contentSize = { width: nonPinnedColumnsTotalWidth, height: roundToDecimalPlaces(rowsMeta.currentPageTotalHeight, 1) }; let viewportOuterSize; let viewportInnerSize; let hasScrollX = false; let hasScrollY = false; if (props.autoHeight) { hasScrollY = false; hasScrollX = Math.round(columnsTotalWidth) > Math.round(rootDimensionsRef.current.width); viewportOuterSize = { width: rootDimensionsRef.current.width, height: topContainerHeight + bottomContainerHeight + contentSize.height }; viewportInnerSize = { width: Math.max(0, viewportOuterSize.width - (hasScrollY ? scrollbarSize : 0)), height: Math.max(0, viewportOuterSize.height - (hasScrollX ? scrollbarSize : 0)) }; } else { viewportOuterSize = { width: rootDimensionsRef.current.width, height: rootDimensionsRef.current.height }; viewportInnerSize = { width: Math.max(0, viewportOuterSize.width - leftPinnedWidth - rightPinnedWidth), height: Math.max(0, viewportOuterSize.height - topContainerHeight - bottomContainerHeight) }; const content = contentSize; const container = viewportInnerSize; const hasScrollXIfNoYScrollBar = content.width > container.width; const hasScrollYIfNoXScrollBar = content.height > container.height; if (hasScrollXIfNoYScrollBar || hasScrollYIfNoXScrollBar) { hasScrollY = hasScrollYIfNoXScrollBar; hasScrollX = content.width + (hasScrollY ? scrollbarSize : 0) > container.width; // We recalculate the scroll y to consider the size of the x scrollbar. if (hasScrollX) { hasScrollY = content.height + scrollbarSize > container.height; } } if (hasScrollY) { viewportInnerSize.width -= scrollbarSize; } if (hasScrollX) { viewportInnerSize.height -= scrollbarSize; } } const rowWidth = Math.max(viewportOuterSize.width, columnsTotalWidth + (hasScrollY ? scrollbarSize : 0)); const minimumSize = { width: columnsTotalWidth, height: topContainerHeight + contentSize.height + bottomContainerHeight }; const newDimensions = { isReady: true, root: rootDimensionsRef.current, viewportOuterSize, viewportInnerSize, contentSize, minimumSize, hasScrollX, hasScrollY, scrollbarSize, headerHeight, groupHeaderHeight, headerFilterHeight, rowWidth, rowHeight, columnsTotalWidth, leftPinnedWidth, rightPinnedWidth, headersTotalHeight, topContainerHeight, bottomContainerHeight }; const prevDimensions = apiRef.current.state.dimensions; if (isDeepEqual(prevDimensions, newDimensions)) { return; } setDimensions(newDimensions); if (!areElementSizesEqual(newDimensions.viewportInnerSize, prevDimensions.viewportInnerSize)) { apiRef.current.publishEvent('viewportInnerSizeChange', newDimensions.viewportInnerSize); } apiRef.current.updateRenderContext?.(); }, [apiRef, setDimensions, props.scrollbarSize, props.autoHeight, rowHeight, headerHeight, groupHeaderHeight, headerFilterHeight, columnsTotalWidth, headersTotalHeight, leftPinnedWidth, rightPinnedWidth]); const updateDimensionCallback = useEventCallback(updateDimensions); const debouncedUpdateDimensions = React.useMemo(() => props.resizeThrottleMs > 0 ? throttle(() => { updateDimensionCallback(); apiRef.current.publishEvent('debouncedResize', rootDimensionsRef.current); }, props.resizeThrottleMs) : undefined, [apiRef, props.resizeThrottleMs, updateDimensionCallback]); React.useEffect(() => debouncedUpdateDimensions?.clear, [debouncedUpdateDimensions]); const apiPublic = { resize, getRootDimensions }; const apiPrivate = { updateDimensions, getViewportPageSize }; useEnhancedEffect(updateDimensions, [updateDimensions]); useGridApiMethod(apiRef, apiPublic, 'public'); useGridApiMethod(apiRef, apiPrivate, 'private'); const handleRootMount = React.useCallback(root => { setCSSVariables(root, gridDimensionsSelector(apiRef.current.state)); }, [apiRef]); const handleResize = React.useCallback(size => { rootDimensionsRef.current = size; if (size.height === 0 && !errorShown.current && !props.autoHeight && !isJSDOM) { logger.error(['The parent DOM element of the Data Grid has an empty height.', 'Please make sure that this element has an intrinsic height.', 'The grid displays with a height of 0px.', '', 'More details: https://mui.com/r/x-data-grid-no-dimensions.'].join('\n')); errorShown.current = true; } if (size.width === 0 && !errorShown.current && !isJSDOM) { logger.error(['The parent DOM element of the Data Grid has an empty width.', 'Please make sure that this element has an intrinsic width.', 'The grid displays with a width of 0px.', '', 'More details: https://mui.com/r/x-data-grid-no-dimensions.'].join('\n')); errorShown.current = true; } if (isFirstSizing.current || !debouncedUpdateDimensions) { // We want to initialize the grid dimensions as soon as possible to avoid flickering isFirstSizing.current = false; updateDimensions(); return; } debouncedUpdateDimensions(); }, [updateDimensions, props.autoHeight, debouncedUpdateDimensions, logger]); useGridApiOptionHandler(apiRef, 'rootMount', handleRootMount); useGridApiOptionHandler(apiRef, 'resize', handleResize); useGridApiOptionHandler(apiRef, 'debouncedResize', props.onResize); } function setCSSVariables(root, dimensions) { const set = (k, v) => root.style.setProperty(k, v); set('--DataGrid-hasScrollX', `${Number(dimensions.hasScrollX)}`); set('--DataGrid-hasScrollY', `${Number(dimensions.hasScrollY)}`); set('--DataGrid-scrollbarSize', `${dimensions.scrollbarSize}px`); set('--DataGrid-rowWidth', `${dimensions.rowWidth}px`); set('--DataGrid-columnsTotalWidth', `${dimensions.columnsTotalWidth}px`); set('--DataGrid-leftPinnedWidth', `${dimensions.leftPinnedWidth}px`); set('--DataGrid-rightPinnedWidth', `${dimensions.rightPinnedWidth}px`); set('--DataGrid-headerHeight', `${dimensions.headerHeight}px`); set('--DataGrid-headersTotalHeight', `${dimensions.headersTotalHeight}px`); set('--DataGrid-topContainerHeight', `${dimensions.topContainerHeight}px`); set('--DataGrid-bottomContainerHeight', `${dimensions.bottomContainerHeight}px`); set('--height', `${dimensions.rowHeight}px`); } function getStaticDimensions(props, apiRef, density, pinnedColumnns) { const validRowHeight = getValidRowHeight(props.rowHeight, DATA_GRID_PROPS_DEFAULT_VALUES.rowHeight, rowHeightWarning); return { rowHeight: Math.floor(validRowHeight * density), headerHeight: Math.floor(props.columnHeaderHeight * density), groupHeaderHeight: Math.floor((props.columnGroupHeaderHeight ?? props.columnHeaderHeight) * density), headerFilterHeight: Math.floor((props.headerFilterHeight ?? props.columnHeaderHeight) * density), columnsTotalWidth: columnsTotalWidthSelector(apiRef), headersTotalHeight: getTotalHeaderHeight(apiRef, props), leftPinnedWidth: pinnedColumnns.left.reduce((w, col) => w + col.computedWidth, 0), rightPinnedWidth: pinnedColumnns.right.reduce((w, col) => w + col.computedWidth, 0) }; } const scrollbarSizeCache = new WeakMap(); function measureScrollbarSize(element, scrollbarSize) { if (scrollbarSize !== undefined) { return scrollbarSize; } if (element === null) { return 0; } const cachedSize = scrollbarSizeCache.get(element); if (cachedSize !== undefined) { return cachedSize; } const doc = ownerDocument(element); const scrollDiv = doc.createElement('div'); scrollDiv.style.width = '99px'; scrollDiv.style.height = '99px'; scrollDiv.style.position = 'absolute'; scrollDiv.style.overflow = 'scroll'; scrollDiv.className = 'scrollDiv'; element.appendChild(scrollDiv); const size = scrollDiv.offsetWidth - scrollDiv.clientWidth; element.removeChild(scrollDiv); scrollbarSizeCache.set(element, size); return size; } function areElementSizesEqual(a, b) { return a.width === b.width && a.height === b.height; }