From 33db58d41e00a92224d910a8622b8badea021817 Mon Sep 17 00:00:00 2001 From: Matheus Wichman Date: Mon, 11 Apr 2022 12:42:44 -0300 Subject: [PATCH 1/3] [DataGrid] Prevent column header scroll (#4280) --- .../tests/columnHeaders.DataGridPro.test.tsx | 20 +++++++++++++++++++ .../columnHeaders/GridColumnHeaderItem.tsx | 10 ++++------ .../columnHeaders/GridColumnHeaders.tsx | 6 ++---- .../columnHeaders/useGridColumnHeaders.tsx | 2 +- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/grid/x-data-grid-pro/src/tests/columnHeaders.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/columnHeaders.DataGridPro.test.tsx index ee497120c623..dae33df01212 100644 --- a/packages/grid/x-data-grid-pro/src/tests/columnHeaders.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/columnHeaders.DataGridPro.test.tsx @@ -32,6 +32,26 @@ describe(' - Column Headers', () => { ], }; + it('should not scroll the column headers when a column is focused', function test() { + if (isJSDOM) { + this.skip(); // JSDOM version of .focus() doesn't scroll + } + render( +
+ +
, + ); + const columnHeaders = document.querySelector('.MuiDataGrid-columnHeaders')!; + expect(columnHeaders.scrollLeft).to.equal(0); + const columnCell = getColumnHeaderCell(0); + columnCell.focus(); + fireEvent.keyDown(columnCell, { key: 'End' }); + expect(columnHeaders.scrollLeft).to.equal(0); + }); + describe('GridColumnHeaderMenu', () => { it('should close the menu when the window is scrolled', () => { render( diff --git a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaderItem.tsx b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaderItem.tsx index 4a44c10038c8..b599e85e40b2 100644 --- a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaderItem.tsx +++ b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaderItem.tsx @@ -207,13 +207,11 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) { const columnMenuState = apiRef.current.state.columnMenu; if (hasFocus && !columnMenuState.open) { const focusableElement = headerCellRef.current!.querySelector('[tabindex="0"]'); - if (focusableElement) { - focusableElement!.focus(); - } else { - headerCellRef.current!.focus(); - } + const elementToFocus = focusableElement || headerCellRef.current; + elementToFocus?.focus(); + apiRef.current.columnHeadersContainerElementRef!.current!.scrollLeft = 0; } - }); + }, [apiRef, hasFocus]); const headerClassName = typeof column.headerClassName === 'function' diff --git a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaders.tsx b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaders.tsx index 0d8bffd29ba1..445b517e17f5 100644 --- a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaders.tsx +++ b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaders.tsx @@ -44,13 +44,11 @@ const GridColumnHeadersRoot = styled('div', { }; }); -interface GridColumnHeadersProps extends React.HTMLAttributes { - innerRef?: React.Ref; -} +interface GridColumnHeadersProps extends React.HTMLAttributes {} export const GridColumnHeaders = React.forwardRef( function GridColumnHeaders(props, ref) { - const { innerRef, className, ...other } = props; + const { className, ...other } = props; const rootProps = useGridRootProps(); const ownerState = { classes: rootProps.classes }; diff --git a/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx b/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx index 2c97ff84059d..975d56e31ce3 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx @@ -171,7 +171,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { columns.push( Date: Mon, 11 Apr 2022 12:43:13 -0300 Subject: [PATCH 2/3] [DataGrid] Fix `api` prop leaking to DOM (#4384) --- .../columnSelection/GridCellCheckboxRenderer.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/grid/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/grid/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 018b054abaeb..cb9cbb2df414 100644 --- a/packages/grid/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/grid/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { unstable_composeClasses as composeClasses } from '@mui/material'; import { useForkRef } from '@mui/material/utils'; import { GridEvents } from '../../models/events'; -import { GridCellParams } from '../../models/params/gridCellParams'; +import { GridRenderCellParams } from '../../models/params/gridCellParams'; import { isNavigationKey, isSpaceKey } from '../../utils/keyboardUtils'; import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; @@ -27,7 +27,7 @@ interface TouchRippleActions { stop: (event: any, callback?: () => void) => void; } -const GridCellCheckboxForwardRef = React.forwardRef( +const GridCellCheckboxForwardRef = React.forwardRef( function GridCellCheckboxRenderer(props, ref) { const { field, @@ -42,6 +42,7 @@ const GridCellCheckboxForwardRef = React.forwardRef Date: Mon, 11 Apr 2022 13:17:23 -0300 Subject: [PATCH 3/3] [core] Add React 18 to peer dependencies (#4332) --- docs/data/data-grid/events/events.json | 2 +- .../components/data-grid/events/events.json | 2 +- .../grid/x-data-grid-generator/package.json | 2 +- packages/grid/x-data-grid-pro/package.json | 2 +- .../components/DataGridProColumnHeaders.tsx | 22 +++--------- .../components/DataGridProVirtualScroller.tsx | 4 --- packages/grid/x-data-grid/package.json | 2 +- .../columnHeaders/useGridColumnHeaders.tsx | 35 ++++++++++++++++--- .../virtualization/useGridVirtualScroller.tsx | 30 +++++++++++----- .../src/models/events/gridEventLookup.ts | 2 +- packages/x-date-pickers-pro/package.json | 4 +-- packages/x-date-pickers/package.json | 4 +-- packages/x-license-pro/package.json | 2 +- 13 files changed, 68 insertions(+), 45 deletions(-) diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json index e61873a789a6..2e7bd43681bf 100644 --- a/docs/data/data-grid/events/events.json +++ b/docs/data/data-grid/events/events.json @@ -249,7 +249,7 @@ "name": "rowsScroll", "description": "Fired during the scroll of the grid viewport.", "params": "GridScrollParams", - "event": "MuiEvent<{}>" + "event": "MuiEvent" }, { "name": "rowsScrollEnd", diff --git a/docs/src/pages/components/data-grid/events/events.json b/docs/src/pages/components/data-grid/events/events.json index e61873a789a6..2e7bd43681bf 100644 --- a/docs/src/pages/components/data-grid/events/events.json +++ b/docs/src/pages/components/data-grid/events/events.json @@ -249,7 +249,7 @@ "name": "rowsScroll", "description": "Fired during the scroll of the grid viewport.", "params": "GridScrollParams", - "event": "MuiEvent<{}>" + "event": "MuiEvent" }, { "name": "rowsScrollEnd", diff --git a/packages/grid/x-data-grid-generator/package.json b/packages/grid/x-data-grid-generator/package.json index cbb38467de2c..eccbe83516c7 100644 --- a/packages/grid/x-data-grid-generator/package.json +++ b/packages/grid/x-data-grid-generator/package.json @@ -45,7 +45,7 @@ "peerDependencies": { "@mui/icons-material": "^5.2.5", "@mui/material": "^5.2.8", - "react": "^17.0.2" + "react": "^17.0.2 || ^18.0.0" }, "setupFiles": [ "/src/setupTests.js" diff --git a/packages/grid/x-data-grid-pro/package.json b/packages/grid/x-data-grid-pro/package.json index 0a9ad0962893..731af17d40c2 100644 --- a/packages/grid/x-data-grid-pro/package.json +++ b/packages/grid/x-data-grid-pro/package.json @@ -53,7 +53,7 @@ "peerDependencies": { "@mui/material": "^5.2.8", "@mui/system": "^5.2.8", - "react": "^17.0.2" + "react": "^17.0.2 || ^18.0.0" }, "setupFiles": [ "/src/setupTests.js" diff --git a/packages/grid/x-data-grid-pro/src/components/DataGridProColumnHeaders.tsx b/packages/grid/x-data-grid-pro/src/components/DataGridProColumnHeaders.tsx index 23ef7d629d5d..42229dfbb151 100644 --- a/packages/grid/x-data-grid-pro/src/components/DataGridProColumnHeaders.tsx +++ b/packages/grid/x-data-grid-pro/src/components/DataGridProColumnHeaders.tsx @@ -122,27 +122,15 @@ export const DataGridProColumnHeaders = React.forwardRef< const pinnedColumns = useGridSelector(apiRef, gridPinnedColumnsSelector); const [leftPinnedColumns, rightPinnedColumns] = filterColumns(pinnedColumns, visibleColumnFields); - const { - isDragging, - renderContext, - updateInnerPosition, - getRootProps, - getInnerProps, - getColumns, - } = useGridColumnHeaders({ - innerRef, - minColumnIndex: leftPinnedColumns.length, - }); + const { isDragging, renderContext, getRootProps, getInnerProps, getColumns } = + useGridColumnHeaders({ + innerRef, + minColumnIndex: leftPinnedColumns.length, + }); const ownerState = { leftPinnedColumns, rightPinnedColumns, classes: rootProps.classes }; const classes = useUtilityClasses(ownerState); - React.useEffect(() => { - if (renderContext) { - updateInnerPosition(renderContext); - } - }, [renderContext, updateInnerPosition]); - const leftRenderContext = renderContext && leftPinnedColumns.length ? { diff --git a/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx b/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx index 454de43a2789..ad882cf95ad8 100644 --- a/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx +++ b/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx @@ -210,10 +210,6 @@ const DataGridProVirtualScroller = React.forwardRef< useGridApiEventHandler(apiRef, GridEvents.columnWidthChange, refreshRenderZonePosition); useGridApiEventHandler(apiRef, GridEvents.columnOrderChange, refreshRenderZonePosition); - React.useEffect(() => { - refreshRenderZonePosition(); - }, [refreshRenderZonePosition]); - const leftRenderContext = renderContext && leftPinnedColumns.length > 0 ? { diff --git a/packages/grid/x-data-grid/package.json b/packages/grid/x-data-grid/package.json index fd35b4d7d6fc..06379dee3270 100644 --- a/packages/grid/x-data-grid/package.json +++ b/packages/grid/x-data-grid/package.json @@ -54,7 +54,7 @@ "peerDependencies": { "@mui/material": "^5.2.8", "@mui/system": "^5.2.8", - "react": "^17.0.2" + "react": "^17.0.2 || ^18.0.0" }, "setupFiles": [ "/src/setupTests.js" diff --git a/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx b/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx index 975d56e31ce3..d6a05b1ef752 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import * as ReactDOM from 'react-dom'; import { useForkRef } from '@mui/material/utils'; import { useGridApiContext } from '../../utils/useGridApiContext'; import { useGridSelector } from '../../utils/useGridSelector'; @@ -26,6 +27,10 @@ interface UseGridColumnHeadersProps { minColumnIndex?: number; } +function isUIEvent(event: any): event is React.UIEvent { + return !!event.target; +} + export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { const { innerRef: innerRefProp, minColumnIndex = 0 } = props; @@ -70,8 +75,14 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { [columnPositions, minColumnIndex, rootProps.columnBuffer], ); + React.useLayoutEffect(() => { + if (renderContext) { + updateInnerPosition(renderContext); + } + }, [renderContext, updateInnerPosition]); + const handleScroll = React.useCallback>( - ({ left, renderContext: nextRenderContext = null }) => { + ({ left, renderContext: nextRenderContext = null }, event) => { if (!innerRef.current) { return; } @@ -87,13 +98,30 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { } prevScrollLeft.current = left; + // We can only update the position when we guarantee that the render context has been + // rendered. This is achieved using ReactDOM.flushSync or when the context doesn't change. + let canUpdateInnerPosition = false; + if (nextRenderContext !== prevRenderContext.current || !prevRenderContext.current) { - setRenderContext(nextRenderContext); + // ReactDOM.flushSync cannot be called on `scroll` events fired inside effects + if (isUIEvent(event)) { + // To prevent flickering, the inner position can only be updated after the new context has + // been rendered. ReactDOM.flushSync ensures that the state changes will happen before + // updating the position. + ReactDOM.flushSync(() => { + setRenderContext(nextRenderContext); + }); + canUpdateInnerPosition = true; + } else { + setRenderContext(nextRenderContext); + } prevRenderContext.current = nextRenderContext; + } else { + canUpdateInnerPosition = true; } // Pass directly the render context to avoid waiting for the next render - if (nextRenderContext) { + if (nextRenderContext && canUpdateInnerPosition) { updateInnerPosition(nextRenderContext); } }, @@ -204,7 +232,6 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { renderContext, getColumns, isDragging: !!dragCol, - updateInnerPosition, getRootProps: (other = {}) => ({ style: rootStyle, ...other }), getInnerProps: () => ({ ref: handleInnerRef, 'aria-rowindex': 1, role: 'row' }), }; diff --git a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index c681a1445228..ad266c61effa 100644 --- a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import * as ReactDOM from 'react-dom'; import { useForkRef } from '@mui/material/utils'; import { useGridApiContext } from '../../utils/useGridApiContext'; import { useGridRootProps } from '../../utils/useGridRootProps'; @@ -197,13 +198,18 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { ], ); + React.useLayoutEffect(() => { + if (renderContext) { + updateRenderZonePosition(renderContext); + } + }, [renderContext, updateRenderZonePosition]); + const updateRenderContext = React.useCallback( (nextRenderContext) => { setRenderContext(nextRenderContext); - updateRenderZonePosition(nextRenderContext); prevRenderContext.current = nextRenderContext; }, - [setRenderContext, prevRenderContext, updateRenderZonePosition], + [setRenderContext, prevRenderContext], ); React.useEffect(() => { @@ -212,7 +218,6 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { } const initialRenderContext = computeRenderContext(); - prevRenderContext.current = initialRenderContext; updateRenderContext(initialRenderContext); const { top, left } = scrollPosition.current!; @@ -257,14 +262,21 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { prevTotalWidth.current !== columnsTotalWidth; // TODO v6: rename event to a wider name, it's not only fired for row scrolling - apiRef.current.publishEvent(GridEvents.rowsScroll, { - top: scrollTop, - left: scrollLeft, - renderContext: shouldSetState ? nextRenderContext : prevRenderContext.current, - }); + apiRef.current.publishEvent( + GridEvents.rowsScroll, + { + top: scrollTop, + left: scrollLeft, + renderContext: shouldSetState ? nextRenderContext : prevRenderContext.current, + }, + event, + ); if (shouldSetState) { - updateRenderContext(nextRenderContext); + // Prevents batching render context changes + ReactDOM.flushSync(() => { + updateRenderContext(nextRenderContext); + }); prevTotalWidth.current = columnsTotalWidth; } }; diff --git a/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts b/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts index c7c406605292..3fb41f504cd8 100644 --- a/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts +++ b/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts @@ -207,7 +207,7 @@ export interface GridEventLookup }; // Scroll - rowsScroll: { params: GridScrollParams }; + rowsScroll: { params: GridScrollParams; event: React.UIEvent | MuiBaseEvent }; virtualScrollerContentSizeChange: {}; // Selection diff --git a/packages/x-date-pickers-pro/package.json b/packages/x-date-pickers-pro/package.json index e2a39725cbc3..70221d3f0ccb 100644 --- a/packages/x-date-pickers-pro/package.json +++ b/packages/x-date-pickers-pro/package.json @@ -60,8 +60,8 @@ "dayjs": "^1.10.7", "luxon": "^1.28.0 || ^2.0.0", "moment": "^2.29.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" }, "peerDependenciesMeta": { "date-fns": { diff --git a/packages/x-date-pickers/package.json b/packages/x-date-pickers/package.json index 0945a85718b3..4c2d8e2179da 100644 --- a/packages/x-date-pickers/package.json +++ b/packages/x-date-pickers/package.json @@ -61,8 +61,8 @@ "dayjs": "^1.10.7", "luxon": "^1.28.0 || ^2.0.0", "moment": "^2.29.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" }, "peerDependenciesMeta": { "date-fns": { diff --git a/packages/x-license-pro/package.json b/packages/x-license-pro/package.json index c1ba0048a6c3..c3d1d3c50cae 100644 --- a/packages/x-license-pro/package.json +++ b/packages/x-license-pro/package.json @@ -39,7 +39,7 @@ "yargs": "^17.4.1" }, "peerDependencies": { - "react": "^17.0.2" + "react": "^17.0.2 || ^18.0.0" }, "setupFiles": [ "/src/setupTests.js"