UNPKG

@mui/x-data-grid

Version:

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

287 lines (283 loc) 13 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 { useGridApiEventHandler, useGridApiOptionHandler } from '../../utils/useGridApiEventHandler'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { useGridLogger } from '../../utils/useGridLogger'; import { gridColumnsTotalWidthSelector, gridVisiblePinnedColumnDefinitionsSelector } from '../columns'; import { gridDimensionsSelector } from './gridDimensionsSelectors'; import { gridDensityFactorSelector } from '../density'; import { gridRenderContextSelector } from '../virtualization'; import { useGridSelector } from '../../utils'; import { getVisibleRows } from '../../utils/useGridVisibleRows'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; import { calculatePinnedRowsHeight } from '../rows/gridRowsUtils'; import { getTotalHeaderHeight } from '../columns/gridColumnsUtils'; 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, headerFilterHeight: 0, rowWidth: 0, rowHeight: 0, columnsTotalWidth: 0, leftPinnedWidth: 0, rightPinnedWidth: 0, headersTotalHeight: 0, topContainerHeight: 0, bottomContainerHeight: 0 }; export const dimensionsStateInitializer = state => { const dimensions = EMPTY_DIMENSIONS; return _extends({}, state, { dimensions }); }; export function useGridDimensions(apiRef, props) { const logger = useGridLogger(apiRef, 'useResizeContainer'); const errorShown = React.useRef(false); const rootDimensionsRef = React.useRef(EMPTY_SIZE); const dimensionsState = useGridSelector(apiRef, gridDimensionsSelector); const rowsMeta = useGridSelector(apiRef, gridRowsMetaSelector); const pinnedColumns = useGridSelector(apiRef, gridVisiblePinnedColumnDefinitionsSelector); const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector); const rowHeight = Math.floor(props.rowHeight * densityFactor); const headerHeight = Math.floor(props.columnHeaderHeight * densityFactor); const headerFilterHeight = Math.floor((props.headerFilterHeight ?? props.columnHeaderHeight) * densityFactor); const columnsTotalWidth = roundToDecimalPlaces(gridColumnsTotalWidthSelector(apiRef), 6); const headersTotalHeight = getTotalHeaderHeight(apiRef, props); const leftPinnedWidth = pinnedColumns.left.reduce((w, col) => w + col.computedWidth, 0); const rightPinnedWidth = pinnedColumns.right.reduce((w, col) => w + col.computedWidth, 0); const [savedSize, setSavedSize] = React.useState(); const debouncedSetSavedSize = React.useMemo(() => throttle(setSavedSize, props.resizeThrottleMs), [props.resizeThrottleMs]); const previousSize = React.useRef(); const getRootDimensions = () => apiRef.current.state.dimensions; const setDimensions = useEventCallback(dimensions => { apiRef.current.setState(state => _extends({}, state, { dimensions })); }); 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, { pagination: props.pagination, paginationMode: props.paginationMode }); // 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.pagination, props.paginationMode, props.getRowHeight, rowHeight]); const updateDimensions = React.useCallback(() => { const rootElement = apiRef.current.rootElementRef.current; const pinnedRowsHeight = calculatePinnedRowsHeight(apiRef); const scrollbarSize = measureScrollbarSize(rootElement, columnsTotalWidth, props.scrollbarSize); const topContainerHeight = headersTotalHeight + pinnedRowsHeight.top; const bottomContainerHeight = pinnedRowsHeight.bottom; const nonPinnedColumnsTotalWidth = columnsTotalWidth - leftPinnedWidth - rightPinnedWidth; const contentSize = { width: nonPinnedColumnsTotalWidth, height: rowsMeta.currentPageTotalHeight }; 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, headerFilterHeight, rowWidth, rowHeight, columnsTotalWidth, leftPinnedWidth, rightPinnedWidth, headersTotalHeight, topContainerHeight, bottomContainerHeight }; const prevDimensions = apiRef.current.state.dimensions; setDimensions(newDimensions); if (!areElementSizesEqual(newDimensions.viewportInnerSize, prevDimensions.viewportInnerSize)) { apiRef.current.publishEvent('viewportInnerSizeChange', newDimensions.viewportInnerSize); } apiRef.current.updateRenderContext?.(); }, [apiRef, setDimensions, props.scrollbarSize, props.autoHeight, rowsMeta.currentPageTotalHeight, rowHeight, headerHeight, headerFilterHeight, columnsTotalWidth, headersTotalHeight, leftPinnedWidth, rightPinnedWidth]); const apiPublic = { resize, getRootDimensions }; const apiPrivate = { updateDimensions, getViewportPageSize }; useGridApiMethod(apiRef, apiPublic, 'public'); useGridApiMethod(apiRef, apiPrivate, 'private'); useEnhancedEffect(() => { if (savedSize) { updateDimensions(); apiRef.current.publishEvent('debouncedResize', rootDimensionsRef.current); } }, [apiRef, savedSize, updateDimensions]); const root = apiRef.current.rootElementRef.current; useEnhancedEffect(() => { if (!root) { return; } const set = (k, v) => root.style.setProperty(k, v); set('--DataGrid-width', `${dimensionsState.viewportOuterSize.width}px`); set('--DataGrid-hasScrollX', `${Number(dimensionsState.hasScrollX)}`); set('--DataGrid-hasScrollY', `${Number(dimensionsState.hasScrollY)}`); set('--DataGrid-scrollbarSize', `${dimensionsState.scrollbarSize}px`); set('--DataGrid-rowWidth', `${dimensionsState.rowWidth}px`); set('--DataGrid-columnsTotalWidth', `${dimensionsState.columnsTotalWidth}px`); set('--DataGrid-leftPinnedWidth', `${dimensionsState.leftPinnedWidth}px`); set('--DataGrid-rightPinnedWidth', `${dimensionsState.rightPinnedWidth}px`); set('--DataGrid-headerHeight', `${dimensionsState.headerHeight}px`); set('--DataGrid-headersTotalHeight', `${dimensionsState.headersTotalHeight}px`); set('--DataGrid-topContainerHeight', `${dimensionsState.topContainerHeight}px`); set('--DataGrid-bottomContainerHeight', `${dimensionsState.bottomContainerHeight}px`); set('--height', `${dimensionsState.rowHeight}px`); }, [root, dimensionsState]); const isFirstSizing = React.useRef(true); const handleResize = React.useCallback(size => { rootDimensionsRef.current = size; // jsdom has no layout capabilities const isJSDOM = /jsdom/.test(window.navigator.userAgent); 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) { // We want to initialize the grid dimensions as soon as possible to avoid flickering setSavedSize(size); isFirstSizing.current = false; return; } debouncedSetSavedSize(size); }, [props.autoHeight, debouncedSetSavedSize, logger]); useEnhancedEffect(updateDimensions, [updateDimensions]); useGridApiOptionHandler(apiRef, 'sortedRowsSet', updateDimensions); useGridApiOptionHandler(apiRef, 'paginationModelChange', updateDimensions); useGridApiOptionHandler(apiRef, 'columnsChange', updateDimensions); useGridApiEventHandler(apiRef, 'resize', handleResize); useGridApiOptionHandler(apiRef, 'debouncedResize', props.onResize); } function measureScrollbarSize(rootElement, columnsTotalWidth, scrollbarSize) { if (scrollbarSize !== undefined) { return scrollbarSize; } if (rootElement === null || columnsTotalWidth === 0) { return 0; } const doc = ownerDocument(rootElement); const scrollDiv = doc.createElement('div'); scrollDiv.style.width = '99px'; scrollDiv.style.height = '99px'; scrollDiv.style.position = 'absolute'; scrollDiv.style.overflow = 'scroll'; scrollDiv.className = 'scrollDiv'; rootElement.appendChild(scrollDiv); const size = scrollDiv.offsetWidth - scrollDiv.clientWidth; rootElement.removeChild(scrollDiv); return size; } // Get rid of floating point imprecision errors // https://github.com/mui/mui-x/issues/9550#issuecomment-1619020477 function roundToDecimalPlaces(value, decimals) { return Math.round(value * 10 ** decimals) / 10 ** decimals; } function areElementSizesEqual(a, b) { return a.width === b.width && a.height === b.height; }