diff --git a/docs/data/data-grid/column-pinning/ControlPinnedColumns.tsx b/docs/data/data-grid/column-pinning/ControlPinnedColumns.tsx index 21aa40edfce8..4e8e7868915d 100644 --- a/docs/data/data-grid/column-pinning/ControlPinnedColumns.tsx +++ b/docs/data/data-grid/column-pinning/ControlPinnedColumns.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { DataGridPro, GridColDef, - GridPinnedColumns, + GridPinnedColumnFields, GridRowsProp, } from '@mui/x-data-grid-pro'; import { @@ -15,12 +15,12 @@ import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; export default function ControlPinnedColumns() { - const [pinnedColumns, setPinnedColumns] = React.useState({ + const [pinnedColumns, setPinnedColumns] = React.useState({ left: ['name'], }); const handlePinnedColumnsChange = React.useCallback( - (updatedPinnedColumns: GridPinnedColumns) => { + (updatedPinnedColumns: GridPinnedColumnFields) => { setPinnedColumns(updatedPinnedColumns); }, [], diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json index 325889a84c4e..0ff032511df7 100644 --- a/docs/data/data-grid/events/events.json +++ b/docs/data/data-grid/events/events.json @@ -273,8 +273,8 @@ { "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"], "name": "renderedRowsIntervalChange", - "description": "Fired when the rendered rows index interval changes. Called with a GridRenderedRowsIntervalChangeParams object.", - "params": "GridRenderedRowsIntervalChangeParams", + "description": "Fired when the rendered rows index interval changes. Called with a GridRenderContext object.", + "params": "GridRenderContext", "event": "MuiEvent<{}>" }, { diff --git a/docs/data/data-grid/filtering/CustomHeaderFilterDataGridPro.js b/docs/data/data-grid/filtering/CustomHeaderFilterDataGridPro.js index 7ccaf3f7296e..59520ac9e415 100644 --- a/docs/data/data-grid/filtering/CustomHeaderFilterDataGridPro.js +++ b/docs/data/data-grid/filtering/CustomHeaderFilterDataGridPro.js @@ -62,7 +62,11 @@ function CustomHeaderFilter(props) { return ( { const dimensions = apiRef.current.getRootDimensions(); - return dimensions!.viewportInnerSize.width; + return dimensions.viewportInnerSize.width; }); const handleViewportInnerSizeChange = React.useCallback(() => { const dimensions = apiRef.current.getRootDimensions(); - setWidth(dimensions!.viewportInnerSize.width); + setWidth(dimensions.viewportInnerSize.width); }, [apiRef]); React.useEffect(() => { diff --git a/docs/data/data-grid/performance/GridVisualization.tsx b/docs/data/data-grid/performance/GridVisualization.tsx index 2d62ee4dada1..51781f6abc4c 100644 --- a/docs/data/data-grid/performance/GridVisualization.tsx +++ b/docs/data/data-grid/performance/GridVisualization.tsx @@ -28,7 +28,7 @@ const TraceUpdates = React.forwardRef((props, ref) => { const CellWithTracer = React.forwardRef((props, ref) => { return ; -}); +}) as typeof GridCell; const slots = { cell: CellWithTracer, diff --git a/docs/data/data-grid/style/StripedGrid.js b/docs/data/data-grid/style/StripedGrid.js index 3b0524776ee1..b3a6344ece42 100644 --- a/docs/data/data-grid/style/StripedGrid.js +++ b/docs/data/data-grid/style/StripedGrid.js @@ -8,7 +8,7 @@ const ODD_OPACITY = 0.2; const StripedDataGrid = styled(DataGrid)(({ theme }) => ({ [`& .${gridClasses.row}.even`]: { backgroundColor: theme.palette.grey[200], - '&:hover, &.Mui-hovered': { + '&:hover': { backgroundColor: alpha(theme.palette.primary.main, ODD_OPACITY), '@media (hover: none)': { backgroundColor: 'transparent', @@ -19,7 +19,7 @@ const StripedDataGrid = styled(DataGrid)(({ theme }) => ({ theme.palette.primary.main, ODD_OPACITY + theme.palette.action.selectedOpacity, ), - '&:hover, &.Mui-hovered': { + '&:hover': { backgroundColor: alpha( theme.palette.primary.main, ODD_OPACITY + diff --git a/docs/data/data-grid/style/StripedGrid.tsx b/docs/data/data-grid/style/StripedGrid.tsx index 3b0524776ee1..b3a6344ece42 100644 --- a/docs/data/data-grid/style/StripedGrid.tsx +++ b/docs/data/data-grid/style/StripedGrid.tsx @@ -8,7 +8,7 @@ const ODD_OPACITY = 0.2; const StripedDataGrid = styled(DataGrid)(({ theme }) => ({ [`& .${gridClasses.row}.even`]: { backgroundColor: theme.palette.grey[200], - '&:hover, &.Mui-hovered': { + '&:hover': { backgroundColor: alpha(theme.palette.primary.main, ODD_OPACITY), '@media (hover: none)': { backgroundColor: 'transparent', @@ -19,7 +19,7 @@ const StripedDataGrid = styled(DataGrid)(({ theme }) => ({ theme.palette.primary.main, ODD_OPACITY + theme.palette.action.selectedOpacity, ), - '&:hover, &.Mui-hovered': { + '&:hover': { backgroundColor: alpha( theme.palette.primary.main, ODD_OPACITY + diff --git a/docs/data/migration/migration-data-grid-v6/migration-data-grid-v6.md b/docs/data/migration/migration-data-grid-v6/migration-data-grid-v6.md index f7da6166a977..aac644fca6cf 100644 --- a/docs/data/migration/migration-data-grid-v6/migration-data-grid-v6.md +++ b/docs/data/migration/migration-data-grid-v6/migration-data-grid-v6.md @@ -69,6 +69,14 @@ Since v7 is a major release, it contains some changes that affect the public API These changes were done for consistency, improve stability and make room for new features. Below are described the steps you need to make to migrate from v6 to v7. +### DOM changes + +The layout of the grid has been substantially altered to use CSS sticky positioned elements. As a result, the following changes have been made: + +- The main element now corresponds to the virtal scroller element. +- Headers are now contained in the virtual scroller. +- Pinned row and column sections are now contained in the virtual scroller. + @@ -138,6 +146,9 @@ Below are described the steps you need to make to migrate from v6 to v7. ; ``` +- The type `GridPinnedColumns` has been renamed to `GridPinnedColumnFields`. +- The type `GridPinnedPosition` has been renamed to `GridPinnedColumnPosition`. + @@ -281,17 +292,28 @@ Below are described the steps you need to make to migrate from v6 to v7. } ``` - + | MUI X v6 classes | MUI X v7 classes | Note | + | :------------------------------------------ | :--------------- | :--------------------- | --- | + | `.Mui-hovered` | `:hover` | For rows | + | `.MuiDataGrid--pinnedColumns-(left\|right)` | Removed | Not applicable anymore | --> | - +- The method `getRootDimensions()` now returns a non-null value. +- The field `mainElementRef` is now always non-null. +- The field `rootElementRef` is now always non-null. +- The field `virtualScrollerRef` is now always non-null. +- The event `renderedRowsIntervalChange` params changed from `GridRenderedRowsIntervalChangeParams` to `GridRenderContext`, and the former has been removed. + +### Changes to slots + +- The slot `columnHeaders` has had these props removed: `columnPositions`, `densityFactor`, `minColumnIndex`. +- The slot `row` has had these props removed: `containerWidth`, `position`. +- The slot `row` has typed props now. +- The slot `headerFilterCell` has had these props removed: `filterOperators`. diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index 13e29be42ec8..ea56fe4a27d7 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -412,7 +412,7 @@ "onPinnedColumnsChange": { "type": { "name": "func" }, "signature": { - "type": "function(pinnedColumns: GridPinnedColumns, details: GridCallbackDetails) => void", + "type": "function(pinnedColumns: GridPinnedColumnFields, details: GridCallbackDetails) => void", "describedArgs": ["pinnedColumns", "details"] } }, @@ -691,6 +691,18 @@ "description": "Styles applied to the cell element if the cell is in edit mode.", "isGlobal": false }, + { + "key": "cell--pinnedLeft", + "className": "MuiDataGridPremium-cell--pinnedLeft", + "description": "Styles applied to the cell element if it is pinned to the left.", + "isGlobal": false + }, + { + "key": "cell--pinnedRight", + "className": "MuiDataGridPremium-cell--pinnedRight", + "description": "Styles applied to the cell element if it is pinned to the right.", + "isGlobal": false + }, { "key": "cell--rangeBottom", "className": "MuiDataGridPremium-cell--rangeBottom", @@ -739,6 +751,12 @@ "description": "Styles applied to the cell element if `align=\"right\"`.", "isGlobal": false }, + { + "key": "cell--withLeftBorder", + "className": "MuiDataGridPremium-cell--withLeftBorder", + "description": "Styles applied the cell if `showColumnVerticalBorder={true}`.", + "isGlobal": false + }, { "key": "cell--withRenderer", "className": "MuiDataGridPremium-cell--withRenderer", @@ -763,6 +781,12 @@ "description": "Styles applied to the element that wraps the cell content.", "isGlobal": false }, + { + "key": "cellEmpty", + "className": "MuiDataGridPremium-cellEmpty", + "description": "Styles applied to the empty cell element.", + "isGlobal": false + }, { "key": "cellSkeleton", "className": "MuiDataGridPremium-cellSkeleton", @@ -961,6 +985,18 @@ "description": "Styles applied to the columns panel row element.", "isGlobal": false }, + { + "key": "container--bottom", + "className": "MuiDataGridPremium-container--bottom", + "description": "Styles applied to the bottom container.", + "isGlobal": false + }, + { + "key": "container--top", + "className": "MuiDataGridPremium-container--top", + "description": "Styles applied to the top container.", + "isGlobal": false + }, { "key": "detailPanel", "className": "MuiDataGridPremium-detailPanel", @@ -997,6 +1033,24 @@ "description": "Styles applied to the root of the input component.", "isGlobal": false }, + { + "key": "filler", + "className": "MuiDataGridPremium-filler", + "description": "Styles applied to the filler row.", + "isGlobal": false + }, + { + "key": "filler--pinnedLeft", + "className": "MuiDataGridPremium-filler--pinnedLeft", + "description": "Styles applied to the filler row pinned left section.", + "isGlobal": false + }, + { + "key": "filler--pinnedRight", + "className": "MuiDataGridPremium-filler--pinnedRight", + "description": "Styles applied to the filler row pinned right section.", + "isGlobal": false + }, { "key": "filterForm", "className": "MuiDataGridPremium-filterForm", @@ -1087,6 +1141,12 @@ "description": "Styles applied to the main container element.", "isGlobal": false }, + { + "key": "main--hasPinnedRight", + "className": "MuiDataGridPremium-main--hasPinnedRight", + "description": "Styles applied to the main container element when it has right pinned columns.", + "isGlobal": false + }, { "key": "menu", "className": "MuiDataGridPremium-menu", @@ -1195,18 +1255,6 @@ "description": "Styles applied to the pinned columns.", "isGlobal": false }, - { - "key": "pinnedColumns--left", - "className": "MuiDataGridPremium-pinnedColumns--left", - "description": "Styles applied to the left pinned columns.", - "isGlobal": false - }, - { - "key": "pinnedColumns--right", - "className": "MuiDataGridPremium-pinnedColumns--right", - "description": "Styles applied to the right pinned columns.", - "isGlobal": false - }, { "key": "pinnedRows", "className": "MuiDataGridPremium-pinnedRows", @@ -1297,6 +1345,12 @@ "description": "Styles applied to the row element if the row is in edit mode.", "isGlobal": false }, + { + "key": "row--firstVisible", + "className": "MuiDataGridPremium-row--firstVisible", + "description": "Styles applied to the first visible row element on every page of the grid.", + "isGlobal": false + }, { "key": "row--lastVisible", "className": "MuiDataGridPremium-row--lastVisible", @@ -1351,6 +1405,24 @@ "description": "Styles applied to the right scroll area element.", "isGlobal": false }, + { + "key": "scrollbar", + "className": "MuiDataGridPremium-scrollbar", + "description": "Styles applied to the scrollbars.", + "isGlobal": false + }, + { + "key": "scrollbar--horizontal", + "className": "MuiDataGridPremium-scrollbar--horizontal", + "description": "Styles applied to the horizontal scrollbar.", + "isGlobal": false + }, + { + "key": "scrollbar--vertical", + "className": "MuiDataGridPremium-scrollbar--vertical", + "description": "Styles applied to the horizontal scrollbar.", + "isGlobal": false + }, { "key": "selectedRowCount", "className": "MuiDataGridPremium-selectedRowCount", @@ -1416,6 +1488,12 @@ "className": "MuiDataGridPremium-withBorderColor", "description": "Styles applied to cells, column header and other elements that have border.\nSets border color only.", "isGlobal": false + }, + { + "key": "withVerticalBorder", + "className": "MuiDataGridPremium-withVerticalBorder", + "description": "Styles applied the grid if `showColumnVerticalBorder={true}`.", + "isGlobal": false } ], "muiName": "MuiDataGridPremium", diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 7d6ec7501e6c..cd2ed8571915 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -366,7 +366,7 @@ "onPinnedColumnsChange": { "type": { "name": "func" }, "signature": { - "type": "function(pinnedColumns: GridPinnedColumns, details: GridCallbackDetails) => void", + "type": "function(pinnedColumns: GridPinnedColumnFields, details: GridCallbackDetails) => void", "describedArgs": ["pinnedColumns", "details"] } }, @@ -630,6 +630,18 @@ "description": "Styles applied to the cell element if the cell is in edit mode.", "isGlobal": false }, + { + "key": "cell--pinnedLeft", + "className": "MuiDataGridPro-cell--pinnedLeft", + "description": "Styles applied to the cell element if it is pinned to the left.", + "isGlobal": false + }, + { + "key": "cell--pinnedRight", + "className": "MuiDataGridPro-cell--pinnedRight", + "description": "Styles applied to the cell element if it is pinned to the right.", + "isGlobal": false + }, { "key": "cell--rangeBottom", "className": "MuiDataGridPro-cell--rangeBottom", @@ -678,6 +690,12 @@ "description": "Styles applied to the cell element if `align=\"right\"`.", "isGlobal": false }, + { + "key": "cell--withLeftBorder", + "className": "MuiDataGridPro-cell--withLeftBorder", + "description": "Styles applied the cell if `showColumnVerticalBorder={true}`.", + "isGlobal": false + }, { "key": "cell--withRenderer", "className": "MuiDataGridPro-cell--withRenderer", @@ -702,6 +720,12 @@ "description": "Styles applied to the element that wraps the cell content.", "isGlobal": false }, + { + "key": "cellEmpty", + "className": "MuiDataGridPro-cellEmpty", + "description": "Styles applied to the empty cell element.", + "isGlobal": false + }, { "key": "cellSkeleton", "className": "MuiDataGridPro-cellSkeleton", @@ -900,6 +924,18 @@ "description": "Styles applied to the columns panel row element.", "isGlobal": false }, + { + "key": "container--bottom", + "className": "MuiDataGridPro-container--bottom", + "description": "Styles applied to the bottom container.", + "isGlobal": false + }, + { + "key": "container--top", + "className": "MuiDataGridPro-container--top", + "description": "Styles applied to the top container.", + "isGlobal": false + }, { "key": "detailPanel", "className": "MuiDataGridPro-detailPanel", @@ -936,6 +972,24 @@ "description": "Styles applied to the root of the input component.", "isGlobal": false }, + { + "key": "filler", + "className": "MuiDataGridPro-filler", + "description": "Styles applied to the filler row.", + "isGlobal": false + }, + { + "key": "filler--pinnedLeft", + "className": "MuiDataGridPro-filler--pinnedLeft", + "description": "Styles applied to the filler row pinned left section.", + "isGlobal": false + }, + { + "key": "filler--pinnedRight", + "className": "MuiDataGridPro-filler--pinnedRight", + "description": "Styles applied to the filler row pinned right section.", + "isGlobal": false + }, { "key": "filterForm", "className": "MuiDataGridPro-filterForm", @@ -1026,6 +1080,12 @@ "description": "Styles applied to the main container element.", "isGlobal": false }, + { + "key": "main--hasPinnedRight", + "className": "MuiDataGridPro-main--hasPinnedRight", + "description": "Styles applied to the main container element when it has right pinned columns.", + "isGlobal": false + }, { "key": "menu", "className": "MuiDataGridPro-menu", @@ -1134,18 +1194,6 @@ "description": "Styles applied to the pinned columns.", "isGlobal": false }, - { - "key": "pinnedColumns--left", - "className": "MuiDataGridPro-pinnedColumns--left", - "description": "Styles applied to the left pinned columns.", - "isGlobal": false - }, - { - "key": "pinnedColumns--right", - "className": "MuiDataGridPro-pinnedColumns--right", - "description": "Styles applied to the right pinned columns.", - "isGlobal": false - }, { "key": "pinnedRows", "className": "MuiDataGridPro-pinnedRows", @@ -1236,6 +1284,12 @@ "description": "Styles applied to the row element if the row is in edit mode.", "isGlobal": false }, + { + "key": "row--firstVisible", + "className": "MuiDataGridPro-row--firstVisible", + "description": "Styles applied to the first visible row element on every page of the grid.", + "isGlobal": false + }, { "key": "row--lastVisible", "className": "MuiDataGridPro-row--lastVisible", @@ -1290,6 +1344,24 @@ "description": "Styles applied to the right scroll area element.", "isGlobal": false }, + { + "key": "scrollbar", + "className": "MuiDataGridPro-scrollbar", + "description": "Styles applied to the scrollbars.", + "isGlobal": false + }, + { + "key": "scrollbar--horizontal", + "className": "MuiDataGridPro-scrollbar--horizontal", + "description": "Styles applied to the horizontal scrollbar.", + "isGlobal": false + }, + { + "key": "scrollbar--vertical", + "className": "MuiDataGridPro-scrollbar--vertical", + "description": "Styles applied to the horizontal scrollbar.", + "isGlobal": false + }, { "key": "selectedRowCount", "className": "MuiDataGridPro-selectedRowCount", @@ -1355,6 +1427,12 @@ "className": "MuiDataGridPro-withBorderColor", "description": "Styles applied to cells, column header and other elements that have border.\nSets border color only.", "isGlobal": false + }, + { + "key": "withVerticalBorder", + "className": "MuiDataGridPro-withVerticalBorder", + "description": "Styles applied the grid if `showColumnVerticalBorder={true}`.", + "isGlobal": false } ], "muiName": "MuiDataGridPro", diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 77d24d39db65..0e3101a2ad4a 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -513,6 +513,18 @@ "description": "Styles applied to the cell element if the cell is in edit mode.", "isGlobal": false }, + { + "key": "cell--pinnedLeft", + "className": "MuiDataGrid-cell--pinnedLeft", + "description": "Styles applied to the cell element if it is pinned to the left.", + "isGlobal": false + }, + { + "key": "cell--pinnedRight", + "className": "MuiDataGrid-cell--pinnedRight", + "description": "Styles applied to the cell element if it is pinned to the right.", + "isGlobal": false + }, { "key": "cell--rangeBottom", "className": "MuiDataGrid-cell--rangeBottom", @@ -561,6 +573,12 @@ "description": "Styles applied to the cell element if `align=\"right\"`.", "isGlobal": false }, + { + "key": "cell--withLeftBorder", + "className": "MuiDataGrid-cell--withLeftBorder", + "description": "Styles applied the cell if `showColumnVerticalBorder={true}`.", + "isGlobal": false + }, { "key": "cell--withRenderer", "className": "MuiDataGrid-cell--withRenderer", @@ -585,6 +603,12 @@ "description": "Styles applied to the element that wraps the cell content.", "isGlobal": false }, + { + "key": "cellEmpty", + "className": "MuiDataGrid-cellEmpty", + "description": "Styles applied to the empty cell element.", + "isGlobal": false + }, { "key": "cellSkeleton", "className": "MuiDataGrid-cellSkeleton", @@ -783,6 +807,18 @@ "description": "Styles applied to the columns panel row element.", "isGlobal": false }, + { + "key": "container--bottom", + "className": "MuiDataGrid-container--bottom", + "description": "Styles applied to the bottom container.", + "isGlobal": false + }, + { + "key": "container--top", + "className": "MuiDataGrid-container--top", + "description": "Styles applied to the top container.", + "isGlobal": false + }, { "key": "detailPanel", "className": "MuiDataGrid-detailPanel", @@ -819,6 +855,24 @@ "description": "Styles applied to the root of the input component.", "isGlobal": false }, + { + "key": "filler", + "className": "MuiDataGrid-filler", + "description": "Styles applied to the filler row.", + "isGlobal": false + }, + { + "key": "filler--pinnedLeft", + "className": "MuiDataGrid-filler--pinnedLeft", + "description": "Styles applied to the filler row pinned left section.", + "isGlobal": false + }, + { + "key": "filler--pinnedRight", + "className": "MuiDataGrid-filler--pinnedRight", + "description": "Styles applied to the filler row pinned right section.", + "isGlobal": false + }, { "key": "filterForm", "className": "MuiDataGrid-filterForm", @@ -909,6 +963,12 @@ "description": "Styles applied to the main container element.", "isGlobal": false }, + { + "key": "main--hasPinnedRight", + "className": "MuiDataGrid-main--hasPinnedRight", + "description": "Styles applied to the main container element when it has right pinned columns.", + "isGlobal": false + }, { "key": "menu", "className": "MuiDataGrid-menu", @@ -1017,18 +1077,6 @@ "description": "Styles applied to the pinned columns.", "isGlobal": false }, - { - "key": "pinnedColumns--left", - "className": "MuiDataGrid-pinnedColumns--left", - "description": "Styles applied to the left pinned columns.", - "isGlobal": false - }, - { - "key": "pinnedColumns--right", - "className": "MuiDataGrid-pinnedColumns--right", - "description": "Styles applied to the right pinned columns.", - "isGlobal": false - }, { "key": "pinnedRows", "className": "MuiDataGrid-pinnedRows", @@ -1119,6 +1167,12 @@ "description": "Styles applied to the row element if the row is in edit mode.", "isGlobal": false }, + { + "key": "row--firstVisible", + "className": "MuiDataGrid-row--firstVisible", + "description": "Styles applied to the first visible row element on every page of the grid.", + "isGlobal": false + }, { "key": "row--lastVisible", "className": "MuiDataGrid-row--lastVisible", @@ -1173,6 +1227,24 @@ "description": "Styles applied to the right scroll area element.", "isGlobal": false }, + { + "key": "scrollbar", + "className": "MuiDataGrid-scrollbar", + "description": "Styles applied to the scrollbars.", + "isGlobal": false + }, + { + "key": "scrollbar--horizontal", + "className": "MuiDataGrid-scrollbar--horizontal", + "description": "Styles applied to the horizontal scrollbar.", + "isGlobal": false + }, + { + "key": "scrollbar--vertical", + "className": "MuiDataGrid-scrollbar--vertical", + "description": "Styles applied to the horizontal scrollbar.", + "isGlobal": false + }, { "key": "selectedRowCount", "className": "MuiDataGrid-selectedRowCount", @@ -1238,6 +1310,12 @@ "className": "MuiDataGrid-withBorderColor", "description": "Styles applied to cells, column header and other elements that have border.\nSets border color only.", "isGlobal": false + }, + { + "key": "withVerticalBorder", + "className": "MuiDataGrid-withVerticalBorder", + "description": "Styles applied the grid if `showColumnVerticalBorder={true}`.", + "isGlobal": false } ], "muiName": "MuiDataGrid", diff --git a/docs/pages/x/api/data-grid/grid-api.md b/docs/pages/x/api/data-grid/grid-api.md index 37e92f069b2f..597f51912011 100644 --- a/docs/pages/x/api/data-grid/grid-api.md +++ b/docs/pages/x/api/data-grid/grid-api.md @@ -51,8 +51,8 @@ import { GridApi } from '@mui/x-data-grid'; | getDataAsExcel [](/x/introduction/licensing/#premium-plan) | (options?: GridExcelExportOptions) => Promise<Excel.Workbook> \| null | Returns the grid data as an exceljs workbook.
This method is used internally by `exportDataAsExcel`. | | getExpandedDetailPanels [](/x/introduction/licensing/#pro-plan) | () => GridRowId[] | Returns the rows whose detail panel is open. | | getLocaleText | <T extends GridTranslationKeys>(key: T) => GridLocaleText[T] | Returns the translation for the `key`. | -| getPinnedColumns [](/x/introduction/licensing/#pro-plan) | () => GridPinnedColumns | Returns which columns are pinned. | -| getRootDimensions | () => GridDimensions \| null | Returns the dimensions of the grid | +| getPinnedColumns [](/x/introduction/licensing/#pro-plan) | () => GridPinnedColumnFields | Returns which columns are pinned. | +| getRootDimensions | () => GridDimensions | Returns the dimensions of the grid | | getRow | <R extends GridValidRowModel = any>(id: GridRowId) => R \| null | Gets the row data with a given id. | | getRowElement | (id: GridRowId) => HTMLDivElement \| null | Gets the underlying DOM element for a row at the given `id`. | | getRowGroupChildren [](/x/introduction/licensing/#pro-plan) | (params: GridRowGroupChildrenGetterParams) => GridRowId[] | Gets the rows of a grouping criteria.
Only contains the rows provided to the grid, not the rows automatically generated by it. | @@ -79,10 +79,10 @@ import { GridApi } from '@mui/x-data-grid'; | ignoreDiacritics | DataGridProcessedProps['ignoreDiacritics'] | Returns the value of the `ignoreDiacritics` prop. | | isCellEditable | (params: GridCellParams) => boolean | Controls if a cell is editable. | | isCellSelected [](/x/introduction/licensing/#premium-plan) | (id: GridRowId, field: GridColDef['field']) => boolean | Determines if a cell is selected or not. | -| isColumnPinned [](/x/introduction/licensing/#pro-plan) | (field: string) => GridPinnedPosition \| false | Returns which side a column is pinned to. | +| isColumnPinned [](/x/introduction/licensing/#pro-plan) | (field: string) => GridPinnedColumnPosition \| false | Returns which side a column is pinned to. | | isRowSelectable | (id: GridRowId) => boolean | Determines if a row can be selected or not. | | isRowSelected | (id: GridRowId) => boolean | Determines if a row is selected or not. | -| pinColumn [](/x/introduction/licensing/#pro-plan) | (field: string, side: GridPinnedPosition) => void | Pins a column to the left or right side of the grid. | +| pinColumn [](/x/introduction/licensing/#pro-plan) | (field: string, side: GridPinnedColumnPosition) => void | Pins a column to the left or right side of the grid. | | publishEvent | GridEventPublisher | Emits an event. | | removeRowGroupingCriteria [](/x/introduction/licensing/#premium-plan) | (groupingCriteriaField: string) => void | Remove the field from the row grouping model. | | resetRowHeights | () => void | Forces the recalculation of the heights of all rows. | @@ -111,7 +111,7 @@ import { GridApi } from '@mui/x-data-grid'; | setPage | (page: number) => void | Sets the displayed page to the value given by `page`. | | setPageSize | (pageSize: number) => void | Sets the number of displayed rows to the value given by `pageSize`. | | setPaginationModel | (model: GridPaginationModel) => void | Sets the `paginationModel` to a new value. | -| setPinnedColumns [](/x/introduction/licensing/#pro-plan) | (pinnedColumns: GridPinnedColumns) => void | Changes the pinned columns. | +| setPinnedColumns [](/x/introduction/licensing/#pro-plan) | (pinnedColumns: GridPinnedColumnFields) => void | Changes the pinned columns. | | setQuickFilterValues | (values: any[]) => void | Set the quick filter values to the one given by `values` | | setRowChildrenExpansion [](/x/introduction/licensing/#pro-plan) | (id: GridRowId, isExpanded: boolean) => void | Expand or collapse a row children. | | setRowGroupingCriteriaIndex [](/x/introduction/licensing/#premium-plan) | (groupingCriteriaField: string, groupingIndex: number) => void | Sets the grouping index of a grouping criteria. | diff --git a/docs/pages/x/api/data-grid/grid-column-pinning-api.json b/docs/pages/x/api/data-grid/grid-column-pinning-api.json index ca096a53f413..6988ed2cd184 100644 --- a/docs/pages/x/api/data-grid/grid-column-pinning-api.json +++ b/docs/pages/x/api/data-grid/grid-column-pinning-api.json @@ -5,22 +5,22 @@ { "name": "getPinnedColumns", "description": "Returns which columns are pinned.", - "type": "() => GridPinnedColumns" + "type": "() => GridPinnedColumnFields" }, { "name": "isColumnPinned", "description": "Returns which side a column is pinned to.", - "type": "(field: string) => GridPinnedPosition | false" + "type": "(field: string) => GridPinnedColumnPosition | false" }, { "name": "pinColumn", "description": "Pins a column to the left or right side of the grid.", - "type": "(field: string, side: GridPinnedPosition) => void" + "type": "(field: string, side: GridPinnedColumnPosition) => void" }, { "name": "setPinnedColumns", "description": "Changes the pinned columns.", - "type": "(pinnedColumns: GridPinnedColumns) => void" + "type": "(pinnedColumns: GridPinnedColumnFields) => void" }, { "name": "unpinColumn", "description": "Unpins a column.", "type": "(field: string) => void" } ] diff --git a/docs/pages/x/api/data-grid/selectors.json b/docs/pages/x/api/data-grid/selectors.json index d8ac05a379a5..cd772984ed36 100644 --- a/docs/pages/x/api/data-grid/selectors.json +++ b/docs/pages/x/api/data-grid/selectors.json @@ -96,6 +96,13 @@ "description": "Get the column visibility model, containing the visibility status of each column.\nIf a column is not registered in the model, it is visible.", "supportsApiRef": true }, + { + "name": "gridColumnsStateSelector", + "returnType": "GridColumnsState", + "category": "Columns", + "description": "Get the columns state", + "supportsApiRef": false + }, { "name": "gridColumnsTotalWidthSelector", "returnType": "number", @@ -145,6 +152,12 @@ "description": "", "supportsApiRef": true }, + { + "name": "gridDimensionsSelector", + "returnType": "GridDimensions", + "description": "", + "supportsApiRef": false + }, { "name": "gridExpandedRowCountSelector", "returnType": "number", @@ -308,8 +321,9 @@ }, { "name": "gridPinnedColumnsSelector", - "returnType": "GridPinnedColumns", - "description": "", + "returnType": "GridPinnedColumnFields", + "category": "Visible Columns", + "description": "Get the visible pinned columns model.", "supportsApiRef": false }, { @@ -489,6 +503,13 @@ "description": "Get the field of each visible column.", "supportsApiRef": true }, + { + "name": "gridVisiblePinnedColumnDefinitionsSelector", + "returnType": "{ left: GridStateColDef[]; right: GridStateColDef[] }", + "category": "Visible Columns", + "description": "Get the visible pinned columns.", + "supportsApiRef": true + }, { "name": "selectedGridRowsCountSelector", "returnType": "number", diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index 99b67b13056d..5fad536889e7 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -690,6 +690,16 @@ "nodeName": "the cell element", "conditions": "the cell is in edit mode" }, + "cell--pinnedLeft": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is pinned to the left" + }, + "cell--pinnedRight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is pinned to the right" + }, "cell--rangeBottom": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the cell element", @@ -730,6 +740,9 @@ "nodeName": "the cell element", "conditions": "align=\"right\"" }, + "cell--withLeftBorder": { + "description": "Styles applied the cell if showColumnVerticalBorder={true}." + }, "cell--withRenderer": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the cell element", @@ -746,6 +759,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the element that wraps the cell content" }, + "cellEmpty": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the empty cell element" + }, "cellSkeleton": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the skeleton cell element" @@ -893,6 +910,14 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the columns panel row element" }, + "container--bottom": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the bottom container" + }, + "container--top": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the top container" + }, "detailPanel": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the detail panel element" @@ -918,6 +943,15 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the root of the input component" }, + "filler": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the filler row" }, + "filler--pinnedLeft": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the filler row pinned left section" + }, + "filler--pinnedRight": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the filler row pinned right section" + }, "filterForm": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the root of the filter form component" @@ -976,6 +1010,11 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the main container element" }, + "main--hasPinnedRight": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the main container element", + "conditions": "it has right pinned columns" + }, "menu": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the menu element" }, "menuIcon": { "description": "Styles applied to {{nodeName}}.", @@ -1040,14 +1079,6 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the pinned columns" }, - "pinnedColumns--left": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the left pinned columns" - }, - "pinnedColumns--right": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the right pinned columns" - }, "pinnedRows": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the pinned rows container" @@ -1111,6 +1142,10 @@ "nodeName": "the row element", "conditions": "the row is in edit mode" }, + "row--firstVisible": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the first visible row element on every page of the grid" + }, "row--lastVisible": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the last visible row element on every page of the grid" @@ -1145,6 +1180,15 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the right scroll area element" }, + "scrollbar": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the scrollbars" }, + "scrollbar--horizontal": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the horizontal scrollbar" + }, + "scrollbar--vertical": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the horizontal scrollbar" + }, "selectedRowCount": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the footer selected row count element" @@ -1190,6 +1234,9 @@ "description": "Styles applied to {{nodeName}}, {{conditions}}. Sets border color only.", "nodeName": "cells", "conditions": "column header and other elements that have border" + }, + "withVerticalBorder": { + "description": "Styles applied the grid if showColumnVerticalBorder={true}." } } } diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 194a411e8991..65d3dc3b819f 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -632,6 +632,16 @@ "nodeName": "the cell element", "conditions": "the cell is in edit mode" }, + "cell--pinnedLeft": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is pinned to the left" + }, + "cell--pinnedRight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is pinned to the right" + }, "cell--rangeBottom": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the cell element", @@ -672,6 +682,9 @@ "nodeName": "the cell element", "conditions": "align=\"right\"" }, + "cell--withLeftBorder": { + "description": "Styles applied the cell if showColumnVerticalBorder={true}." + }, "cell--withRenderer": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the cell element", @@ -688,6 +701,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the element that wraps the cell content" }, + "cellEmpty": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the empty cell element" + }, "cellSkeleton": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the skeleton cell element" @@ -835,6 +852,14 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the columns panel row element" }, + "container--bottom": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the bottom container" + }, + "container--top": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the top container" + }, "detailPanel": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the detail panel element" @@ -860,6 +885,15 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the root of the input component" }, + "filler": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the filler row" }, + "filler--pinnedLeft": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the filler row pinned left section" + }, + "filler--pinnedRight": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the filler row pinned right section" + }, "filterForm": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the root of the filter form component" @@ -918,6 +952,11 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the main container element" }, + "main--hasPinnedRight": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the main container element", + "conditions": "it has right pinned columns" + }, "menu": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the menu element" }, "menuIcon": { "description": "Styles applied to {{nodeName}}.", @@ -982,14 +1021,6 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the pinned columns" }, - "pinnedColumns--left": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the left pinned columns" - }, - "pinnedColumns--right": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the right pinned columns" - }, "pinnedRows": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the pinned rows container" @@ -1053,6 +1084,10 @@ "nodeName": "the row element", "conditions": "the row is in edit mode" }, + "row--firstVisible": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the first visible row element on every page of the grid" + }, "row--lastVisible": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the last visible row element on every page of the grid" @@ -1087,6 +1122,15 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the right scroll area element" }, + "scrollbar": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the scrollbars" }, + "scrollbar--horizontal": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the horizontal scrollbar" + }, + "scrollbar--vertical": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the horizontal scrollbar" + }, "selectedRowCount": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the footer selected row count element" @@ -1132,6 +1176,9 @@ "description": "Styles applied to {{nodeName}}, {{conditions}}. Sets border color only.", "nodeName": "cells", "conditions": "column header and other elements that have border" + }, + "withVerticalBorder": { + "description": "Styles applied the grid if showColumnVerticalBorder={true}." } } } diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index d1ca8a00743e..6f81db9663cb 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -498,6 +498,16 @@ "nodeName": "the cell element", "conditions": "the cell is in edit mode" }, + "cell--pinnedLeft": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is pinned to the left" + }, + "cell--pinnedRight": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the cell element", + "conditions": "it is pinned to the right" + }, "cell--rangeBottom": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the cell element", @@ -538,6 +548,9 @@ "nodeName": "the cell element", "conditions": "align=\"right\"" }, + "cell--withLeftBorder": { + "description": "Styles applied the cell if showColumnVerticalBorder={true}." + }, "cell--withRenderer": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the cell element", @@ -554,6 +567,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the element that wraps the cell content" }, + "cellEmpty": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the empty cell element" + }, "cellSkeleton": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the skeleton cell element" @@ -701,6 +718,14 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the columns panel row element" }, + "container--bottom": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the bottom container" + }, + "container--top": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the top container" + }, "detailPanel": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the detail panel element" @@ -726,6 +751,15 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the root of the input component" }, + "filler": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the filler row" }, + "filler--pinnedLeft": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the filler row pinned left section" + }, + "filler--pinnedRight": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the filler row pinned right section" + }, "filterForm": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the root of the filter form component" @@ -784,6 +818,11 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the main container element" }, + "main--hasPinnedRight": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the main container element", + "conditions": "it has right pinned columns" + }, "menu": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the menu element" }, "menuIcon": { "description": "Styles applied to {{nodeName}}.", @@ -848,14 +887,6 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the pinned columns" }, - "pinnedColumns--left": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the left pinned columns" - }, - "pinnedColumns--right": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the right pinned columns" - }, "pinnedRows": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the pinned rows container" @@ -919,6 +950,10 @@ "nodeName": "the row element", "conditions": "the row is in edit mode" }, + "row--firstVisible": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the first visible row element on every page of the grid" + }, "row--lastVisible": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the last visible row element on every page of the grid" @@ -953,6 +988,15 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the right scroll area element" }, + "scrollbar": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the scrollbars" }, + "scrollbar--horizontal": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the horizontal scrollbar" + }, + "scrollbar--vertical": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the horizontal scrollbar" + }, "selectedRowCount": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the footer selected row count element" @@ -998,6 +1042,9 @@ "description": "Styles applied to {{nodeName}}, {{conditions}}. Sets border color only.", "nodeName": "cells", "conditions": "column header and other elements that have border" + }, + "withVerticalBorder": { + "description": "Styles applied the grid if showColumnVerticalBorder={true}." } } } diff --git a/packages/grid/x-data-grid-generator/src/hooks/useBasicDemoData.ts b/packages/grid/x-data-grid-generator/src/hooks/useBasicDemoData.ts index 1b18bf282a20..93da3a18adf5 100644 --- a/packages/grid/x-data-grid-generator/src/hooks/useBasicDemoData.ts +++ b/packages/grid/x-data-grid-generator/src/hooks/useBasicDemoData.ts @@ -2,12 +2,5 @@ import * as React from 'react'; import { getBasicGridData, GridBasicData } from '../services'; export const useBasicDemoData = (nbRows: number, nbCols: number): GridBasicData => { - const [data, setData] = React.useState({ rows: [], columns: [] }); - - React.useEffect(() => { - const newData = getBasicGridData(nbRows, nbCols); - setData(newData); - }, [nbRows, nbCols]); - - return data; + return React.useMemo(() => getBasicGridData(nbRows, nbCols), [nbRows, nbCols]); }; diff --git a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 8711cef01dd3..b053112de969 100644 --- a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -8,13 +8,10 @@ import { GridRoot, GridContextProvider, GridValidRowModel, - useGridSelector, - gridPinnedColumnsSelector, } from '@mui/x-data-grid-pro'; import { propValidatorsDataGrid, propValidatorsDataGridPro, - DataGridProVirtualScroller, PropValidator, validateProps, } from '@mui/x-data-grid-pro/internals'; @@ -42,10 +39,7 @@ const DataGridPremiumRaw = React.forwardRef(function DataGridPremium - + @@ -725,7 +716,7 @@ DataGridPremiumRaw.propTypes = { onPaginationModelChange: PropTypes.func, /** * Callback fired when the pinned columns have changed. - * @param {GridPinnedColumns} pinnedColumns The changed pinned columns. + * @param {GridPinnedColumnFields} pinnedColumns The changed pinned columns. * @param {GridCallbackDetails} details Additional details for this callback. */ onPinnedColumnsChange: PropTypes.func, diff --git a/packages/grid/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx b/packages/grid/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx index aaca7bff91ac..706c5f66d1de 100644 --- a/packages/grid/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx +++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx @@ -28,6 +28,7 @@ import { sortingStateInitializer, useGridScroll, useGridEvents, + dimensionsStateInitializer, useGridDimensions, useGridStatePersistence, useGridRowSelectionPreProcessors, @@ -110,6 +111,7 @@ export const useDataGridPremiumComponent = ( /** * Register all state initializers here. */ + useGridInitializeState(dimensionsStateInitializer, apiRef, props); useGridInitializeState(headerFilteringStateInitializer, apiRef, props); useGridInitializeState(rowGroupingStateInitializer, apiRef, props); useGridInitializeState(aggregationStateInitializer, apiRef, props); diff --git a/packages/grid/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts b/packages/grid/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts index bc0062559120..7d3d888d5d26 100644 --- a/packages/grid/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts +++ b/packages/grid/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts @@ -277,19 +277,19 @@ export const useGridCellSelection = ( const dimensions = apiRef.current.getRootDimensions(); - if (mouseY <= AUTO_SCROLL_SENSITIVITY && dimensions?.hasScrollY) { + if (mouseY <= AUTO_SCROLL_SENSITIVITY && dimensions.hasScrollY) { // When scrolling up, the multiplier increases going closer to the top edge factor = (AUTO_SCROLL_SENSITIVITY - mouseY) / -AUTO_SCROLL_SENSITIVITY; deltaY = AUTO_SCROLL_SPEED; - } else if (mouseY >= height - AUTO_SCROLL_SENSITIVITY && dimensions?.hasScrollY) { + } else if (mouseY >= height - AUTO_SCROLL_SENSITIVITY && dimensions.hasScrollY) { // When scrolling down, the multiplier increases going closer to the bottom edge factor = (mouseY - (height - AUTO_SCROLL_SENSITIVITY)) / AUTO_SCROLL_SENSITIVITY; deltaY = AUTO_SCROLL_SPEED; - } else if (mouseX <= AUTO_SCROLL_SENSITIVITY && dimensions?.hasScrollX) { + } else if (mouseX <= AUTO_SCROLL_SENSITIVITY && dimensions.hasScrollX) { // When scrolling left, the multiplier increases going closer to the left edge factor = (AUTO_SCROLL_SENSITIVITY - mouseX) / -AUTO_SCROLL_SENSITIVITY; deltaX = AUTO_SCROLL_SPEED; - } else if (mouseX >= width - AUTO_SCROLL_SENSITIVITY && dimensions?.hasScrollX) { + } else if (mouseX >= width - AUTO_SCROLL_SENSITIVITY && dimensions.hasScrollX) { // When scrolling right, the multiplier increases going closer to the right edge factor = (mouseX - (width - AUTO_SCROLL_SENSITIVITY)) / AUTO_SCROLL_SENSITIVITY; deltaX = AUTO_SCROLL_SPEED; diff --git a/packages/grid/x-data-grid-premium/src/tests/DataGridPremium.spec.tsx b/packages/grid/x-data-grid-premium/src/tests/DataGridPremium.spec.tsx index f461c95d2f92..e7c53357b2e8 100644 --- a/packages/grid/x-data-grid-premium/src/tests/DataGridPremium.spec.tsx +++ b/packages/grid/x-data-grid-premium/src/tests/DataGridPremium.spec.tsx @@ -88,10 +88,6 @@ function ApiRefPrivateMethods() { apiRef.current.getLastMeasuredRowIndex; // @ts-expect-error Property 'getViewportPageSize' does not exist on type 'GridApiPremium' apiRef.current.getViewportPageSize; - // @ts-expect-error Property 'updateGridDimensionsRef' does not exist on type 'GridApiPremium' - apiRef.current.updateGridDimensionsRef; - // @ts-expect-error Property 'getRenderContext' does not exist on type 'GridApiPremium' - apiRef.current.getRenderContext; // @ts-expect-error Property 'setCellEditingEditCellValue' does not exist on type 'GridApiPremium' apiRef.current.setCellEditingEditCellValue; // @ts-expect-error Property 'getRowWithUpdatedValuesFromCellEditing' does not exist on type 'GridApiPremium' diff --git a/packages/grid/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx b/packages/grid/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx index e7bf76103e2d..854f9d363613 100644 --- a/packages/grid/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx +++ b/packages/grid/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx @@ -835,8 +835,6 @@ describe(' - Row grouping', () => { />, ); - expect(renderIdCell.lastCall.firstArg.id).to.equal(4); - expect(renderIdCell.lastCall.firstArg.field).to.equal('id'); expect(getColumnValues(0)).to.deep.equal([ 'Cat A (3)', 'Custom leaf', @@ -1257,8 +1255,6 @@ describe(' - Row grouping', () => { />, ); - expect(renderIdCell.lastCall.firstArg.id).to.equal(4); - expect(renderIdCell.lastCall.firstArg.field).to.equal('id'); expect(getColumnValues(0)).to.deep.equal([ 'Cat A (3)', '', diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 867083d39dea..5c23ddde3211 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -8,15 +8,12 @@ import { GridRoot, GridContextProvider, GridValidRowModel, - useGridSelector, } from '@mui/x-data-grid'; import { validateProps } from '@mui/x-data-grid/internals'; import { useDataGridProComponent } from './useDataGridProComponent'; import { DataGridProProps } from '../models/dataGridProProps'; import { useDataGridProProps } from './useDataGridProProps'; -import { DataGridProVirtualScroller } from '../components/DataGridProVirtualScroller'; import { getReleaseInfo } from '../utils/releaseInfo'; -import { gridPinnedColumnsSelector } from '../hooks/features/columnPinning/gridColumnPinningSelector'; import { propValidatorsDataGridPro } from '../internals/propValidation'; const releaseInfo = getReleaseInfo(); @@ -29,10 +26,7 @@ const DataGridProRaw = React.forwardRef(function DataGridPro - + @@ -641,7 +632,7 @@ DataGridProRaw.propTypes = { onPaginationModelChange: PropTypes.func, /** * Callback fired when the pinned columns have changed. - * @param {GridPinnedColumns} pinnedColumns The changed pinned columns. + * @param {GridPinnedColumnFields} pinnedColumns The changed pinned columns. * @param {GridCallbackDetails} details Additional details for this callback. */ onPinnedColumnsChange: PropTypes.func, diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx index 23220818d45a..933153b306c1 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx @@ -28,6 +28,7 @@ import { sortingStateInitializer, useGridScroll, useGridEvents, + dimensionsStateInitializer, useGridDimensions, useGridStatePersistence, useGridRowSelectionPreProcessors, @@ -102,6 +103,7 @@ export const useDataGridProComponent = ( /** * Register all state initializers here. */ + useGridInitializeState(dimensionsStateInitializer, apiRef, props); useGridInitializeState(headerFilteringStateInitializer, apiRef, props); useGridInitializeState(rowSelectionStateInitializer, apiRef, props); useGridInitializeState(detailPanelStateInitializer, apiRef, props); diff --git a/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx b/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx deleted file mode 100644 index f764a456743a..000000000000 --- a/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx +++ /dev/null @@ -1,557 +0,0 @@ -import * as React from 'react'; -import { styled, alpha, Theme, useTheme } from '@mui/material/styles'; -import { unstable_composeClasses as composeClasses } from '@mui/utils'; -import { - useGridSelector, - getDataGridUtilityClass, - gridClasses, - gridVisibleColumnFieldsSelector, - gridRowsMetaSelector, - useGridApiEventHandler, - GridRowId, - GridOverlays, -} from '@mui/x-data-grid'; -import { - GridVirtualScroller, - GridVirtualScrollerContent, - GridVirtualScrollerRenderZone, - useGridVirtualScroller, - calculatePinnedRowsHeight, -} from '@mui/x-data-grid/internals'; -import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext'; -import { useGridRootProps } from '../hooks/utils/useGridRootProps'; -import { DataGridProProcessedProps } from '../models/dataGridProProps'; -import { - gridPinnedColumnsSelector, - GridPinnedColumns, - GridPinnedPosition, -} from '../hooks/features/columnPinning'; -import { - gridDetailPanelExpandedRowsContentCacheSelector, - gridDetailPanelExpandedRowsHeightCacheSelector, - gridDetailPanelExpandedRowIdsSelector, -} from '../hooks/features/detailPanel'; -import { GridDetailPanel } from './GridDetailPanel'; -import { gridPinnedRowsSelector } from '../hooks/features/rowPinning/gridRowPinningSelector'; - -export const filterColumns = ( - pinnedColumns: GridPinnedColumns, - columns: string[], - invert?: boolean, -): [string[], string[]] => { - if (!Array.isArray(pinnedColumns.left) && !Array.isArray(pinnedColumns.right)) { - return [[], []]; - } - - if (pinnedColumns.left?.length === 0 && pinnedColumns.right?.length === 0) { - return [[], []]; - } - - const filter = (newPinnedColumns: any[] | undefined, remainingColumns: string[]) => { - if (!Array.isArray(newPinnedColumns)) { - return []; - } - return newPinnedColumns.filter((field) => remainingColumns.includes(field)); - }; - - const leftPinnedColumns = filter(pinnedColumns.left, columns); - const columnsWithoutLeftPinnedColumns = columns.filter( - // Filter out from the remaining columns those columns already pinned to the left - (field) => !leftPinnedColumns.includes(field), - ); - const rightPinnedColumns = filter(pinnedColumns.right, columnsWithoutLeftPinnedColumns); - if (invert) { - return [rightPinnedColumns, leftPinnedColumns]; - } - return [leftPinnedColumns, rightPinnedColumns]; -}; - -type OwnerState = DataGridProProcessedProps; - -const useUtilityClasses = (ownerState: OwnerState) => { - const { classes } = ownerState; - - const slots = { - leftPinnedColumns: ['pinnedColumns', 'pinnedColumns--left'], - rightPinnedColumns: ['pinnedColumns', 'pinnedColumns--right', 'withBorderColor'], - topPinnedRows: ['pinnedRows', 'pinnedRows--top'], - bottomPinnedRows: ['pinnedRows', 'pinnedRows--bottom'], - pinnedRowsRenderZone: ['pinnedRowsRenderZone'], - detailPanels: ['detailPanels'], - detailPanel: ['detailPanel'], - }; - - return composeClasses(slots, getDataGridUtilityClass, classes); -}; - -interface VirtualScrollerPinnedColumnsProps { - side: GridPinnedPosition; - showCellVerticalBorder: boolean; -} - -// Inspired by https://github.com/material-components/material-components-ios/blob/bca36107405594d5b7b16265a5b0ed698f85a5ee/components/Elevation/src/UIColor%2BMaterialElevation.m#L61 -const getOverlayAlpha = (elevation: number) => { - let alphaValue: number; - if (elevation < 1) { - alphaValue = 5.11916 * elevation ** 2; - } else { - alphaValue = 4.5 * Math.log(elevation + 1) + 2; - } - return alphaValue / 100; -}; - -const getBoxShadowColor = (theme: Theme) => { - return theme.vars ? `rgba(0 0 0 / 0.21)` : alpha(theme.palette.common.black, 0.21); -}; - -const VirtualScrollerDetailPanels = styled('div', { - name: 'MuiDataGrid', - slot: 'DetailPanels', - overridesResolver: (props, styles) => styles.detailPanels, -})<{ ownerState: OwnerState }>({ - position: 'relative', -}); - -const darkModeBackgroundImage = `linear-gradient(${alpha('#fff', getOverlayAlpha(2))}, ${alpha( - '#fff', - getOverlayAlpha(2), -)})`; - -const VirtualScrollerPinnedColumns = styled('div', { - name: 'MuiDataGrid', - slot: 'PinnedColumns', - overridesResolver: (props, styles) => [ - { [`&.${gridClasses['pinnedColumns--left']}`]: styles['pinnedColumns--left'] }, - { [`&.${gridClasses['pinnedColumns--right']}`]: styles['pinnedColumns--right'] }, - styles.pinnedColumns, - ], -})<{ ownerState: OwnerState & VirtualScrollerPinnedColumnsProps }>(({ theme, ownerState }) => { - const boxShadowColor = getBoxShadowColor(theme); - return { - position: 'sticky', - overflow: 'hidden', - zIndex: 1, - backgroundColor: (theme.vars || theme).palette.background.default, - ...(theme.vars - ? { backgroundImage: theme.vars.overlays?.[2] } - : { ...(theme.palette.mode === 'dark' && { backgroundImage: darkModeBackgroundImage }) }), - ...(ownerState.side === GridPinnedPosition.left && { - left: 0, - float: 'left', - boxShadow: `2px 0px 4px -2px ${boxShadowColor}`, - }), - ...(ownerState.side === GridPinnedPosition.right && { - right: 0, - float: 'right', - boxShadow: `-2px 0px 4px -2px ${boxShadowColor}`, - }), - ...(ownerState.side === GridPinnedPosition.right && - ownerState.showCellVerticalBorder && { - borderLeftWidth: '1px', - borderLeftStyle: 'solid', - }), - }; -}); - -enum PinnedRowsPosition { - top = 'top', - bottom = 'bottom', -} - -const VirtualScrollerPinnedRows = styled('div', { - name: 'MuiDataGrid', - slot: 'PinnedRows', - overridesResolver: (props, styles) => [ - { [`&.${gridClasses['pinnedRows--top']}`]: styles['pinnedRows--top'] }, - { [`&.${gridClasses['pinnedRows--bottom']}`]: styles['pinnedRows--bottom'] }, - styles.pinnedRows, - ], -})<{ ownerState: OwnerState & { position: PinnedRowsPosition } }>(({ theme, ownerState }) => { - const boxShadowColor = getBoxShadowColor(theme); - return { - position: 'sticky', - // should be above the no rows overlay - zIndex: 4, - backgroundColor: (theme.vars || theme).palette.background.default, - ...(theme.vars - ? { backgroundImage: theme.vars.overlays?.[2] } - : { ...(theme.palette.mode === 'dark' && { backgroundImage: darkModeBackgroundImage }) }), - ...(ownerState.position === 'top' && { - top: 0, - boxShadow: `0px 3px 4px -2px ${boxShadowColor}`, - }), - ...(ownerState.position === PinnedRowsPosition.bottom && { - boxShadow: `0px -3px 4px -2px ${boxShadowColor}`, - bottom: 0, - }), - }; -}); - -const VirtualScrollerPinnedRowsRenderZone = styled('div')({ - position: 'absolute', -}); - -interface DataGridProVirtualScrollerProps extends React.HTMLAttributes { - disableVirtualization?: boolean; -} - -const DataGridProVirtualScroller = React.forwardRef< - HTMLDivElement, - DataGridProVirtualScrollerProps ->(function DataGridProVirtualScroller(props, ref) { - const { className, disableVirtualization, ...other } = props; - const apiRef = useGridPrivateApiContext(); - const rootProps = useGridRootProps(); - const visibleColumnFields = useGridSelector(apiRef, gridVisibleColumnFieldsSelector); - const expandedRowIds = useGridSelector(apiRef, gridDetailPanelExpandedRowIdsSelector); - const detailPanelsContent = useGridSelector( - apiRef, - gridDetailPanelExpandedRowsContentCacheSelector, - ); - const detailPanelsHeights = useGridSelector( - apiRef, - gridDetailPanelExpandedRowsHeightCacheSelector, - ); - const leftColumns = React.useRef(null); - const rightColumns = React.useRef(null); - const topPinnedRowsRenderZoneRef = React.useRef(null); - const bottomPinnedRowsRenderZoneRef = React.useRef(null); - const theme = useTheme(); - - const handleRenderZonePositioning = React.useCallback( - ({ top, left }: { top: number; left: number }) => { - if (leftColumns.current) { - leftColumns.current!.style.transform = `translate3d(0px, ${top}px, 0px)`; - } - if (rightColumns.current) { - rightColumns.current!.style.transform = `translate3d(0px, ${top}px, 0px)`; - } - if (topPinnedRowsRenderZoneRef.current) { - topPinnedRowsRenderZoneRef.current!.style.transform = `translate3d(${left}px, 0px, 0px)`; - } - if (bottomPinnedRowsRenderZoneRef.current) { - bottomPinnedRowsRenderZoneRef.current!.style.transform = `translate3d(${left}px, 0px, 0px)`; - } - }, - [], - ); - - // Create a lookup for faster check if the row is expanded - const expandedRowIdsLookup = React.useMemo(() => { - const lookup: Set = new Set(); - expandedRowIds.forEach((id: GridRowId) => { - lookup.add(id); - }); - return lookup; - }, [expandedRowIds]); - - const getRowProps = React.useCallback( - (id: GridRowId) => { - if (!expandedRowIdsLookup.has(id)) { - return null; - } - const height = detailPanelsHeights[id]; - return { style: { marginBottom: height } }; - }, - [detailPanelsHeights, expandedRowIdsLookup], - ); - - const pinnedColumns = useGridSelector(apiRef, gridPinnedColumnsSelector); - const [leftPinnedColumns, rightPinnedColumns] = filterColumns( - pinnedColumns, - visibleColumnFields, - theme.direction === 'rtl', - ); - - const pinnedRows = useGridSelector(apiRef, gridPinnedRowsSelector); - const topPinnedRowsData = React.useMemo(() => pinnedRows?.top || [], [pinnedRows?.top]); - const bottomPinnedRowsData = React.useMemo(() => pinnedRows?.bottom || [], [pinnedRows?.bottom]); - - const ownerState = { ...rootProps, classes: rootProps.classes }; - const classes = useUtilityClasses(ownerState); - - const { - renderContext, - getRows, - getRootProps, - getContentProps, - getRenderZoneProps, - updateRenderZonePosition, - } = useGridVirtualScroller({ - ref, - renderZoneMinColumnIndex: leftPinnedColumns.length, - renderZoneMaxColumnIndex: visibleColumnFields.length - rightPinnedColumns.length, - onRenderZonePositioning: handleRenderZonePositioning, - getRowProps, - ...props, - }); - - const refreshRenderZonePosition = React.useCallback(() => { - if (renderContext) { - updateRenderZonePosition(renderContext); - } - }, [renderContext, updateRenderZonePosition]); - - useGridApiEventHandler(apiRef, 'columnWidthChange', refreshRenderZonePosition); - useGridApiEventHandler(apiRef, 'columnOrderChange', refreshRenderZonePosition); - useGridApiEventHandler(apiRef, 'rowOrderChange', refreshRenderZonePosition); - - const leftRenderContext = - renderContext && leftPinnedColumns.length > 0 - ? { - ...renderContext, - firstColumnIndex: 0, - lastColumnIndex: leftPinnedColumns.length, - } - : null; - - const rightRenderContext = - renderContext && rightPinnedColumns.length > 0 - ? { - ...renderContext, - firstColumnIndex: visibleColumnFields.length - rightPinnedColumns.length, - lastColumnIndex: visibleColumnFields.length, - } - : null; - - const getDetailPanel = (rowId: GridRowId): React.ReactNode => { - const rowsMeta = gridRowsMetaSelector(apiRef.current.state); - const content = detailPanelsContent[rowId]; - - // Check if the id exists in the current page - const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(rowId); - const exists = rowIndex !== undefined; - - if (React.isValidElement(content) && exists) { - const hasAutoHeight = apiRef.current.detailPanelHasAutoHeight(rowId); - const height = hasAutoHeight ? 'auto' : detailPanelsHeights[rowId]; - - const sizes = apiRef.current.unstable_getRowInternalSizes(rowId); - const spacingTop = sizes?.spacingTop || 0; - const top = - rowsMeta.positions[rowIndex] + apiRef.current.unstable_getRowHeight(rowId) + spacingTop; - - return ( - - {content} - - ); - } - return null; - }; - - const detailPanels: React.ReactNode[] = []; - const topPinnedRows = getRows({ renderContext, rows: topPinnedRowsData, position: 'center' }); - - const pinnedRowsHeight = calculatePinnedRowsHeight(apiRef); - - const mainRows = getRows({ - renderContext, - rowIndexOffset: topPinnedRowsData.length, - position: 'center', - onRowRender: (rowId: GridRowId) => { - if (rootProps.getDetailPanelContent == null) { - return; - } - if (!expandedRowIdsLookup.has(rowId)) { - return; - } - - const detailPanel = getDetailPanel(rowId); - if (detailPanel) { - detailPanels.push(detailPanel); - } - }, - }); - - const bottomPinnedRows = getRows({ - renderContext, - rows: bottomPinnedRowsData, - rowIndexOffset: topPinnedRowsData.length + (mainRows ? mainRows.length : 0), - position: 'center', - }); - - const contentProps = getContentProps(); - - const pinnedColumnsStyle = { minHeight: contentProps.style.minHeight }; - - if (contentProps.style.minHeight && contentProps.style.minHeight === '100%') { - contentProps.style.minHeight = `calc(100% - ${pinnedRowsHeight.top}px - ${pinnedRowsHeight.bottom}px)`; - } - - return ( - - - {topPinnedRowsData.length > 0 ? ( - - {leftRenderContext && ( - - {getRows({ - renderContext: leftRenderContext, - minFirstColumn: leftRenderContext.firstColumnIndex, - maxLastColumn: leftRenderContext.lastColumnIndex, - availableSpace: 0, - rows: topPinnedRowsData, - position: 'left', - })} - - )} - - {topPinnedRows} - - {rightRenderContext && ( - - {getRows({ - renderContext: rightRenderContext, - minFirstColumn: rightRenderContext.firstColumnIndex, - maxLastColumn: rightRenderContext.lastColumnIndex, - availableSpace: 0, - rows: topPinnedRowsData, - position: 'right', - })} - - )} - - ) : null} - - {leftRenderContext && ( - - {getRows({ - renderContext: leftRenderContext, - minFirstColumn: leftRenderContext.firstColumnIndex, - maxLastColumn: leftRenderContext.lastColumnIndex, - availableSpace: 0, - rowIndexOffset: topPinnedRowsData.length, - position: 'left', - })} - - )} - - {mainRows} - - {rightRenderContext && ( - - {getRows({ - renderContext: rightRenderContext, - minFirstColumn: rightRenderContext.firstColumnIndex, - maxLastColumn: rightRenderContext.lastColumnIndex, - availableSpace: 0, - rowIndexOffset: topPinnedRowsData.length, - position: 'right', - })} - - )} - {detailPanels.length > 0 && ( - - {detailPanels} - - )} - - {bottomPinnedRowsData.length > 0 ? ( - - {leftRenderContext && ( - - {getRows({ - renderContext: leftRenderContext, - minFirstColumn: leftRenderContext.firstColumnIndex, - maxLastColumn: leftRenderContext.lastColumnIndex, - availableSpace: 0, - rows: bottomPinnedRowsData, - rowIndexOffset: topPinnedRowsData.length + (mainRows ? mainRows.length : 0), - position: 'left', - })} - - )} - - {bottomPinnedRows} - - {rightRenderContext && ( - - {getRows({ - renderContext: rightRenderContext, - minFirstColumn: rightRenderContext.firstColumnIndex, - maxLastColumn: rightRenderContext.lastColumnIndex, - availableSpace: 0, - rows: bottomPinnedRowsData, - rowIndexOffset: topPinnedRowsData.length + (mainRows ? mainRows.length : 0), - position: 'right', - })} - - )} - - ) : null} - - ); -}); - -export { DataGridProVirtualScroller }; diff --git a/packages/grid/x-data-grid-pro/src/components/GridColumnHeaders.tsx b/packages/grid/x-data-grid-pro/src/components/GridColumnHeaders.tsx index 74506a02dbfb..6a03a203063c 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridColumnHeaders.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridColumnHeaders.tsx @@ -1,33 +1,30 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { - refType, - unstable_composeClasses as composeClasses, - unstable_useEventCallback as useEventCallback, -} from '@mui/utils'; -import { styled, alpha, useTheme } from '@mui/material/styles'; +import { refType, unstable_composeClasses as composeClasses } from '@mui/utils'; +import { styled } from '@mui/material/styles'; import { getDataGridUtilityClass, gridClasses, - useGridApiEventHandler, GridColumnHeaderSeparatorSides, + GridPinnedColumnPosition, + useGridSelector, + gridVisiblePinnedColumnDefinitionsSelector, } from '@mui/x-data-grid'; import { GridBaseColumnHeaders, GridColumnHeadersInner, + GridPinnedColumnFields, UseGridColumnHeadersProps, } from '@mui/x-data-grid/internals'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import { useGridApiContext } from '../hooks/utils/useGridApiContext'; import { DataGridProProcessedProps } from '../models/dataGridProProps'; -import { GridPinnedPosition, GridPinnedColumns } from '../hooks/features/columnPinning'; import { useGridColumnHeaders } from '../hooks/features/columnHeaders/useGridColumnHeaders'; -import { filterColumns } from './DataGridProVirtualScroller'; import { GridScrollArea } from './GridScrollArea'; type OwnerState = DataGridProProcessedProps & { - leftPinnedColumns: GridPinnedColumns['left']; - rightPinnedColumns: GridPinnedColumns['right']; + leftPinnedColumns: GridPinnedColumnFields['left']; + rightPinnedColumns: GridPinnedColumnFields['right']; }; const useUtilityClasses = (ownerState: OwnerState) => { @@ -49,60 +46,46 @@ const useUtilityClasses = (ownerState: OwnerState) => { }; interface GridColumnHeadersPinnedColumnHeadersProps { - side: GridPinnedPosition; + side: GridPinnedColumnPosition; showCellVerticalBorder: boolean; } -// Inspired by https://github.com/material-components/material-components-ios/blob/bca36107405594d5b7b16265a5b0ed698f85a5ee/components/Elevation/src/UIColor%2BMaterialElevation.m#L61 -const getOverlayAlpha = (elevation: number) => { - let alphaValue; - if (elevation < 1) { - alphaValue = 5.11916 * elevation ** 2; - } else { - alphaValue = 4.5 * Math.log(elevation + 1) + 2; - } - return alphaValue / 100; -}; - const GridColumnHeadersPinnedColumnHeaders = styled('div', { name: 'MuiDataGrid', slot: 'PinnedColumnHeaders', - overridesResolver: (props, styles) => [ + overridesResolver: (_, styles) => [ { [`&.${gridClasses['pinnedColumnHeaders--left']}`]: styles['pinnedColumnHeaders--left'] }, { [`&.${gridClasses['pinnedColumnHeaders--right']}`]: styles['pinnedColumnHeaders--right'] }, styles.pinnedColumnHeaders, ], -})<{ ownerState: OwnerState & GridColumnHeadersPinnedColumnHeadersProps }>( - ({ theme, ownerState }) => ({ - position: 'absolute', - top: 0, - overflow: 'hidden', - zIndex: 1, - display: 'flex', - flexDirection: 'column', - boxShadow: theme.shadows[2], - backgroundColor: (theme.vars || theme).palette.background.default, - ...(theme.vars - ? { - backgroundImage: theme.vars.overlays?.[2], - } - : { - ...(theme.palette.mode === 'dark' && { - backgroundImage: `linear-gradient(${alpha('#fff', getOverlayAlpha(2))}, ${alpha( - '#fff', - getOverlayAlpha(2), - )})`, - }), - }), - ...(ownerState.side === GridPinnedPosition.left && { left: 0 }), - ...(ownerState.side === GridPinnedPosition.right && { right: 0 }), - ...(ownerState.side === GridPinnedPosition.right && - ownerState.showCellVerticalBorder && { - borderLeftWidth: '1px', - borderLeftStyle: 'solid', - }), - }), -); +})<{ ownerState: OwnerState & GridColumnHeadersPinnedColumnHeadersProps }>(({ ownerState }) => ({ + position: 'sticky', + zIndex: 5, + top: 0, + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + backgroundColor: 'var(--DataGrid-pinnedBackground)', + ...(ownerState.side === GridPinnedColumnPosition.LEFT && { left: 0 }), + ...(ownerState.side === GridPinnedColumnPosition.RIGHT && { right: 0 }), + [`&.${gridClasses['pinnedColumnHeaders--left']}`]: { + left: 0, + '& > [role="row"] > [role="columnheader"]:last-of-type': { + borderRight: '1px solid var(--DataGrid-rowBorderColor)', + }, + }, + [`&.${gridClasses['pinnedColumnHeaders--right']}`]: { + right: 0, + '& > [role="row"] > [role="columnheader"]:first-of-type': { + borderLeft: '1px solid var(--DataGrid-rowBorderColor)', + }, + }, +})); + +const Filler = styled('div')({ + flex: 1, + backgroundColor: 'var(--DataGrid-containerBackground)', +}); GridColumnHeadersPinnedColumnHeaders.propTypes = { // ----------------------------- Warning -------------------------------- @@ -116,7 +99,6 @@ interface DataGridProColumnHeadersProps extends React.HTMLAttributes, Omit { innerRef?: React.Ref; - pinnedColumns: GridPinnedColumns; } const GridColumnHeaders = React.forwardRef( @@ -128,54 +110,28 @@ const GridColumnHeaders = React.forwardRef { - const rootDimensions = apiRef.current.getRootDimensions(); - if (!rootDimensions) { - return; - } - - const newScrollbarSize = rootDimensions.hasScrollY ? rootDimensions.scrollBarSize : 0; - if (scrollbarSize !== newScrollbarSize) { - setScrollbarSize(newScrollbarSize); - } - }); - - useGridApiEventHandler(apiRef, 'virtualScrollerContentSizeChange', handleContentSizeChange); - - const visibleColumnFields = React.useMemo( - () => visibleColumns.map(({ field }) => field), - [visibleColumns], - ); + const rootProps = useGridRootProps(); - const [leftPinnedColumns, rightPinnedColumns] = filterColumns( - pinnedColumns, - visibleColumnFields, - theme.direction === 'rtl', + const visiblePinnedColumns = useGridSelector( + apiRef, + gridVisiblePinnedColumnDefinitionsSelector, ); const { isDragging, renderContext, - getRootProps, getInnerProps, getColumnHeaders, getColumnFilters, @@ -185,43 +141,40 @@ const GridColumnHeaders = React.forwardRef c.field), + rightPinnedColumns: visiblePinnedColumns.right.map((c) => c.field), classes: rootProps.classes, }; const classes = useUtilityClasses(ownerState); const leftRenderContext = - renderContext && leftPinnedColumns.length + renderContext && visiblePinnedColumns.left.length ? { ...renderContext, firstColumnIndex: 0, - lastColumnIndex: leftPinnedColumns.length, + lastColumnIndex: visiblePinnedColumns.left.length, } : null; const rightRenderContext = - renderContext && rightPinnedColumns.length + renderContext && visiblePinnedColumns.right.length ? { ...renderContext, - firstColumnIndex: visibleColumnFields.length - rightPinnedColumns.length, - lastColumnIndex: visibleColumnFields.length, + firstColumnIndex: visibleColumns.length - visiblePinnedColumns.right.length, + lastColumnIndex: visibleColumns.length, } : null; @@ -232,32 +185,34 @@ const GridColumnHeaders = React.forwardRef + {leftRenderContext && ( {getColumnGroupHeaders({ + position: GridPinnedColumnPosition.LEFT, renderContext: leftRenderContext, minFirstColumn: leftRenderContext.firstColumnIndex, maxLastColumn: leftRenderContext.lastColumnIndex, })} {getColumnHeaders( { + position: GridPinnedColumnPosition.LEFT, renderContext: leftRenderContext, minFirstColumn: leftRenderContext.firstColumnIndex, maxLastColumn: leftRenderContext.lastColumnIndex, }, { disableReorder: true }, )} - {getColumnFilters({ + position: GridPinnedColumnPosition.LEFT, renderContext: leftRenderContext, minFirstColumn: leftRenderContext.firstColumnIndex, maxLastColumn: leftRenderContext.lastColumnIndex, @@ -269,47 +224,49 @@ const GridColumnHeaders = React.forwardRef {getColumnGroupHeaders({ renderContext, - minFirstColumn: leftPinnedColumns.length, - maxLastColumn: visibleColumnFields.length - rightPinnedColumns.length, + minFirstColumn: visiblePinnedColumns.left.length, + maxLastColumn: visibleColumns.length - visiblePinnedColumns.right.length, })} {getColumnHeaders({ renderContext, - minFirstColumn: leftPinnedColumns.length, - maxLastColumn: visibleColumnFields.length - rightPinnedColumns.length, + minFirstColumn: visiblePinnedColumns.left.length, + maxLastColumn: visibleColumns.length - visiblePinnedColumns.right.length, })} {getColumnFilters({ renderContext, - minFirstColumn: leftPinnedColumns.length, - maxLastColumn: visibleColumnFields.length - rightPinnedColumns.length, + minFirstColumn: visiblePinnedColumns.left.length, + maxLastColumn: visibleColumns.length - visiblePinnedColumns.right.length, })} + {rightRenderContext && ( {getColumnGroupHeaders({ + position: GridPinnedColumnPosition.RIGHT, renderContext: rightRenderContext, minFirstColumn: rightRenderContext.firstColumnIndex, maxLastColumn: rightRenderContext.lastColumnIndex, })} {getColumnHeaders( { + position: GridPinnedColumnPosition.RIGHT, renderContext: rightRenderContext, minFirstColumn: rightRenderContext.firstColumnIndex, maxLastColumn: rightRenderContext.lastColumnIndex, }, { disableReorder: true, separatorSide: GridColumnHeaderSeparatorSides.Left }, )} - {getColumnFilters({ + position: GridPinnedColumnPosition.RIGHT, renderContext: rightRenderContext, minFirstColumn: rightRenderContext.firstColumnIndex, maxLastColumn: rightRenderContext.lastColumnIndex, @@ -352,18 +309,11 @@ GridColumnHeaders.propTypes = { field: PropTypes.string, open: PropTypes.bool.isRequired, }).isRequired, - columnPositions: PropTypes.arrayOf(PropTypes.number).isRequired, columnVisibility: PropTypes.object.isRequired, - densityFactor: PropTypes.number.isRequired, filterColumnLookup: PropTypes.object.isRequired, hasOtherElementInTabSequence: PropTypes.bool.isRequired, headerGroupingMaxDepth: PropTypes.number.isRequired, innerRef: refType, - minColumnIndex: PropTypes.number, - pinnedColumns: PropTypes.shape({ - left: PropTypes.arrayOf(PropTypes.string), - right: PropTypes.arrayOf(PropTypes.string), - }).isRequired, sortColumnLookup: PropTypes.object.isRequired, visibleColumns: PropTypes.arrayOf(PropTypes.object).isRequired, } as any; diff --git a/packages/grid/x-data-grid-pro/src/components/GridColumnMenuPinningItem.tsx b/packages/grid/x-data-grid-pro/src/components/GridColumnMenuPinningItem.tsx index 87eb116af23a..8af8bb84ceb1 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridColumnMenuPinningItem.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridColumnMenuPinningItem.tsx @@ -4,8 +4,7 @@ import PropTypes from 'prop-types'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; -import { GridColumnMenuItemProps } from '@mui/x-data-grid'; -import { GridPinnedPosition } from '../hooks/features/columnPinning'; +import { GridPinnedColumnPosition, GridColumnMenuItemProps } from '@mui/x-data-grid'; import { useGridApiContext } from '../hooks/utils/useGridApiContext'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; @@ -16,7 +15,7 @@ function GridColumnMenuPinningItem(props: GridColumnMenuItemProps) { const theme = useTheme(); const pinColumn = React.useCallback( - (side: GridPinnedPosition) => (event: React.MouseEvent) => { + (side: GridPinnedColumnPosition) => (event: React.MouseEvent) => { apiRef.current.pinColumn(colDef.field, side); onClick(event); }, @@ -28,7 +27,7 @@ function GridColumnMenuPinningItem(props: GridColumnMenuItemProps) { onClick(event); }; const pinToLeftMenuItem = ( - + @@ -37,7 +36,7 @@ function GridColumnMenuPinningItem(props: GridColumnMenuItemProps) { ); const pinToRightMenuItem = ( - + @@ -53,10 +52,12 @@ function GridColumnMenuPinningItem(props: GridColumnMenuItemProps) { if (side) { const otherSide = - side === GridPinnedPosition.right ? GridPinnedPosition.left : GridPinnedPosition.right; - const label = otherSide === GridPinnedPosition.right ? 'pinToRight' : 'pinToLeft'; + side === GridPinnedColumnPosition.RIGHT + ? GridPinnedColumnPosition.LEFT + : GridPinnedColumnPosition.RIGHT; + const label = otherSide === GridPinnedColumnPosition.RIGHT ? 'pinToRight' : 'pinToLeft'; const Icon = - side === GridPinnedPosition.right + side === GridPinnedColumnPosition.RIGHT ? rootProps.slots.columnMenuPinLeftIcon : rootProps.slots.columnMenuPinRightIcon; return ( diff --git a/packages/grid/x-data-grid-pro/src/components/GridDetailPanel.tsx b/packages/grid/x-data-grid-pro/src/components/GridDetailPanel.tsx index 49619426e06e..82577308cb75 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridDetailPanel.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridDetailPanel.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { styled, SxProps, Theme } from '@mui/material/styles'; +import { styled } from '@mui/material/styles'; import { GridRowId } from '@mui/x-data-grid'; +import { useResizeObserver } from '@mui/x-data-grid/internals'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext'; import { DataGridProProcessedProps } from '../models/dataGridProProps'; @@ -12,49 +13,42 @@ const DetailPanel = styled('div', { slot: 'DetailPanel', overridesResolver: (props, styles) => styles.detailPanel, })<{ ownerState: OwnerState }>(({ theme }) => ({ - zIndex: 2, - width: '100%', - position: 'absolute', + width: + 'calc(var(--DataGrid-rowWidth) - var(--DataGrid-hasScrollY) * var(--DataGrid-scrollbarSize))', backgroundColor: (theme.vars || theme).palette.background.default, overflow: 'auto', })); -interface GridDetailPanelProps extends React.HTMLAttributes { +interface GridDetailPanelProps + extends Pick, 'className' | 'children'> { /** - * The system prop that allows defining system overrides as well as additional CSS styles. + * The row ID that this panel belongs to. */ - sx?: SxProps; + rowId: GridRowId; /** * The panel height. */ height: number | 'auto'; - /** - * The row ID that this panel belongs to. - */ - rowId: GridRowId; } function GridDetailPanel(props: GridDetailPanelProps) { - const { rowId, height, style: styleProp = {}, ...other } = props; + const { rowId, height, className, children } = props; const apiRef = useGridPrivateApiContext(); const ref = React.useRef(null); const rootProps = useGridRootProps(); const ownerState = rootProps; + const hasAutoHeight = height === 'auto'; React.useLayoutEffect(() => { - if (height === 'auto' && typeof ResizeObserver === 'undefined') { + if (hasAutoHeight && typeof ResizeObserver === 'undefined') { // Fallback for IE apiRef.current.storeDetailPanelHeight(rowId, ref.current!.clientHeight); } - }, [apiRef, height, rowId]); + }, [apiRef, hasAutoHeight, rowId]); - React.useLayoutEffect(() => { - const hasFixedHeight = height !== 'auto'; - if (hasFixedHeight || typeof ResizeObserver === 'undefined') { - return undefined; - } - - const resizeObserver = new ResizeObserver((entries) => { + useResizeObserver( + ref, + (entries) => { const [entry] = entries; const observedHeight = entry.borderBoxSize && entry.borderBoxSize.length > 0 @@ -62,16 +56,21 @@ function GridDetailPanel(props: GridDetailPanelProps) { : entry.contentRect.height; apiRef.current.storeDetailPanelHeight(rowId, observedHeight); - }); - - resizeObserver.observe(ref.current!); - - return () => resizeObserver.disconnect(); - }, [apiRef, height, rowId]); - - const style = { ...styleProp, height }; + }, + hasAutoHeight, + ); - return ; + return ( + + {children} + + ); } export { GridDetailPanel }; diff --git a/packages/grid/x-data-grid-pro/src/components/GridDetailPanels.tsx b/packages/grid/x-data-grid-pro/src/components/GridDetailPanels.tsx new file mode 100644 index 000000000000..64f3cbed737e --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/components/GridDetailPanels.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { unstable_composeClasses as composeClasses } from '@mui/utils'; +import { getDataGridUtilityClass, useGridSelector, GridRowId } from '@mui/x-data-grid'; +import { GridDetailPanelsProps, EMPTY_DETAIL_PANELS } from '@mui/x-data-grid/internals'; +import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext'; +import { useGridRootProps } from '../hooks/utils/useGridRootProps'; +import { + gridDetailPanelExpandedRowsContentCacheSelector, + gridDetailPanelExpandedRowsHeightCacheSelector, + gridDetailPanelExpandedRowIdsSelector, +} from '../hooks/features/detailPanel'; +import { GridDetailPanel } from './GridDetailPanel'; + +const useUtilityClasses = () => { + const slots = { + detailPanel: ['detailPanel'], + }; + return composeClasses(slots, getDataGridUtilityClass, {}); +}; + +export function GridDetailPanels(props: GridDetailPanelsProps) { + const rootProps = useGridRootProps(); + if (!rootProps.getDetailPanelContent) { + return null; + } + return React.createElement(GridDetailPanelsImpl, props); +} + +function GridDetailPanelsImpl({ virtualScroller }: GridDetailPanelsProps) { + const apiRef = useGridPrivateApiContext(); + const classes = useUtilityClasses(); + const { setPanels } = virtualScroller; + + const expandedRowIds = useGridSelector(apiRef, gridDetailPanelExpandedRowIdsSelector); + const detailPanelsContent = useGridSelector( + apiRef, + gridDetailPanelExpandedRowsContentCacheSelector, + ); + const detailPanelsHeights = useGridSelector( + apiRef, + gridDetailPanelExpandedRowsHeightCacheSelector, + ); + + const getDetailPanel = React.useCallback( + (rowId: GridRowId): React.ReactNode => { + const content = detailPanelsContent[rowId]; + + // Check if the id exists in the current page + const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(rowId); + const exists = rowIndex !== undefined; + + if (!React.isValidElement(content) || !exists) { + return null; + } + + const hasAutoHeight = apiRef.current.detailPanelHasAutoHeight(rowId); + const height = hasAutoHeight ? 'auto' : detailPanelsHeights[rowId]; + + return ( + + {content} + + ); + }, + [apiRef, classes.detailPanel, detailPanelsHeights, detailPanelsContent], + ); + + React.useEffect(() => { + if (expandedRowIds.length === 0) { + setPanels(EMPTY_DETAIL_PANELS); + } else { + setPanels( + new Map( + expandedRowIds.map((rowId) => [rowId, getDetailPanel(rowId)]), + ), + ); + } + }, [expandedRowIds, setPanels, getDetailPanel]); + + return null; +} diff --git a/packages/grid/x-data-grid-pro/src/components/GridPinnedRows.tsx b/packages/grid/x-data-grid-pro/src/components/GridPinnedRows.tsx new file mode 100644 index 000000000000..0aaefee5ea85 --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/components/GridPinnedRows.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { unstable_composeClasses as composeClasses } from '@mui/utils'; +import { getDataGridUtilityClass, gridClasses, useGridSelector } from '@mui/x-data-grid'; +import { + GridPinnedRowsProps, + gridPinnedRowsSelector, + useGridPrivateApiContext, +} from '@mui/x-data-grid/internals'; + +const useUtilityClasses = () => { + const slots = { + root: ['pinnedRows'], + }; + return composeClasses(slots, getDataGridUtilityClass, {}); +}; + +export function GridPinnedRows({ position, virtualScroller, ...other }: GridPinnedRowsProps) { + const classes = useUtilityClasses(); + const apiRef = useGridPrivateApiContext(); + + const pinnedRowsData = useGridSelector(apiRef, gridPinnedRowsSelector); + const pinnedRows = virtualScroller.getRows({ + position, + rows: pinnedRowsData[position], + }); + + return ( +
+ {pinnedRows} +
+ ); +} diff --git a/packages/grid/x-data-grid-pro/src/components/GridScrollArea.tsx b/packages/grid/x-data-grid-pro/src/components/GridScrollArea.tsx index c9510b7cf6f1..a8aec7987f26 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridScrollArea.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridScrollArea.tsx @@ -6,7 +6,7 @@ import { unstable_useEventCallback as useEventCallback, } from '@mui/utils'; import { styled } from '@mui/system'; -import { getTotalHeaderHeight, useTimeout } from '@mui/x-data-grid/internals'; +import { getTotalHeaderHeight, fastMemo, useTimeout } from '@mui/x-data-grid/internals'; import { GridEventListener, GridScrollParams, @@ -67,8 +67,6 @@ function GridScrollAreaRaw(props: ScrollAreaProps) { const rootRef = React.useRef(null); const apiRef = useGridApiContext(); const timeout = useTimeout(); - const [dragging, setDragging] = React.useState(false); - const [canScrollMore, setCanScrollMore] = React.useState(true); const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector); const columnsTotalWidth = useGridSelector(apiRef, gridColumnsTotalWidthSelector); @@ -77,63 +75,62 @@ function GridScrollAreaRaw(props: ScrollAreaProps) { top: 0, }); + const getCanScrollMore = () => { + if (scrollDirection === 'left') { + // Only render if the user has not reached yet the start of the list + return scrollPosition.current.left > 0; + } + + if (scrollDirection === 'right') { + const dimensions = apiRef.current.getRootDimensions(); + + // Only render if the user has not reached yet the end of the list + const maxScrollLeft = columnsTotalWidth - dimensions.viewportInnerSize.width; + return scrollPosition.current.left < maxScrollLeft; + } + + return false; + }; + + const [dragging, setDragging] = React.useState(false); + const [canScrollMore, setCanScrollMore] = React.useState(getCanScrollMore); + const rootProps = useGridRootProps(); const ownerState = { ...rootProps, scrollDirection }; const classes = useUtilityClasses(ownerState); const totalHeaderHeight = getTotalHeaderHeight(apiRef, rootProps.columnHeaderHeight); const headerHeight = Math.floor(rootProps.columnHeaderHeight * densityFactor); - const handleScrolling = React.useCallback>( - (newScrollPosition) => { - scrollPosition.current = newScrollPosition; + const handleScrolling: GridEventListener<'scrollPositionChange'> = (newScrollPosition) => { + scrollPosition.current = newScrollPosition; - const dimensions = apiRef.current.getRootDimensions(); + setCanScrollMore(getCanScrollMore); + }; - setCanScrollMore(() => { - if (scrollDirection === 'left') { - // Only render if the user has not reached yet the start of the list - return scrollPosition.current.left > 0; - } + const handleDragOver = useEventCallback((event: React.DragEvent) => { + let offset: number; - if (scrollDirection === 'right') { - // Only render if the user has not reached yet the end of the list - const maxScrollLeft = columnsTotalWidth - dimensions!.viewportInnerSize.width; - return scrollPosition.current.left < maxScrollLeft; - } + // Prevents showing the forbidden cursor + event.preventDefault(); - return false; - }); - }, - [apiRef, columnsTotalWidth, scrollDirection], - ); + if (scrollDirection === 'left') { + offset = event.clientX - rootRef.current!.getBoundingClientRect().right; + } else if (scrollDirection === 'right') { + offset = Math.max(1, event.clientX - rootRef.current!.getBoundingClientRect().left); + } else { + throw new Error('MUI X: Wrong drag direction'); + } + + offset = (offset - CLIFF) * SLOP + CLIFF; - const handleDragOver = React.useCallback( - (event: React.DragEvent) => { - let offset: number; - - // Prevents showing the forbidden cursor - event.preventDefault(); - - if (scrollDirection === 'left') { - offset = event.clientX - rootRef.current!.getBoundingClientRect().right; - } else if (scrollDirection === 'right') { - offset = Math.max(1, event.clientX - rootRef.current!.getBoundingClientRect().left); - } else { - throw new Error('MUI X: Wrong drag direction.'); - } - - offset = (offset - CLIFF) * SLOP + CLIFF; - - // Avoid freeze and inertia. - timeout.start(0, () => { - apiRef.current.scroll({ - left: scrollPosition.current.left + offset, - top: scrollPosition.current.top, - }); + // Avoid freeze and inertia. + timeout.start(0, () => { + apiRef.current.scroll({ + left: scrollPosition.current.left + offset, + top: scrollPosition.current.top, }); - }, - [scrollDirection, apiRef, timeout], - ); + }); + }); const handleColumnHeaderDragStart = useEventCallback(() => { setDragging(true); @@ -170,6 +167,6 @@ GridScrollAreaRaw.propTypes = { scrollDirection: PropTypes.oneOf(['left', 'right']).isRequired, } as any; -const GridScrollArea = React.memo(GridScrollAreaRaw); +const GridScrollArea = fastMemo(GridScrollAreaRaw); export { GridScrollArea }; diff --git a/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx b/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx index f38e9486778d..e2d8d8de81c8 100644 --- a/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx +++ b/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx @@ -18,6 +18,7 @@ import { gridFilterableColumnLookupSelector, } from '@mui/x-data-grid'; import { + fastMemo, GridStateColDef, useGridPrivateApiContext, gridHeaderFilteringEditFieldSelector, @@ -35,7 +36,6 @@ export interface GridHeaderFilterCellProps extends Pick; @@ -73,7 +73,6 @@ const GridHeaderFilterCell = React.forwardRef(null); const buttonRef = React.useRef(null); - const isEditing = gridHeaderFilteringEditFieldSelector(apiRef) === colDef.field; - const isMenuOpen = gridHeaderFilteringMenuSelector(apiRef) === colDef.field; + const editingField = useGridSelector(apiRef, gridHeaderFilteringEditFieldSelector); + const isEditing = editingField === colDef.field; + const menuOpenField = useGridSelector(apiRef, gridHeaderFilteringMenuSelector); + const isMenuOpen = menuOpenField === colDef.field; + + // TODO: Support for `isAnyOf` operator + const filterOperators = + colDef.filterOperators?.filter((operator) => operator.value !== 'isAnyOf') ?? []; const filterModel = useGridSelector(apiRef, gridFilterModelSelector); const filterableColumnsLookup = useGridSelector(apiRef, gridFilterableColumnLookupSelector); @@ -338,18 +343,6 @@ GridHeaderFilterCell.propTypes = { // ---------------------------------------------------------------------- colDef: PropTypes.object.isRequired, colIndex: PropTypes.number.isRequired, - filterOperators: PropTypes.arrayOf( - PropTypes.shape({ - getApplyFilterFn: PropTypes.func.isRequired, - getValueAsString: PropTypes.func, - headerLabel: PropTypes.string, - InputComponent: PropTypes.elementType, - InputComponentProps: PropTypes.object, - label: PropTypes.string, - requiresFilterValue: PropTypes.bool, - value: PropTypes.string.isRequired, - }), - ), hasFocus: PropTypes.bool, /** * Class name that will be added in the column header cell. @@ -372,4 +365,6 @@ GridHeaderFilterCell.propTypes = { width: PropTypes.number.isRequired, } as any; -export { GridHeaderFilterCell }; +const Memoized = fastMemo(GridHeaderFilterCell); + +export { Memoized as GridHeaderFilterCell }; diff --git a/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterMenuContainer.tsx b/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterMenuContainer.tsx index 4742c2280f21..4e89cdfd4d6c 100644 --- a/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterMenuContainer.tsx +++ b/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterMenuContainer.tsx @@ -3,8 +3,9 @@ import PropTypes from 'prop-types'; import { GridFilterItem, GridFilterOperator, - useGridApiContext, GridColDef, + useGridApiContext, + useGridSelector, } from '@mui/x-data-grid'; import { refType, unstable_useId as useId } from '@mui/utils'; import { gridHeaderFilteringMenuSelector } from '@mui/x-data-grid/internals'; @@ -40,9 +41,8 @@ function GridHeaderFilterMenuContainer(props: { const rootProps = useGridRootProps(); const apiRef = useGridApiContext(); - const open = Boolean( - gridHeaderFilteringMenuSelector(apiRef) === field && headerFilterMenuRef.current, - ); + const menuOpenField = useGridSelector(apiRef, gridHeaderFilteringMenuSelector); + const open = Boolean(menuOpenField === field && headerFilterMenuRef.current); const handleClick = (event: React.MouseEvent) => { headerFilterMenuRef.current = event.currentTarget; diff --git a/packages/grid/x-data-grid-pro/src/constants/dataGridProDefaultSlotsComponents.ts b/packages/grid/x-data-grid-pro/src/constants/dataGridProDefaultSlotsComponents.ts index bcca11e477a6..4888e8877347 100644 --- a/packages/grid/x-data-grid-pro/src/constants/dataGridProDefaultSlotsComponents.ts +++ b/packages/grid/x-data-grid-pro/src/constants/dataGridProDefaultSlotsComponents.ts @@ -4,6 +4,8 @@ import { GridProColumnMenu } from '../components/GridProColumnMenu'; import { GridColumnHeaders } from '../components/GridColumnHeaders'; import { GridHeaderFilterMenu } from '../components/headerFiltering/GridHeaderFilterMenu'; import { GridHeaderFilterCell } from '../components/headerFiltering/GridHeaderFilterCell'; +import { GridDetailPanels } from '../components/GridDetailPanels'; +import { GridPinnedRows } from '../components/GridPinnedRows'; import materialSlots from '../material'; export const DATA_GRID_PRO_DEFAULT_SLOTS_COMPONENTS: GridProSlotsComponent = { @@ -11,6 +13,8 @@ export const DATA_GRID_PRO_DEFAULT_SLOTS_COMPONENTS: GridProSlotsComponent = { ...materialSlots, columnMenu: GridProColumnMenu, columnHeaders: GridColumnHeaders, + detailPanels: GridDetailPanels, headerFilterCell: GridHeaderFilterCell, headerFilterMenu: GridHeaderFilterMenu, + pinnedRows: GridPinnedRows, }; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx index 8e85d7c80d5f..06435ab269a4 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx @@ -7,15 +7,14 @@ import { getDataGridUtilityClass, GridFilterItem, } from '@mui/x-data-grid'; -import { styled } from '@mui/system'; import { useGridColumnHeaders as useGridColumnHeadersCommunity, UseGridColumnHeadersProps, GetHeadersParams, - getTotalHeaderHeight, useGridPrivateApiContext, getGridFilter, GridStateColDef, + GridColumnHeaderRow, } from '@mui/x-data-grid/internals'; import { unstable_composeClasses as composeClasses } from '@mui/utils'; import { useGridRootProps } from '../../utils/useGridRootProps'; @@ -34,14 +33,6 @@ const useUtilityClasses = (ownerState: OwnerState) => { }, [classes]); }; -const GridHeaderFilterRow = styled('div', { - name: 'MuiDataGrid', - slot: 'HeaderFilterRow', - overridesResolver: (props, styles) => styles.headerFilterRow, -})<{ ownerState: OwnerState }>(() => ({ - display: 'flex', -})); - const filterItemsCache: Record = Object.create(null); export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { @@ -51,7 +42,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { apiRef, gridTabIndexColumnHeaderFilterSelector, ); - const { getColumnsToRender, getRootProps, ...otherProps } = useGridColumnHeadersCommunity({ + const { getColumnsToRender, ...otherProps } = useGridColumnHeadersCommunity({ ...props, hasOtherElementInTabSequence: hasOtherElementInTabSequence || columnHeaderFilterTabIndexState !== null, @@ -64,11 +55,8 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { const rootProps = useGridRootProps(); const classes = useUtilityClasses(rootProps); const disableHeaderFiltering = !rootProps.headerFilters; - const headerHeight = Math.floor(rootProps.columnHeaderHeight * props.densityFactor); + const dimensions = apiRef.current.getRootDimensions(); const filterModel = useGridSelector(apiRef, gridFilterModelSelector); - const totalHeaderHeight = - getTotalHeaderHeight(apiRef, rootProps.columnHeaderHeight) + - (disableHeaderFiltering ? 0 : headerHeight); const columnHeaderFilterFocus = useGridSelector(apiRef, gridFocusColumnHeaderFilterSelector); @@ -99,13 +87,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { return null; } - const columnsToRender = getColumnsToRender(params); - - if (columnsToRender == null) { - return null; - } - - const { renderedColumns, firstColumnToRender } = columnsToRender; + const { renderedColumns, firstColumnToRender } = getColumnsToRender(params); const filters: React.JSX.Element[] = []; for (let i = 0; i < renderedColumns.length; i += 1) { @@ -124,24 +106,19 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { ? colDef.headerClassName({ field: colDef.field, colDef }) : colDef.headerClassName; - // TODO: Support for `isAnyOf` operator - const filterOperators = - colDef.filterOperators?.filter((operator) => operator.value !== 'isAnyOf') ?? []; - const item = getFilterItem(colDef); filters.push( { } return ( - {filters} - + {otherProps.getFiller(params, true)} + ); }; - const rootStyle = { - minHeight: totalHeaderHeight, - maxHeight: totalHeaderHeight, - lineHeight: `${headerHeight}px`, - }; - return { ...otherProps, getColumnFilters, - getRootProps: disableHeaderFiltering - ? getRootProps - : (other = {}) => ({ style: rootStyle, ...other }), }; }; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/gridColumnPinningInterface.ts b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/gridColumnPinningInterface.ts index a3c4e81c925a..f2b79d38359c 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/gridColumnPinningInterface.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/gridColumnPinningInterface.ts @@ -1,14 +1,5 @@ -export interface GridPinnedColumns { - left?: string[]; - right?: string[]; -} - -export type GridColumnPinningState = GridPinnedColumns; - -enum GridPinnedPosition { - left = 'left', - right = 'right', -} +import { GridPinnedColumnPosition } from '@mui/x-data-grid'; +import { GridPinnedColumnFields } from '@mui/x-data-grid/internals'; /** * The column pinning API interface that is available in the grid [[apiRef]]. @@ -17,9 +8,9 @@ export interface GridColumnPinningApi { /** * Pins a column to the left or right side of the grid. * @param {string} field The column field to pin. - * @param {GridPinnedPosition} side Which side to pin the column. + * @param {GridPinnedColumnPosition} side Which side to pin the column. */ - pinColumn: (field: string, side: GridPinnedPosition) => void; + pinColumn: (field: string, side: GridPinnedColumnPosition) => void; /** * Unpins a column. * @param {string} field The column field to unpin. @@ -27,20 +18,20 @@ export interface GridColumnPinningApi { unpinColumn: (field: string) => void; /** * Returns which columns are pinned. - * @returns {GridPinnedColumns} An object containing the pinned columns. + * @returns {GridPinnedColumnFields} An object containing the pinned columns. */ - getPinnedColumns: () => GridPinnedColumns; + getPinnedColumns: () => GridPinnedColumnFields; /** * Changes the pinned columns. - * @param {GridPinnedColumns} pinnedColumns An object containing the columns to pin. + * @param {GridPinnedColumnFields} pinnedColumns An object containing the columns to pin. */ - setPinnedColumns: (pinnedColumns: GridPinnedColumns) => void; + setPinnedColumns: (pinnedColumns: GridPinnedColumnFields) => void; /** * Returns which side a column is pinned to. * @param {string} field The column field to check. * @returns {string | false} Which side the column is pinned or `false` if not pinned. */ - isColumnPinned: (field: string) => GridPinnedPosition | false; + isColumnPinned: (field: string) => GridPinnedColumnPosition | false; } export interface GridColumnPinningInternalCache { @@ -49,5 +40,3 @@ export interface GridColumnPinningInternalCache { */ orderedFieldsBeforePinningColumns: string[] | null; } - -export { GridPinnedPosition }; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/gridColumnPinningSelector.ts b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/gridColumnPinningSelector.ts deleted file mode 100644 index 3251190b2f83..000000000000 --- a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/gridColumnPinningSelector.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { GridStatePro } from '../../../models/gridStatePro'; - -export const gridPinnedColumnsSelector = (state: GridStatePro) => state.pinnedColumns; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/index.ts b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/index.ts index 6fdd99dd829f..4c2de27b834b 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/index.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/index.ts @@ -1,2 +1 @@ -export * from './gridColumnPinningSelector'; export * from './gridColumnPinningInterface'; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinning.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinning.tsx index 6bdc7646b06e..3dde2718c002 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinning.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinning.tsx @@ -1,32 +1,28 @@ import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; import { useGridSelector, gridVisibleColumnDefinitionsSelector, gridColumnsTotalWidthSelector, gridColumnPositionsSelector, - gridVisibleColumnFieldsSelector, useGridApiMethod, useGridApiEventHandler, GridEventListener, + GridPinnedColumnPosition, gridColumnFieldsSelector, } from '@mui/x-data-grid'; import { useGridRegisterPipeProcessor, + gridPinnedColumnsSelector, + gridVisiblePinnedColumnDefinitionsSelector, GridPipeProcessor, GridRestoreStatePreProcessingContext, GridStateInitializer, + GridPinnedColumnFields, } from '@mui/x-data-grid/internals'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; -import { GridInitialStatePro, GridStatePro } from '../../../models/gridStatePro'; +import { GridInitialStatePro } from '../../../models/gridStatePro'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; -import { - GridColumnPinningApi, - GridPinnedPosition, - GridPinnedColumns, -} from './gridColumnPinningInterface'; -import { gridPinnedColumnsSelector } from './gridColumnPinningSelector'; -import { filterColumns } from '../../../components/DataGridProVirtualScroller'; +import { GridColumnPinningApi } from './gridColumnPinningInterface'; export const columnPinningStateInitializer: GridStateInitializer< Pick @@ -35,13 +31,13 @@ export const columnPinningStateInitializer: GridStateInitializer< orderedFieldsBeforePinningColumns: null, }; - let model: GridPinnedColumns; + let model: GridPinnedColumnFields; if (props.disableColumnPinning) { model = {}; } else if (props.pinnedColumns) { model = props.pinnedColumns; } else if (props.initialState?.pinnedColumns) { - model = props.initialState?.pinnedColumns; + model = props.initialState.pinnedColumns; } else { model = {}; } @@ -52,10 +48,6 @@ export const columnPinningStateInitializer: GridStateInitializer< }; }; -const mergeStateWithPinnedColumns = - (pinnedColumns: GridPinnedColumns) => - (state: GridStatePro): GridStatePro => ({ ...state, pinnedColumns }); - export const useGridColumnPinning = ( apiRef: React.MutableRefObject, props: Pick< @@ -69,7 +61,6 @@ export const useGridColumnPinning = ( >, ): void => { const pinnedColumns = useGridSelector(apiRef, gridPinnedColumnsSelector); - const theme = useTheme(); /** * PRE-PROCESSING @@ -80,14 +71,12 @@ export const useGridColumnPinning = ( return initialValue; } - const visibleColumnFields = gridVisibleColumnFieldsSelector(apiRef); - const [leftPinnedColumns, rightPinnedColumns] = filterColumns( - pinnedColumns, - visibleColumnFields, - theme.direction === 'rtl', - ); + const visiblePinnedColumns = gridVisiblePinnedColumnDefinitionsSelector(apiRef); - if (!params.colIndex || (leftPinnedColumns.length === 0 && rightPinnedColumns.length === 0)) { + if ( + !params.colIndex || + (visiblePinnedColumns.left.length === 0 && visiblePinnedColumns.right.length === 0) + ) { return initialValue; } @@ -101,9 +90,10 @@ export const useGridColumnPinning = ( const offsetWidth = visibleColumns[params.colIndex].computedWidth; const offsetLeft = columnPositions[params.colIndex]; - const leftPinnedColumnsWidth = columnPositions[leftPinnedColumns.length]; + const leftPinnedColumnsWidth = columnPositions[visiblePinnedColumns.left.length]; const rightPinnedColumnsWidth = - columnsTotalWidth - columnPositions[columnPositions.length - rightPinnedColumns.length]; + columnsTotalWidth - + columnPositions[columnPositions.length - visiblePinnedColumns.right.length]; const elementBottom = offsetLeft + offsetWidth; if (elementBottom - (clientWidth - rightPinnedColumnsWidth) > scrollLeft) { @@ -116,7 +106,7 @@ export const useGridColumnPinning = ( } return initialValue; }, - [apiRef, pinnedColumns, props.disableColumnPinning, theme.direction], + [apiRef, props.disableColumnPinning], ); const addColumnMenuItems = React.useCallback>( @@ -136,30 +126,26 @@ export const useGridColumnPinning = ( const checkIfCanBeReordered = React.useCallback>( (initialValue, { targetIndex }) => { - const visibleColumnFields = gridVisibleColumnFieldsSelector(apiRef); - const [leftPinnedColumns, rightPinnedColumns] = filterColumns( - pinnedColumns, - visibleColumnFields, - theme.direction === 'rtl', - ); - - if (leftPinnedColumns.length === 0 && rightPinnedColumns.length === 0) { + const visiblePinnedColumns = gridVisiblePinnedColumnDefinitionsSelector(apiRef); + + if (visiblePinnedColumns.left.length === 0 && visiblePinnedColumns.right.length === 0) { return initialValue; } - if (leftPinnedColumns.length > 0 && targetIndex < leftPinnedColumns.length) { + if (visiblePinnedColumns.left.length > 0 && targetIndex < visiblePinnedColumns.left.length) { return false; } - if (rightPinnedColumns.length > 0) { + if (visiblePinnedColumns.right.length > 0) { const visibleColumns = gridVisibleColumnDefinitionsSelector(apiRef); - const firstRightPinnedColumnIndex = visibleColumns.length - rightPinnedColumns.length; + const firstRightPinnedColumnIndex = + visibleColumns.length - visiblePinnedColumns.right.length; return targetIndex >= firstRightPinnedColumnIndex ? false : initialValue; } return initialValue; }, - [apiRef, pinnedColumns, theme.direction], + [apiRef], ); const stateExportPreProcessing = React.useCallback>( @@ -193,7 +179,7 @@ export const useGridColumnPinning = ( (params, context: GridRestoreStatePreProcessingContext) => { const newPinnedColumns = context.stateToRestore.pinnedColumns; if (newPinnedColumns != null) { - apiRef.current.setState(mergeStateWithPinnedColumns(newPinnedColumns)); + setState(apiRef, newPinnedColumns); } return params; @@ -227,7 +213,7 @@ export const useGridColumnPinning = ( ); const pinColumn = React.useCallback( - (field: string, side: GridPinnedPosition) => { + (field: string, side: GridPinnedColumnPosition) => { checkIfEnabled('pinColumn'); if (apiRef.current.isColumnPinned(field) === side) { @@ -235,7 +221,9 @@ export const useGridColumnPinning = ( } const otherSide = - side === GridPinnedPosition.right ? GridPinnedPosition.left : GridPinnedPosition.right; + side === GridPinnedColumnPosition.RIGHT + ? GridPinnedColumnPosition.LEFT + : GridPinnedColumnPosition.RIGHT; const newPinnedColumns = { [side]: [...(pinnedColumns[side] || []), field], @@ -266,7 +254,7 @@ export const useGridColumnPinning = ( const setPinnedColumns = React.useCallback( (newPinnedColumns) => { checkIfEnabled('setPinnedColumns'); - apiRef.current.setState(mergeStateWithPinnedColumns(newPinnedColumns)); + setState(apiRef, newPinnedColumns); apiRef.current.forceUpdate(); }, [apiRef, checkIfEnabled], @@ -277,11 +265,11 @@ export const useGridColumnPinning = ( checkIfEnabled('isColumnPinned'); const leftPinnedColumns = pinnedColumns.left || []; if (leftPinnedColumns.includes(field)) { - return GridPinnedPosition.left; + return GridPinnedColumnPosition.LEFT; } const rightPinnedColumns = pinnedColumns.right || []; if (rightPinnedColumns.includes(field)) { - return GridPinnedPosition.right; + return GridPinnedColumnPosition.RIGHT; } return false; }, @@ -298,73 +286,69 @@ export const useGridColumnPinning = ( useGridApiMethod(apiRef, columnPinningApi, 'public'); - const handleColumnOrderChange = React.useCallback>( - (params) => { - if (!apiRef.current.caches.columnPinning.orderedFieldsBeforePinningColumns) { - return; - } + const handleColumnOrderChange: GridEventListener<'columnOrderChange'> = (params) => { + if (!apiRef.current.caches.columnPinning.orderedFieldsBeforePinningColumns) { + return; + } - const { column, targetIndex, oldIndex } = params; - const delta = targetIndex > oldIndex ? 1 : -1; - - const latestColumnFields = gridColumnFieldsSelector(apiRef); - - /** - * When a column X is reordered to somewhere else, the position where this column X is dropped - * on must be moved to left or right to make room for it. The ^^^ below represents the column - * which gave space to receive X. - * - * | X | B | C | D | -> | B | C | D | X | (e.g. X moved to after D, so delta=1) - * ^^^ ^^^ - * - * | A | B | C | X | -> | X | A | B | C | (e.g. X moved before A, so delta=-1) - * ^^^ ^^^ - * - * If column P is pinned, it will not move to provide space. However, it will jump to the next - * non-pinned column. - * - * | X | B | P | D | -> | B | D | P | X | (e.g. X moved to after D, with P pinned) - * ^^^ ^^^ - */ - const siblingField = latestColumnFields[targetIndex - delta]; - - const newOrderedFieldsBeforePinningColumns = [ - ...apiRef.current.caches.columnPinning.orderedFieldsBeforePinningColumns, - ]; - - // The index to start swapping fields - let i = newOrderedFieldsBeforePinningColumns.findIndex( - (currentColumn) => currentColumn === column.field, - ); - // The index of the field to swap with - let j = i + delta; - - // When to stop swapping fields. - // We stop one field before because the swap is done with i + 1 (if delta=1) - const stop = newOrderedFieldsBeforePinningColumns.findIndex( - (currentColumn) => currentColumn === siblingField, - ); - - while (delta > 0 ? i < stop : i > stop) { - // If the field to swap with is a pinned column, jump to the next - while (apiRef.current.isColumnPinned(newOrderedFieldsBeforePinningColumns[j])) { - j += delta; - } - - const temp = newOrderedFieldsBeforePinningColumns[i]; - newOrderedFieldsBeforePinningColumns[i] = newOrderedFieldsBeforePinningColumns[j]; - newOrderedFieldsBeforePinningColumns[j] = temp; - - i = j; - j = i + delta; + const { column, targetIndex, oldIndex } = params; + const delta = targetIndex > oldIndex ? 1 : -1; + + const latestColumnFields = gridColumnFieldsSelector(apiRef); + + /** + * When a column X is reordered to somewhere else, the position where this column X is dropped + * on must be moved to left or right to make room for it. The ^^^ below represents the column + * which gave space to receive X. + * + * | X | B | C | D | -> | B | C | D | X | (e.g. X moved to after D, so delta=1) + * ^^^ ^^^ + * + * | A | B | C | X | -> | X | A | B | C | (e.g. X moved before A, so delta=-1) + * ^^^ ^^^ + * + * If column P is pinned, it will not move to provide space. However, it will jump to the next + * non-pinned column. + * + * | X | B | P | D | -> | B | D | P | X | (e.g. X moved to after D, with P pinned) + * ^^^ ^^^ + */ + const siblingField = latestColumnFields[targetIndex - delta]; + + const newOrderedFieldsBeforePinningColumns = [ + ...apiRef.current.caches.columnPinning.orderedFieldsBeforePinningColumns, + ]; + + // The index to start swapping fields + let i = newOrderedFieldsBeforePinningColumns.findIndex( + (currentColumn) => currentColumn === column.field, + ); + // The index of the field to swap with + let j = i + delta; + + // When to stop swapping fields. + // We stop one field before because the swap is done with i + 1 (if delta=1) + const stop = newOrderedFieldsBeforePinningColumns.findIndex( + (currentColumn) => currentColumn === siblingField, + ); + + while (delta > 0 ? i < stop : i > stop) { + // If the field to swap with is a pinned column, jump to the next + while (apiRef.current.isColumnPinned(newOrderedFieldsBeforePinningColumns[j])) { + j += delta; } - apiRef.current.caches.columnPinning.orderedFieldsBeforePinningColumns = - newOrderedFieldsBeforePinningColumns; - }, + const temp = newOrderedFieldsBeforePinningColumns[i]; + newOrderedFieldsBeforePinningColumns[i] = newOrderedFieldsBeforePinningColumns[j]; + newOrderedFieldsBeforePinningColumns[j] = temp; - [apiRef], - ); + i = j; + j = i + delta; + } + + apiRef.current.caches.columnPinning.orderedFieldsBeforePinningColumns = + newOrderedFieldsBeforePinningColumns; + }; useGridApiEventHandler(apiRef, 'columnOrderChange', handleColumnOrderChange); @@ -374,3 +358,13 @@ export const useGridColumnPinning = ( } }, [apiRef, props.pinnedColumns]); }; + +function setState( + apiRef: React.MutableRefObject, + model: GridPinnedColumnFields, +) { + apiRef.current.setState((state) => ({ + ...state, + pinnedColumns: model, + })); +} diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts index ce2b6d6c1e54..6982ac82e130 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts @@ -1,43 +1,53 @@ import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; -import { GridPipeProcessor, useGridRegisterPipeProcessor } from '@mui/x-data-grid/internals'; +import { + GridPinnedColumnFields, + GridPipeProcessor, + gridPinnedColumnsSelector, + useGridRegisterPipeProcessor, + eslintUseValue, + gridVisiblePinnedColumnDefinitionsSelector, +} from '@mui/x-data-grid/internals'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; -import { gridPinnedColumnsSelector } from './gridColumnPinningSelector'; -import { columnPinningStateInitializer } from './useGridColumnPinning'; -import { GridApiPro, GridPrivateApiPro } from '../../../models/gridApiPro'; -import { filterColumns } from '../../../components/DataGridProVirtualScroller'; +import { GridPrivateApiPro } from '../../../models/gridApiPro'; export const useGridColumnPinningPreProcessors = ( apiRef: React.MutableRefObject, props: DataGridProProcessedProps, ) => { - const { disableColumnPinning, pinnedColumns: pinnedColumnsProp, initialState } = props; - const theme = useTheme(); - let pinnedColumns = gridPinnedColumnsSelector(apiRef.current.state); - if (pinnedColumns == null) { - // Since the state is not ready yet lets use the initializer to get which - // columns should be pinned initially. - const initializedState = columnPinningStateInitializer( - apiRef.current.state, - { disableColumnPinning, pinnedColumns: pinnedColumnsProp, initialState }, - apiRef, - ) as GridApiPro['state']; - pinnedColumns = gridPinnedColumnsSelector(initializedState); + const { disableColumnPinning } = props; + + let pinnedColumns: GridPinnedColumnFields | null; + if (apiRef.current.state.columns) { + pinnedColumns = gridPinnedColumnsSelector(apiRef.current.state); + } else { + pinnedColumns = null; } const prevAllPinnedColumns = React.useRef([]); const reorderPinnedColumns = React.useCallback>( (columnsState) => { + eslintUseValue(pinnedColumns); + if (columnsState.orderedFields.length === 0 || disableColumnPinning) { return columnsState; } - const [leftPinnedColumns, rightPinnedColumns] = filterColumns( - pinnedColumns, - columnsState.orderedFields, - theme.direction === 'rtl', - ); + // HACK: This is a hack needed because the pipe processors aren't pure enough. What + // they should be is `gridState -> gridState` transformers, but they only transform a slice + // of the state, not the full state. So if they need access to other parts of the state (like + // the `state.columns.orderedFields` in this case), they might lag behind because the selectors + // are selecting the old state in `apiRef`, not the state being computed in the current pipe processor. + const savedState = apiRef.current.state; + apiRef.current.state = { ...savedState, columns: columnsState as unknown as any }; + + const visibleColumns = gridVisiblePinnedColumnDefinitionsSelector(apiRef); + + apiRef.current.state = savedState; + // HACK: Ends here // + + const leftPinnedColumns = visibleColumns.left.map((c) => c.field); + const rightPinnedColumns = visibleColumns.right.map((c) => c.field); let newOrderedFields: string[]; const allPinnedColumns = [...leftPinnedColumns, ...rightPinnedColumns]; @@ -121,7 +131,7 @@ export const useGridColumnPinningPreProcessors = ( orderedFields: [...leftPinnedColumns, ...centerColumns, ...rightPinnedColumns], }; }, - [apiRef, disableColumnPinning, pinnedColumns, theme.direction], + [apiRef, disableColumnPinning, pinnedColumns], ); useGridRegisterPipeProcessor(apiRef, 'hydrateColumns', reorderPinnedColumns); diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/columnResize/useGridColumnResize.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/columnResize/useGridColumnResize.tsx index eed1b3bb4b8a..eb0f0835cf81 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/columnResize/useGridColumnResize.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/columnResize/useGridColumnResize.tsx @@ -9,6 +9,7 @@ import { CursorCoordinates, GridColumnHeaderSeparatorSides, GridColumnResizeParams, + GridPinnedColumnPosition, useGridApiEventHandler, useGridApiOptionHandler, useGridApiMethod, @@ -31,6 +32,9 @@ import { import { useTheme, Direction } from '@mui/material/styles'; import { findGridCellElementsFromCol, + findGridElement, + findLeftPinnedCellsAfterCol, + findRightPinnedCellsBeforeCol, getFieldFromHeaderElem, findHeaderElementFromField, findGroupHeaderElementsFromField, @@ -125,8 +129,8 @@ function flipResizeDirection(side: ResizeDirection) { return 'Right'; } -function getResizeDirection(element: HTMLElement, direction: Direction) { - const side = element.classList.contains(gridClasses['columnSeparator--sideRight']) +function getResizeDirection(separator: HTMLElement, direction: Direction) { + const side = separator.classList.contains(gridClasses['columnSeparator--sideRight']) ? 'Right' : 'Left'; if (direction === 'rtl') { @@ -276,14 +280,18 @@ export const useGridColumnResize = ( | 'onColumnWidthChange' >, ) => { + const theme = useTheme(); const logger = useGridLogger(apiRef, 'useGridColumnResize'); const colDefRef = React.useRef(); - const colElementRef = React.useRef(); + const columnHeaderElementRef = React.useRef(); const headerFilterElementRef = React.useRef(); - const colGroupingElementRef = React.useRef(); - const colCellElementsRef = React.useRef(); - const theme = useTheme(); + const groupHeaderElementsRef = React.useRef([]); + const cellElementsRef = React.useRef([]); + const leftPinnedCellsAfterRef = React.useRef([]); + const rightPinnedCellsBeforeRef = React.useRef([]); + const fillerLeftRef = React.useRef(); + const fillerRightRef = React.useRef(); // To improve accessibility, the separator has padding on both sides. // Clicking inside the padding area should be treated as a click in the separator. @@ -297,26 +305,25 @@ export const useGridColumnResize = ( const updateWidth = (newWidth: number) => { logger.debug(`Updating width to ${newWidth} for col ${colDefRef.current!.field}`); - const prevWidth = colElementRef.current!.offsetWidth; + const prevWidth = columnHeaderElementRef.current!.offsetWidth; const widthDiff = newWidth - prevWidth; colDefRef.current!.computedWidth = newWidth; colDefRef.current!.width = newWidth; colDefRef.current!.flex = 0; - colElementRef.current!.style.width = `${newWidth}px`; - colElementRef.current!.style.minWidth = `${newWidth}px`; - colElementRef.current!.style.maxWidth = `${newWidth}px`; + columnHeaderElementRef.current!.style.width = `${newWidth}px`; + columnHeaderElementRef.current!.style.minWidth = `${newWidth}px`; + columnHeaderElementRef.current!.style.maxWidth = `${newWidth}px`; const headerFilterElement = headerFilterElementRef.current; - if (headerFilterElement) { headerFilterElement.style.width = `${newWidth}px`; headerFilterElement.style.minWidth = `${newWidth}px`; headerFilterElement.style.maxWidth = `${newWidth}px`; } - [...colCellElementsRef.current!, ...colGroupingElementRef.current!].forEach((element) => { + groupHeaderElementsRef.current!.forEach((element) => { const div = element as HTMLDivElement; let finalWidth: `${number}px`; @@ -332,6 +339,39 @@ export const useGridColumnResize = ( div.style.minWidth = finalWidth; div.style.maxWidth = finalWidth; }); + + cellElementsRef.current!.forEach((element) => { + const div = element as HTMLDivElement; + let finalWidth: `${number}px`; + + if (div.getAttribute('aria-colspan') === '1') { + finalWidth = `${newWidth}px`; + } else { + // Cell with colspan > 1 cannot be just updated width new width. + // Instead, we add width diff to the current width. + finalWidth = `${div.offsetWidth + widthDiff}px`; + } + + div.style.setProperty('--width', finalWidth); + }); + + const pinnedPosition = apiRef.current.isColumnPinned(colDefRef.current!.field); + + if (pinnedPosition === GridPinnedColumnPosition.LEFT) { + updateProperty(fillerLeftRef.current!, 'width', widthDiff); + + leftPinnedCellsAfterRef.current.forEach((cell) => { + updateProperty(cell, 'left', widthDiff); + }); + } + + if (pinnedPosition === GridPinnedColumnPosition.RIGHT) { + updateProperty(fillerRightRef.current!, 'width', widthDiff); + + rightPinnedCellsBeforeRef.current.forEach((cell) => { + updateProperty(cell, 'right', widthDiff); + }); + } }; const finishResize = (nativeEvent: MouseEvent) => { @@ -350,6 +390,56 @@ export const useGridColumnResize = ( }); }; + const storeReferences = (colDef: GridStateColDef, separator: HTMLElement, xStart: number) => { + const root = apiRef.current.rootElementRef.current!; + + colDefRef.current = colDef as GridStateColDef; + + columnHeaderElementRef.current = findHeaderElementFromField( + apiRef.current.columnHeadersContainerElementRef!.current!, + colDef.field, + ); + + const headerFilterElement = root.querySelector( + `.${gridClasses.headerFilterRow} [data-field="${colDef.field}"]`, + ); + if (headerFilterElement) { + headerFilterElementRef.current = headerFilterElement as HTMLDivElement; + } + + groupHeaderElementsRef.current = findGroupHeaderElementsFromField( + apiRef.current.columnHeadersContainerElementRef?.current!, + colDef.field, + ); + + cellElementsRef.current = findGridCellElementsFromCol( + columnHeaderElementRef.current, + apiRef.current, + ); + + fillerLeftRef.current = findGridElement(apiRef.current, 'filler--pinnedLeft'); + fillerRightRef.current = findGridElement(apiRef.current, 'filler--pinnedRight'); + + const pinnedPosition = apiRef.current.isColumnPinned(colDef.field); + + leftPinnedCellsAfterRef.current = + pinnedPosition !== GridPinnedColumnPosition.LEFT + ? [] + : findLeftPinnedCellsAfterCol(apiRef.current, columnHeaderElementRef.current); + rightPinnedCellsBeforeRef.current = + pinnedPosition !== GridPinnedColumnPosition.RIGHT + ? [] + : findRightPinnedCellsBeforeCol(apiRef.current, columnHeaderElementRef.current); + + resizeDirection.current = getResizeDirection(separator, theme.direction); + + initialOffsetToSeparator.current = computeOffsetToSeparator( + xStart, + columnHeaderElementRef.current!.getBoundingClientRect(), + resizeDirection.current, + ); + }; + const handleResizeMouseUp = useEventCallback(finishResize); const handleResizeMouseMove = useEventCallback((nativeEvent: MouseEvent) => { @@ -362,7 +452,7 @@ export const useGridColumnResize = ( let newWidth = computeNewWidth( initialOffsetToSeparator.current!, nativeEvent.clientX, - colElementRef.current!.getBoundingClientRect(), + columnHeaderElementRef.current!.getBoundingClientRect(), resizeDirection.current!, ); @@ -370,7 +460,7 @@ export const useGridColumnResize = ( updateWidth(newWidth); const params: GridColumnResizeParams = { - element: colElementRef.current, + element: columnHeaderElementRef.current, colDef: colDefRef.current!, width: newWidth, }; @@ -402,7 +492,7 @@ export const useGridColumnResize = ( let newWidth = computeNewWidth( initialOffsetToSeparator.current!, (finger as CursorCoordinates).x, - colElementRef.current!.getBoundingClientRect(), + columnHeaderElementRef.current!.getBoundingClientRect(), resizeDirection.current!, ); @@ -410,7 +500,7 @@ export const useGridColumnResize = ( updateWidth(newWidth); const params: GridColumnResizeParams = { - element: colElementRef.current, + element: columnHeaderElementRef.current, colDef: colDefRef.current!, width: newWidth, }; @@ -421,7 +511,7 @@ export const useGridColumnResize = ( const cellSeparator = findParentElementFromClassName( event.target, gridClasses['columnSeparator--resizable'], - ); + ) as HTMLElement | null; // Let the event bubble if the target is not a col separator if (!cellSeparator) { return; @@ -437,34 +527,17 @@ export const useGridColumnResize = ( touchId.current = touch.identifier; } - colElementRef.current = findParentElementFromClassName( + const columnHeaderElement = findParentElementFromClassName( event.target, gridClasses.columnHeader, ) as HTMLDivElement; - const field = getFieldFromHeaderElem(colElementRef.current!); + const field = getFieldFromHeaderElem(columnHeaderElement); const colDef = apiRef.current.getColumn(field); - colGroupingElementRef.current = findGroupHeaderElementsFromField( - apiRef.current.columnHeadersContainerElementRef?.current!, - field, - ); logger.debug(`Start Resize on col ${colDef.field}`); apiRef.current.publishEvent('columnResizeStart', { field }, event); - colDefRef.current = colDef as GridStateColDef; - colElementRef.current = findHeaderElementFromField( - apiRef.current.columnHeadersElementRef?.current!, - colDef.field, - ) as HTMLDivElement; - colCellElementsRef.current = findGridCellElementsFromCol(colElementRef.current, apiRef.current); - - resizeDirection.current = getResizeDirection(event.target, theme.direction); - - initialOffsetToSeparator.current = computeOffsetToSeparator( - touch.clientX, - colElementRef.current!.getBoundingClientRect(), - resizeDirection.current!, - ); + storeReferences(colDef, cellSeparator, touch.clientX); const doc = ownerDocument(event.currentTarget as HTMLElement); doc.addEventListener('touchmove', handleTouchMove); @@ -483,12 +556,12 @@ export const useGridColumnResize = ( setTimeout(() => { doc.removeEventListener('click', preventClick, true); }, 100); - if (colElementRef.current) { - colElementRef.current!.style.pointerEvents = 'unset'; + if (columnHeaderElementRef.current) { + columnHeaderElementRef.current!.style.pointerEvents = 'unset'; } }, [ apiRef, - colElementRef, + columnHeaderElementRef, handleResizeMouseMove, handleResizeMouseUp, handleTouchMove, @@ -532,41 +605,11 @@ export const useGridColumnResize = ( logger.debug(`Start Resize on col ${colDef.field}`); apiRef.current.publishEvent('columnResizeStart', { field: colDef.field }, event); - colDefRef.current = colDef as GridStateColDef; - colElementRef.current = - apiRef.current.columnHeadersContainerElementRef?.current!.querySelector( - `[data-field="${colDef.field}"]`, - )!; - - const headerFilterRowElement = apiRef.current.headerFiltersElementRef?.current; - - if (headerFilterRowElement) { - headerFilterElementRef.current = headerFilterRowElement.querySelector( - `[data-field="${colDef.field}"]`, - ) as HTMLDivElement; - } - - colGroupingElementRef.current = findGroupHeaderElementsFromField( - apiRef.current.columnHeadersContainerElementRef?.current!, - colDef.field, - ); - - colCellElementsRef.current = findGridCellElementsFromCol( - colElementRef.current, - apiRef.current, - ); + storeReferences(colDef, event.currentTarget, event.clientX); const doc = ownerDocument(apiRef.current.rootElementRef!.current); doc.body.style.cursor = 'col-resize'; - resizeDirection.current = getResizeDirection(event.currentTarget, theme.direction); - - initialOffsetToSeparator.current = computeOffsetToSeparator( - event.clientX, - colElementRef.current!.getBoundingClientRect(), - resizeDirection.current, - ); - doc.addEventListener('mousemove', handleResizeMouseMove); doc.addEventListener('mouseup', handleResizeMouseUp); @@ -646,7 +689,7 @@ export const useGridColumnResize = ( total + (widthByField[column.field] ?? column.computedWidth ?? column.width), 0, ); - const availableWidth = apiRef.current.getRootDimensions()?.viewportInnerSize.width ?? 0; + const availableWidth = apiRef.current.getRootDimensions().viewportInnerSize.width; const remainingWidth = availableWidth - totalWidth; if (remainingWidth > 0) { @@ -705,3 +748,7 @@ export const useGridColumnResize = ( useGridApiOptionHandler(apiRef, 'columnResize', props.onColumnResize); useGridApiOptionHandler(apiRef, 'columnWidthChange', props.onColumnWidthChange); }; + +function updateProperty(element: HTMLElement, property: 'right' | 'left' | 'width', delta: number) { + element.style[property] = `${parseInt(element.style[property], 10) + delta}px`; +} diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts b/packages/grid/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts index d478a13778dd..b7e8fc2256cb 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts @@ -28,6 +28,9 @@ import { GridDetailPanelState, } from './gridDetailPanelInterface'; +// FIXME: calling `api.updateDimensions()` here triggers a cycle where `updateDimensions` is +// called 3 times when opening/closing a panel. + export const detailPanelStateInitializer: GridStateInitializer< Pick > = (state, props) => { @@ -170,6 +173,7 @@ export const useGridDetailPanel = ( }, }; }); + apiRef.current.updateDimensions(); apiRef.current.forceUpdate(); }, [apiRef], @@ -194,6 +198,7 @@ export const useGridDetailPanel = ( }, }; }); + apiRef.current.updateDimensions(); apiRef.current.requestPipeProcessorsApplication('rowHeight'); }, @@ -248,6 +253,7 @@ export const useGridDetailPanel = ( }, }; }); + apiRef.current.updateDimensions?.(); apiRef.current.forceUpdate(); }, [apiRef, props.getDetailPanelContent, props.getDetailPanelHeight]); @@ -280,6 +286,7 @@ export const useGridDetailPanel = ( }, }; }); + apiRef.current.updateDimensions?.(); previousGetDetailPanelContentProp.current = props.getDetailPanelContent; previousGetDetailPanelHeightProp.current = props.getDetailPanelHeight; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/infiniteLoader/useGridInfiniteLoader.ts b/packages/grid/x-data-grid-pro/src/hooks/features/infiniteLoader/useGridInfiniteLoader.ts index 0fa8bafe1d8a..1842c9359977 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/infiniteLoader/useGridInfiniteLoader.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/infiniteLoader/useGridInfiniteLoader.ts @@ -37,7 +37,7 @@ export const useGridInfiniteLoader = ( const dimensions = apiRef.current.getRootDimensions(); // Prevent the infite loading working in combination with lazy loading - if (!dimensions || props.rowsLoadingMode !== 'client') { + if (!dimensions.isReady || props.rowsLoadingMode !== 'client') { return; } diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts b/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts index 6f19a0a3721f..f81923544904 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts @@ -1,17 +1,17 @@ import * as React from 'react'; import { useGridApiEventHandler, - GridRenderedRowsIntervalChangeParams, useGridSelector, gridSortModelSelector, gridFilterModelSelector, + gridRenderContextSelector, useGridApiOptionHandler, GridEventListener, GridRowEntry, GridDimensions, GridFeatureMode, } from '@mui/x-data-grid'; -import { useGridVisibleRows, getRenderableIndexes } from '@mui/x-data-grid/internals'; +import { useGridVisibleRows } from '@mui/x-data-grid/internals'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; import { DataGridProProcessedProps, @@ -104,28 +104,12 @@ export const useGridLazyLoader = ( const visibleRows = useGridVisibleRows(privateApiRef, props); const sortModel = useGridSelector(privateApiRef, gridSortModelSelector); const filterModel = useGridSelector(privateApiRef, gridFilterModelSelector); - const renderedRowsIntervalCache = React.useRef({ + const renderedRowsIntervalCache = React.useRef({ firstRowToRender: 0, lastRowToRender: 0, }); const { lazyLoading } = (props.experimentalFeatures ?? {}) as GridExperimentalProFeatures; - const getCurrentIntervalToRender = React.useCallback(() => { - const currentRenderContext = privateApiRef.current.getRenderContext(); - const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ - firstIndex: currentRenderContext.firstRowIndex, - lastIndex: currentRenderContext.lastRowIndex, - minFirstIndex: 0, - maxLastIndex: visibleRows.rows.length, - buffer: props.rowBuffer, - }); - - return { - firstRowToRender, - lastRowToRender, - }; - }, [privateApiRef, props.rowBuffer, visibleRows.rows.length]); - const handleRenderedRowsIntervalChange = React.useCallback< GridEventListener<'renderedRowsIntervalChange'> >( @@ -143,15 +127,15 @@ export const useGridLazyLoader = ( } const fetchRowsParams: GridFetchRowsParams = { - firstRowToRender: params.firstRowToRender, - lastRowToRender: params.lastRowToRender, + firstRowToRender: params.firstRowIndex, + lastRowToRender: params.lastRowIndex, sortModel, filterModel, }; if ( - renderedRowsIntervalCache.current.firstRowToRender === params.firstRowToRender && - renderedRowsIntervalCache.current.lastRowToRender === params.lastRowToRender + renderedRowsIntervalCache.current.firstRowToRender === params.firstRowIndex && + renderedRowsIntervalCache.current.lastRowToRender === params.lastRowIndex ) { return; } @@ -161,8 +145,8 @@ export const useGridLazyLoader = ( apiRef: privateApiRef, visibleRows: visibleRows.rows, range: { - firstRowIndex: params.firstRowToRender, - lastRowIndex: params.lastRowToRender, + firstRowIndex: params.firstRowIndex, + lastRowIndex: params.lastRowIndex, }, }); @@ -174,7 +158,10 @@ export const useGridLazyLoader = ( fetchRowsParams.lastRowToRender = skeletonRowsSection.lastRowIndex; } - renderedRowsIntervalCache.current = params; + renderedRowsIntervalCache.current = { + firstRowToRender: params.firstRowIndex, + lastRowToRender: params.lastRowIndex, + }; privateApiRef.current.publishEvent('fetchRows', fetchRowsParams); }, @@ -196,17 +183,17 @@ export const useGridLazyLoader = ( privateApiRef.current.requestPipeProcessorsApplication('hydrateRows'); - const { firstRowToRender, lastRowToRender } = getCurrentIntervalToRender(); + const renderContext = gridRenderContextSelector(privateApiRef); const fetchRowsParams: GridFetchRowsParams = { - firstRowToRender, - lastRowToRender, + firstRowToRender: renderContext.firstRowIndex, + lastRowToRender: renderContext.lastRowIndex, sortModel: newSortModel, filterModel, }; privateApiRef.current.publishEvent('fetchRows', fetchRowsParams); }, - [privateApiRef, props.rowsLoadingMode, filterModel, lazyLoading, getCurrentIntervalToRender], + [privateApiRef, props.rowsLoadingMode, filterModel, lazyLoading], ); const handleGridFilterModelChange = React.useCallback>( @@ -225,17 +212,17 @@ export const useGridLazyLoader = ( privateApiRef.current.requestPipeProcessorsApplication('hydrateRows'); - const { firstRowToRender, lastRowToRender } = getCurrentIntervalToRender(); + const renderContext = gridRenderContextSelector(privateApiRef); const fetchRowsParams: GridFetchRowsParams = { - firstRowToRender, - lastRowToRender, + firstRowToRender: renderContext.firstRowIndex, + lastRowToRender: renderContext.lastRowIndex, sortModel, filterModel: newFilterModel, }; privateApiRef.current.publishEvent('fetchRows', fetchRowsParams); }, - [privateApiRef, props.rowsLoadingMode, sortModel, lazyLoading, getCurrentIntervalToRender], + [privateApiRef, props.rowsLoadingMode, sortModel, lazyLoading], ); useGridApiEventHandler( diff --git a/packages/grid/x-data-grid-pro/src/internals/index.ts b/packages/grid/x-data-grid-pro/src/internals/index.ts index c2f28c31b911..3f8352dd07ed 100644 --- a/packages/grid/x-data-grid-pro/src/internals/index.ts +++ b/packages/grid/x-data-grid-pro/src/internals/index.ts @@ -1,7 +1,6 @@ // eslint-disable-next-line import/export export * from '@mui/x-data-grid/internals'; -export { DataGridProVirtualScroller } from '../components/DataGridProVirtualScroller'; export { GridColumnHeaders } from '../components/GridColumnHeaders'; export { DATA_GRID_PRO_DEFAULT_SLOTS_COMPONENTS } from '../constants/dataGridProDefaultSlotsComponents'; diff --git a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts index 470de284a17c..aed84a135e0a 100644 --- a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts +++ b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts @@ -14,8 +14,8 @@ import { DataGridPropsWithDefaultValues, DataGridPropsWithComplexDefaultValueAfterProcessing, DataGridPropsWithComplexDefaultValueBeforeProcessing, + GridPinnedColumnFields, } from '@mui/x-data-grid/internals'; -import type { GridPinnedColumns } from '../hooks/features/columnPinning'; import type { GridPinnedRowsProp } from '../hooks/features/rowPinning'; import { GridApiPro } from './gridApiPro'; import { @@ -211,13 +211,16 @@ export interface DataGridProPropsWithoutDefaultValue void; + onPinnedColumnsChange?: ( + pinnedColumns: GridPinnedColumnFields, + details: GridCallbackDetails, + ) => void; /** * The grouping column used by the tree data. */ diff --git a/packages/grid/x-data-grid-pro/src/models/gridStatePro.ts b/packages/grid/x-data-grid-pro/src/models/gridStatePro.ts index 2ae098c21f1f..bad4a22adbdf 100644 --- a/packages/grid/x-data-grid-pro/src/models/gridStatePro.ts +++ b/packages/grid/x-data-grid-pro/src/models/gridStatePro.ts @@ -1,13 +1,14 @@ import { GridInitialState as GridInitialStateCommunity, GridState as GridStateCommunity, + GridColumnPinningState, + GridPinnedColumnFields, } from '@mui/x-data-grid'; import type { GridDetailPanelState, GridDetailPanelInitialState, GridColumnReorderState, GridColumnResizeState, - GridColumnPinningState, } from '../hooks'; /** @@ -24,6 +25,6 @@ export interface GridStatePro extends GridStateCommunity { * The initial state of `DataGridPro`. */ export interface GridInitialStatePro extends GridInitialStateCommunity { - pinnedColumns?: GridColumnPinningState; + pinnedColumns?: GridPinnedColumnFields; detailPanel?: GridDetailPanelInitialState; } diff --git a/packages/grid/x-data-grid-pro/src/tests/DataGridPro.spec.tsx b/packages/grid/x-data-grid-pro/src/tests/DataGridPro.spec.tsx index 39e3116284c9..23cf358dbb1a 100644 --- a/packages/grid/x-data-grid-pro/src/tests/DataGridPro.spec.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/DataGridPro.spec.tsx @@ -92,10 +92,6 @@ function ApiRefPrivateMethods() { apiRef.current.getLastMeasuredRowIndex; // @ts-expect-error Property 'getViewportPageSize' does not exist on type 'GridApiPro' apiRef.current.getViewportPageSize; - // @ts-expect-error Property 'updateGridDimensionsRef' does not exist on type 'GridApiPro' - apiRef.current.updateGridDimensionsRef; - // @ts-expect-error Property 'getRenderContext' does not exist on type 'GridApiPro' - apiRef.current.getRenderContext; // @ts-expect-error Property 'setCellEditingEditCellValue' does not exist on type 'GridApiPro' apiRef.current.setCellEditingEditCellValue; // @ts-expect-error Property 'getRowWithUpdatedValuesFromCellEditing' does not exist on type 'GridApiPro' diff --git a/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx index 44e0eb07521a..534f7807be31 100644 --- a/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx @@ -7,12 +7,11 @@ import { useGridApiRef, DataGridProProps, gridClasses, - GridPinnedPosition, + GridPinnedColumnPosition, GridColumnGroupingModel, GridColDef, - GRID_CHECKBOX_SELECTION_FIELD, } from '@mui/x-data-grid-pro'; -import { useBasicDemoData, getBasicGridData } from '@mui/x-data-grid-generator'; +import { useBasicDemoData } from '@mui/x-data-grid-generator'; import { createRenderer, fireEvent, @@ -21,7 +20,14 @@ import { act, userEvent, } from '@mui-internal/test-utils'; -import { getCell, getColumnHeaderCell, getColumnHeadersTextContent } from 'test/utils/helperFn'; +import { + $, + $$, + microtasks, + getCell, + getColumnHeaderCell, + getColumnHeadersTextContent, +} from 'test/utils/helperFn'; // TODO Move to utils // Fix https://github.com/mui/mui-x/pull/2085/files/058f56ac3c729b2142a9a28b79b5b13535cdb819#diff-db85480a519a5286d7341e9b8957844762cf04cdacd946331ebaaaff287482ec @@ -117,117 +123,6 @@ describe(' - Column pinning', () => { expect(virtualScroller.scrollLeft).to.equal(100); }); - it('should apply .Mui-hovered on the entire row when the mouse enters the row', () => { - render(); - const leftColumns = document.querySelector(`.${gridClasses['pinnedColumns--left']}`); - const rightColumns = document.querySelector(`.${gridClasses['pinnedColumns--right']}`); - const renderZone = document.querySelector(`.${gridClasses.virtualScrollerRenderZone}`); - expect(leftColumns!.querySelector('[data-rowindex="0"]')).not.to.have.class('Mui-hovered'); - expect(rightColumns!.querySelector('[data-rowindex="0"]')).not.to.have.class('Mui-hovered'); - expect(renderZone!.querySelector('[data-rowindex="0"]')).not.to.have.class('Mui-hovered'); - const cell = getCell(0, 0); - fireEvent.mouseEnter(cell); - expect(leftColumns!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - expect(rightColumns!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - expect(renderZone!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - }); - - it('should remove .Mui-hovered from the entire row when the mouse leaves the row', () => { - render(); - const cell = getCell(0, 0); - fireEvent.mouseEnter(cell); - const leftColumns = document.querySelector(`.${gridClasses['pinnedColumns--left']}`); - const rightColumns = document.querySelector(`.${gridClasses['pinnedColumns--right']}`); - const renderZone = document.querySelector(`.${gridClasses.virtualScrollerRenderZone}`); - expect(leftColumns!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - expect(rightColumns!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - expect(renderZone!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - fireEvent.mouseLeave(cell); - expect(leftColumns!.querySelector('[data-rowindex="0"]')).not.to.have.class('Mui-hovered'); - expect(rightColumns!.querySelector('[data-rowindex="0"]')).not.to.have.class('Mui-hovered'); - expect(renderZone!.querySelector('[data-rowindex="0"]')).not.to.have.class('Mui-hovered'); - }); - - // https://github.com/mui/mui-x/issues/10176 - it('should keep .Mui-hovered on the entire row when row is selected and deselected', () => { - render( - , - ); - const leftColumns = document.querySelector(`.${gridClasses['pinnedColumns--left']}`); - const rightColumns = document.querySelector(`.${gridClasses['pinnedColumns--right']}`); - const renderZone = document.querySelector(`.${gridClasses.virtualScrollerRenderZone}`); - const cell = getCell(0, 0); - - fireEvent.mouseEnter(cell); - expect(leftColumns!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - expect(rightColumns!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - expect(renderZone!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - - const checkbox = cell.querySelector('input[type="checkbox"]')!; - fireEvent.click(checkbox); - fireEvent.click(checkbox); - expect(leftColumns!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - expect(rightColumns!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - expect(renderZone!.querySelector('[data-rowindex="0"]')).to.have.class('Mui-hovered'); - }); - - it('should update the render zone offset after resize', function test() { - if (isJSDOM) { - // Need layouting - this.skip(); - } - render(); - const renderZone = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone}`, - )!; - expect(renderZone).toHaveInlineStyle({ transform: 'translate3d(100px, 0px, 0px)' }); - const columnHeader = getColumnHeaderCell(0); - const separator = columnHeader.querySelector(`.${gridClasses['columnSeparator--resizable']}`)!; - fireEvent.mouseDown(separator, { clientX: 100 }); - fireEvent.mouseMove(separator, { clientX: 110, buttons: 1 }); - fireEvent.mouseUp(separator); - clock.runToLast(); - expect(renderZone).toHaveInlineStyle({ transform: 'translate3d(110px, 0px, 0px)' }); - }); - - it('should update the column headers offset after resize', function test() { - if (isJSDOM) { - // Need layouting - this.skip(); - } - render(); - const columnHeadersInner = document.querySelector( - `.${gridClasses.columnHeadersInner}`, - )!; - expect(columnHeadersInner).toHaveInlineStyle({ transform: 'translate3d(100px, 0px, 0px)' }); - const columnHeader = getColumnHeaderCell(0); - const separator = columnHeader.querySelector(`.${gridClasses['columnSeparator--resizable']}`)!; - fireEvent.mouseDown(separator, { clientX: 100 }); - fireEvent.mouseMove(separator, { clientX: 110, buttons: 1 }); - fireEvent.mouseUp(separator); - expect(columnHeadersInner).toHaveInlineStyle({ transform: 'translate3d(110px, 0px, 0px)' }); - }); - - it('should update the render zone offset after pinning the column', function test() { - render(); - const renderZone = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone}`, - )!; - expect(renderZone).toHaveInlineStyle({ transform: 'translate3d(0px, 0px, 0px)' }); - - const columnCell = document.querySelector('[role="columnheader"][data-field="id"]')!; - const menuIconButton = columnCell.querySelector('button[aria-label="Menu"]')!; - fireEvent.click(menuIconButton); - - fireEvent.click(screen.getByRole('menuitem', { name: 'Pin to left' })); - expect(renderZone).toHaveInlineStyle({ transform: 'translate3d(100px, 0px, 0px)' }); - }); - it('should increase the width of right pinned columns by resizing to the left', function test() { if (isJSDOM) { // Need layouting @@ -310,104 +205,6 @@ describe(' - Column pinning', () => { expect(getColumnHeadersTextContent()).to.deep.equal(['id', '', 'Currency Pair']); }); - describe('dynamic row height', () => { - let skipTest = false; - - beforeEach(function beforeEach() { - const { userAgent } = window.navigator; - - // Need layouting and on Chrome non-headless and Edge these tests are flacky - skipTest = !userAgent.includes('Headless') || /edg/i.test(userAgent); - }); - - it('should work with dynamic row height', async function test() { - if (skipTest) { - this.skip(); - } - - function Test({ bioHeight }: { bioHeight: number }) { - const data = React.useMemo(() => getBasicGridData(1, 2), []); - - const columns = [ - ...data.columns, - { field: 'bio', renderCell: () =>
}, - ]; - - return ( - 'auto'} - initialState={{ pinnedColumns: { left: ['id'], right: ['bio'] } }} - /> - ); - } - - render(); - await act(() => Promise.resolve()); - clock.runToLast(); - const leftRow = document.querySelector(`.${gridClasses['pinnedColumns--left']} [role="row"]`); - expect(leftRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); - const centerRow = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone} [role="row"]`, - ); - expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); - const rightRow = document.querySelector( - `.${gridClasses['pinnedColumns--right']} [role="row"]`, - ); - expect(rightRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); - }); - - it('should react to content height changes', async function test() { - if (skipTest) { - this.skip(); - } - - function Test({ bioHeight }: { bioHeight: number }) { - const data = React.useMemo(() => getBasicGridData(1, 2), []); - - const columns = [ - ...data.columns, - { field: 'bio', renderCell: () =>
}, - ]; - - return ( - 'auto'} - initialState={{ pinnedColumns: { left: ['id'], right: ['bio'] } }} - /> - ); - } - - const { setProps } = render(); - await act(() => Promise.resolve()); - clock.runToLast(); - const centerRow = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone} [role="row"]`, - ); - expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); - - setProps({ bioHeight: 200 }); - await act(() => Promise.resolve()); - clock.runToLast(); - expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '201px' }); - - setProps({ bioHeight: 100 }); - await act(() => Promise.resolve()); - clock.runToLast(); - // If the new height is smaller than the current one, it won't be reflected unless - // apiRef.current.resetRowHeights() is called - expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '201px' }); - - act(() => apiRef.current.resetRowHeights()); - await act(() => Promise.resolve()); - clock.runToLast(); - expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); - }); - }); - it('should add border to right pinned columns section when `showCellVerticalBorder={true}`', function test() { if (isJSDOM) { // Doesn't work with mocked window.getComputedStyle @@ -421,7 +218,7 @@ describe(' - Column pinning', () => { ); const computedStyle = window.getComputedStyle( - document.querySelector('.MuiDataGrid-pinnedColumns--right')!, + document.querySelector('.MuiDataGrid-cell--pinnedRight')!, ); const borderLeftColor = computedStyle.getPropertyValue('border-left-color'); const borderLeftWidth = computedStyle.getPropertyValue('border-left-width'); @@ -434,19 +231,19 @@ describe(' - Column pinning', () => { it('should call when a column is pinned', () => { const handlePinnedColumnsChange = spy(); render(); - act(() => apiRef.current.pinColumn('currencyPair', GridPinnedPosition.left)); + act(() => apiRef.current.pinColumn('currencyPair', GridPinnedColumnPosition.LEFT)); expect(handlePinnedColumnsChange.lastCall.args[0]).to.deep.equal({ left: ['currencyPair'], right: [], }); - act(() => apiRef.current.pinColumn('price17M', GridPinnedPosition.right)); + act(() => apiRef.current.pinColumn('price17M', GridPinnedColumnPosition.RIGHT)); expect(handlePinnedColumnsChange.lastCall.args[0]).to.deep.equal({ left: ['currencyPair'], right: ['price17M'], }); }); - it('should not change the pinned columns when it is called', () => { + it('should not change the pinned columns when it is called', async () => { const handlePinnedColumnsChange = spy(); render( - Column pinning', () => { onPinnedColumnsChange={handlePinnedColumnsChange} />, ); - expect( - document.querySelectorAll(`.${gridClasses['pinnedColumns--left']} [role="cell"]`), - ).to.have.length(1); - act(() => apiRef.current.pinColumn('price17M', GridPinnedPosition.left)); - expect( - document.querySelectorAll(`.${gridClasses['pinnedColumns--left']} [role="cell"]`), - ).to.have.length(1); + expect($$(`[role="cell"].${gridClasses['cell--pinnedLeft']}`)).to.have.length(1); + act(() => apiRef.current.pinColumn('price17M', GridPinnedColumnPosition.LEFT)); + await microtasks(); + expect($$(`[role="cell"].${gridClasses['cell--pinnedLeft']}`)).to.have.length(1); expect(handlePinnedColumnsChange.lastCall.args[0]).to.deep.equal({ left: ['currencyPair', 'price17M'], right: [], @@ -471,34 +265,32 @@ describe(' - Column pinning', () => { describe('prop: pinnedColumns', () => { it('should pin the columns specified', () => { render(); - const leftColumns = document.querySelector( - `.${gridClasses['pinnedColumns--left']}`, + const cell = document.querySelector( + `.${gridClasses['cell--pinnedLeft']}[data-field="currencyPair"]`, )!; - expect(leftColumns.querySelector('[data-field="currencyPair"]')).not.to.equal(null); + expect(cell).not.to.equal(null); }); it("should not change the pinned columns if the prop didn't change", () => { render(); expect( - document.querySelector( - `.${gridClasses['pinnedColumns--left']} [data-field="currencyPair"]`, - ), + document.querySelector(`.${gridClasses['cell--pinnedLeft']}[data-field="currencyPair"]`), ).not.to.equal(null); - act(() => apiRef.current.pinColumn('price17M', GridPinnedPosition.left)); + act(() => apiRef.current.pinColumn('price17M', GridPinnedColumnPosition.LEFT)); expect( - document.querySelector( - `.${gridClasses['pinnedColumns--left']} [data-field="currencyPair"]`, - ), + document.querySelector(`.${gridClasses['cell--pinnedLeft']}[data-field="currencyPair"]`), ).not.to.equal(null); }); it('should filter our duplicated columns', () => { render(); - const leftColumns = document.querySelector( - `.${gridClasses['pinnedColumns--left']}`, + const cell = document.querySelector( + `.${gridClasses['cell--pinnedLeft']}[data-field="currencyPair"]`, )!; - expect(leftColumns.querySelector('[data-field="currencyPair"]')).not.to.equal(null); - expect(document.querySelector(`.${gridClasses['pinnedColumns--right']}`)).to.equal(null); + expect(cell).not.to.equal(null); + expect( + document.querySelector(`.${gridClasses['cell--pinnedRight']}[data-field="currencyPair"]`), + ).to.equal(null); }); }); @@ -514,7 +306,7 @@ describe(' - Column pinning', () => { it('should throw an error when calling `apiRef.current.pinColumn`', () => { render(); - expect(() => apiRef.current.pinColumn('id', GridPinnedPosition.left)).to.throw(); + expect(() => apiRef.current.pinColumn('id', GridPinnedColumnPosition.LEFT)).to.throw(); }); it('should throw an error when calling `apiRef.current.unpinColumn`', () => { @@ -541,30 +333,16 @@ describe(' - Column pinning', () => { describe('apiRef', () => { it('should reorder the columns to render the left pinned columns before all other columns', () => { render(); - const leftColumns = document.querySelector( - `.${gridClasses['pinnedColumns--left']}`, - )!; - const renderZone = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone}`, - )!; - expect(leftColumns.querySelector('[data-field="currencyPair"]')).not.to.equal(null); - expect(leftColumns.querySelector('[data-field="price1M"]')).not.to.equal(null); - expect(renderZone.querySelector('[data-field="currencyPair"]')).to.equal(null); - expect(renderZone.querySelector('[data-field="price1M"]')).to.equal(null); + expect($(`.${gridClasses['cell--pinnedLeft']}[data-field="currencyPair"]`)).not.to.equal( + null, + ); + expect($(`.${gridClasses['cell--pinnedLeft']}[data-field="price1M"]`)).not.to.equal(null); }); it('should reorder the columns to render the right pinned columns after all other columns', () => { render(); - const rightColumns = document.querySelector( - `.${gridClasses['pinnedColumns--right']}`, - )!; - const renderZone = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone}`, - )!; - expect(rightColumns.querySelector('[data-field="price16M"]')).not.to.equal(null); - expect(rightColumns.querySelector('[data-field="price17M"]')).not.to.equal(null); - expect(renderZone.querySelector('[data-field="price16M"]')).to.equal(null); - expect(renderZone.querySelector('[data-field="price17M"]')).to.equal(null); + expect($(`.${gridClasses['cell--pinnedRight']}[data-field="price16M"]`)).not.to.equal(null); + expect($(`.${gridClasses['cell--pinnedRight']}[data-field="price17M"]`)).not.to.equal(null); }); it('should not crash if a non-existent column is pinned', () => { @@ -577,63 +355,50 @@ describe(' - Column pinning', () => { describe('pinColumn', () => { it('should pin the given column', () => { render(); - const renderZone = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone}`, - )!; - expect(renderZone.querySelector('[data-field="currencyPair"]')).not.to.equal(null); - act(() => apiRef.current.pinColumn('currencyPair', GridPinnedPosition.left)); - const leftColumns = document.querySelector( - `.${gridClasses['pinnedColumns--left']}`, - )!; - expect(leftColumns.querySelector('[data-field="currencyPair"]')).not.to.equal(null); - expect(renderZone.querySelector('[data-field="currencyPair"]')).to.equal(null); + expect($('[data-field="currencyPair"]')?.className).not.to.include('pinned'); + act(() => apiRef.current.pinColumn('currencyPair', GridPinnedColumnPosition.LEFT)); + expect($(`.${gridClasses['cell--pinnedLeft']}[data-field="currencyPair"]`)).not.to.equal( + null, + ); }); it('should change the side when called on a pinned column', () => { render(); - const renderZone = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone}`, - )!; - expect(renderZone.querySelector('[data-field="currencyPair"]')).not.to.equal(null); - expect(renderZone.querySelector('[data-field="currencyPair"]')).not.to.equal(null); - act(() => apiRef.current.pinColumn('currencyPair', GridPinnedPosition.left)); - const leftColumns = document.querySelector( - `.${gridClasses['pinnedColumns--left']}`, - )!; - expect(leftColumns.querySelector('[data-field="currencyPair"]')).not.to.equal(null); - expect(renderZone.querySelector('[data-field="currencyPair"]')).to.equal(null); - - act(() => apiRef.current.pinColumn('currencyPair', GridPinnedPosition.right)); - const rightColumns = document.querySelector( - `.${gridClasses['pinnedColumns--right']}`, - )!; - expect(document.querySelector(`.${gridClasses['pinnedColumns--left']}`)).to.equal(null); - expect(rightColumns.querySelector('[data-field="currencyPair"]')).not.to.equal(null); + const renderZone = $(`.${gridClasses.virtualScrollerRenderZone}`)!; + + expect($(renderZone, '[data-field="currencyPair"]')!.className).not.to.include('pinned'); + + act(() => apiRef.current.pinColumn('currencyPair', GridPinnedColumnPosition.LEFT)); + expect( + $(renderZone, `.${gridClasses['cell--pinnedLeft']}[data-field="currencyPair"]`), + ).not.to.equal(null); + expect($(renderZone, '[data-field="currencyPair"]')!.className).to.include('pinned'); + + act(() => apiRef.current.pinColumn('currencyPair', GridPinnedColumnPosition.RIGHT)); + expect($$(renderZone, `.${gridClasses['cell--pinnedLeft']}`).length).to.equal(0); + expect( + $(renderZone, `.${gridClasses['cell--pinnedRight']}[data-field="currencyPair"]`), + ).not.to.equal(null); }); it('should not change the columns when called on a pinned column with the same side ', () => { render(); - act(() => apiRef.current.pinColumn('currencyPair', GridPinnedPosition.left)); - const leftColumns = document.querySelector( - `.${gridClasses['pinnedColumns--left']}`, - )!; - expect(leftColumns.querySelector('[data-id="0"]')?.children).to.have.length(1); - act(() => apiRef.current.pinColumn('currencyPair', GridPinnedPosition.left)); - expect(leftColumns.querySelector('[data-id="0"]')?.children).to.have.length(1); + act(() => apiRef.current.pinColumn('currencyPair', GridPinnedColumnPosition.LEFT)); + expect($$(`.${gridClasses['cell--pinnedLeft']}`)).to.have.length(1); + act(() => apiRef.current.pinColumn('currencyPair', GridPinnedColumnPosition.LEFT)); + expect($$(`.${gridClasses['cell--pinnedLeft']}`)).to.have.length(1); }); }); describe('unpinColumn', () => { it('should unpin the given column', () => { render(); - act(() => apiRef.current.pinColumn('currencyPair', GridPinnedPosition.left)); - expect(document.querySelector(`.${gridClasses['pinnedColumns--left']}`)).not.to.equal(null); + act(() => apiRef.current.pinColumn('currencyPair', GridPinnedColumnPosition.LEFT)); + expect($$(`.${gridClasses['cell--pinnedLeft']}`).length).not.to.equal(0); act(() => apiRef.current.unpinColumn('currencyPair')); - expect(document.querySelector(`.${gridClasses['pinnedColumns--left']}`)).to.equal(null); - const renderZone = document.querySelector( - `.${gridClasses.virtualScrollerRenderZone}`, - )!; + expect($$(`.${gridClasses['cell--pinnedLeft']}`).length).to.equal(0); + const renderZone = $(`.${gridClasses.virtualScrollerRenderZone}`)!; expect(renderZone.querySelector('[data-field="currencyPair"]')).not.to.equal(null); }); }); @@ -643,8 +408,8 @@ describe(' - Column pinning', () => { render( , ); - expect(apiRef.current.isColumnPinned('id')).to.equal(GridPinnedPosition.left); - expect(apiRef.current.isColumnPinned('price16M')).to.equal(GridPinnedPosition.right); + expect(apiRef.current.isColumnPinned('id')).to.equal(GridPinnedColumnPosition.LEFT); + expect(apiRef.current.isColumnPinned('price16M')).to.equal(GridPinnedColumnPosition.RIGHT); expect(apiRef.current.isColumnPinned('currencyPair')).to.equal(false); }); }); @@ -664,63 +429,49 @@ describe(' - Column pinning', () => { describe('column menu', () => { it('should pin the column to the left when clicking the "Pin to left" pinning button', () => { render(); - const columnCell = document.querySelector('[role="columnheader"][data-field="id"]')!; + const columnCell = $('[role="columnheader"][data-field="id"]')!; const menuIconButton = columnCell.querySelector('button[aria-label="Menu"]')!; fireEvent.click(menuIconButton); fireEvent.click(screen.getByRole('menuitem', { name: 'Pin to left' })); - expect( - document.querySelector(`.${gridClasses['pinnedColumns--left']} [data-field="id"]`), - ).not.to.equal(null); + expect($(`.${gridClasses['cell--pinnedLeft']}[data-field="id"]`)).not.to.equal(null); }); it('should pin the column to the right when clicking the "Pin to right" pinning button', () => { render(); - const columnCell = document.querySelector('[role="columnheader"][data-field="id"]')!; + const columnCell = $('[role="columnheader"][data-field="id"]')!; const menuIconButton = columnCell.querySelector('button[aria-label="Menu"]')!; fireEvent.click(menuIconButton); fireEvent.click(screen.getByRole('menuitem', { name: 'Pin to right' })); - expect( - document.querySelector(`.${gridClasses['pinnedColumns--right']} [data-field="id"]`), - ).not.to.equal(null); + expect($(`.${gridClasses['cell--pinnedRight']}[data-field="id"]`)).not.to.equal(null); }); it('should allow to invert the side when clicking on "Pin to right" pinning button on a left pinned column', () => { render(); - const columnCell = document.querySelector('[role="columnheader"][data-field="id"]')!; + const columnCell = $('[role="columnheader"][data-field="id"]')!; const menuIconButton = columnCell.querySelector('button[aria-label="Menu"]')!; fireEvent.click(menuIconButton); fireEvent.click(screen.getByRole('menuitem', { name: 'Pin to right' })); - expect( - document.querySelector(`.${gridClasses['pinnedColumns--left']} [data-field="id"]`), - ).to.equal(null); - expect( - document.querySelector(`.${gridClasses['pinnedColumns--right']} [data-field="id"]`), - ).not.to.equal(null); + expect($(`.${gridClasses['cell--pinnedLeft']}[data-field="id"]`)).to.equal(null); + expect($(`.${gridClasses['cell--pinnedRight']}[data-field="id"]`)).not.to.equal(null); }); it('should allow to invert the side when clicking on "Pin to left" pinning button on a right pinned column', () => { render(); - const columnCell = document.querySelector('[role="columnheader"][data-field="id"]')!; + const columnCell = $('[role="columnheader"][data-field="id"]')!; const menuIconButton = columnCell.querySelector('button[aria-label="Menu"]')!; fireEvent.click(menuIconButton); fireEvent.click(screen.getByRole('menuitem', { name: 'Pin to left' })); - expect( - document.querySelector(`.${gridClasses['pinnedColumns--right']} [data-field="id"]`), - ).to.equal(null); - expect( - document.querySelector(`.${gridClasses['pinnedColumns--left']} [data-field="id"]`), - ).not.to.equal(null); + expect($(`.${gridClasses['cell--pinnedRight']}[data-field="id"]`)).to.equal(null); + expect($(`.${gridClasses['cell--pinnedLeft']}[data-field="id"]`)).not.to.equal(null); }); it('should allow to unpin a pinned left column when clicking "Unpin" pinning button', () => { render(); - const columnCell = document.querySelector('[role="columnheader"][data-field="id"]')!; + const columnCell = $('[role="columnheader"][data-field="id"]')!; const menuIconButton = columnCell.querySelector('button[aria-label="Menu"]')!; fireEvent.click(menuIconButton); fireEvent.click(screen.getByRole('menuitem', { name: 'Unpin' })); - expect( - document.querySelector(`.${gridClasses['pinnedColumns--left']} [data-field="id"]`), - ).to.equal(null); + expect($(`.${gridClasses['cell--pinnedLeft']}[data-field="id"]`)).to.equal(null); }); it('should not render menu items if the column has `pinnable` equals to false', () => { diff --git a/packages/grid/x-data-grid-pro/src/tests/columns.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/columns.DataGridPro.test.tsx index a4226ca77e1e..0f340451c309 100644 --- a/packages/grid/x-data-grid-pro/src/tests/columns.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/columns.DataGridPro.test.tsx @@ -99,7 +99,7 @@ describe(' - Columns', () => { fireEvent.mouseMove(separator, { clientX: 110, buttons: 1 }); fireEvent.mouseUp(separator); expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '110px' }); - expect(getCell(1, 0)).toHaveInlineStyle({ width: '110px' }); + expect(getCell(1, 0).getBoundingClientRect().width).to.equal(110); }); it('should allow to resize columns with the touch', function test() { @@ -120,7 +120,7 @@ describe(' - Columns', () => { changedTouches: [new Touch({ identifier: now, target: separator, clientX: 110 })], }); expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '110px' }); - expect(getCell(1, 0)).toHaveInlineStyle({ width: '110px' }); + expect(getCell(1, 0).getBoundingClientRect().width).to.equal(110); }); it('should call onColumnResize during resizing', () => { @@ -172,7 +172,7 @@ describe(' - Columns', () => { fireEvent.mouseMove(separator, { clientX: 110, buttons: 1 }); fireEvent.mouseUp(separator); expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '110px' }); - expect(getCell(0, 0)).toHaveInlineStyle({ width: '110px' }); + expect(getCell(0, 0).getBoundingClientRect().width).to.equal(110); expect(screen.getByTestId('dummy-row').firstElementChild).toHaveInlineStyle({ width: '90px', }); @@ -201,15 +201,15 @@ describe(' - Columns', () => { fireEvent.mouseDown(separator, { clientX: 100 }); fireEvent.mouseMove(separator, { clientX: 150, buttons: 1 }); - expect(columnHeaderCell).toHaveInlineStyle({ width: '150px' }); - expect(nonPinnedCell).toHaveInlineStyle({ width: '150px' }); + expect(columnHeaderCell.getBoundingClientRect().width).to.equal(150); + expect(nonPinnedCell.getBoundingClientRect().width).to.equal(150); expect(topPinnedRowCell?.getBoundingClientRect().width).to.equal(150); expect(bottomPinnedRowCell?.getBoundingClientRect().width).to.equal(150); fireEvent.mouseUp(separator); - expect(columnHeaderCell).toHaveInlineStyle({ width: '150px' }); - expect(nonPinnedCell).toHaveInlineStyle({ width: '150px' }); + expect(columnHeaderCell.getBoundingClientRect().width).to.equal(150); + expect(nonPinnedCell.getBoundingClientRect().width).to.equal(150); expect(topPinnedRowCell?.getBoundingClientRect().width).to.equal(150); expect(bottomPinnedRowCell?.getBoundingClientRect().width).to.equal(150); }); diff --git a/packages/grid/x-data-grid-pro/src/tests/detailPanel.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/detailPanel.DataGridPro.test.tsx index 707482b36a1d..511e024d7b53 100644 --- a/packages/grid/x-data-grid-pro/src/tests/detailPanel.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/detailPanel.DataGridPro.test.tsx @@ -19,7 +19,7 @@ import { act, userEvent, } from '@mui-internal/test-utils'; -import { getRow, getCell, getColumnValues, getRows } from 'test/utils/helperFn'; +import { $, $$, grid, getRow, getCell, getColumnValues, microtasks } from 'test/utils/helperFn'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); @@ -38,16 +38,6 @@ describe(' - Detail panel', () => { ); } - it('should add a bottom margin to the expanded row', function test() { - if (isJSDOM) { - this.skip(); // Doesn't work with mocked window.getComputedStyle - } - - render( (id === 0 ?
: null)} />); - fireEvent.click(screen.getAllByRole('button', { name: 'Expand' })[0]); - expect(getRow(0)).toHaveComputedStyle({ marginBottom: '500px' }); - }); - it('should not allow to expand rows that do not specify a detail element', function test() { if (isJSDOM) { this.skip(); // Needs layout @@ -85,29 +75,6 @@ describe(' - Detail panel', () => { expect(getColumnValues(1)[0]).to.equal('2'); // If there was no expanded row, the first rendered would be 5 }); - it('should only render detail panels for the rows that are rendered', function test() { - if (isJSDOM) { - this.skip(); // Needs layout - } - render( - 50} - getDetailPanelContent={() =>
} - rowBuffer={0} - rowThreshold={0} - nbRows={10} - initialState={{ - detailPanel: { - expandedRowIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - }, - }} - />, - ); - const rows = getRows(); - const detailPanels = document.querySelectorAll('.MuiDataGrid-detailPanel'); - expect(detailPanels.length).to.equal(rows.length); - }); - it('should derive the height from the content if getDetailPanelHeight returns "auto"', async function test() { if (isJSDOM) { this.skip(); // Needs layout @@ -123,20 +90,16 @@ describe(' - Detail panel', () => { />, ); fireEvent.click(screen.getAllByRole('button', { name: 'Expand' })[0]); + await microtasks(); - await waitFor(() => { - expect(getRow(0)).toHaveComputedStyle({ marginBottom: `${detailPanelHeight}px` }); - }); - - const virtualScrollerContent = document.querySelector('.MuiDataGrid-virtualScrollerContent')!; + const virtualScrollerContent = $('.MuiDataGrid-virtualScrollerContent')!; expect(virtualScrollerContent).toHaveInlineStyle({ width: 'auto', height: `${rowHeight + detailPanelHeight}px`, }); - const detailPanels = document.querySelector('.MuiDataGrid-detailPanels'); - expect(detailPanels!.children[0]).toHaveComputedStyle({ - top: `${rowHeight}px`, + const detailPanels = $$('.MuiDataGrid-detailPanel'); + expect(detailPanels[0]).toHaveComputedStyle({ height: `${detailPanelHeight}px`, }); }); @@ -164,37 +127,35 @@ describe(' - Detail panel', () => { getDetailPanelHeight={() => 'auto'} />, ); - const virtualScrollerContent = document.querySelector('.MuiDataGrid-virtualScrollerContent')!; + const virtualScrollerContent = grid('virtualScrollerContent')!; fireEvent.click(screen.getByRole('button', { name: 'Expand' })); await waitFor(() => { - expect(getRow(0)).toHaveComputedStyle({ marginBottom: '100px' }); + expect(getRow(0).className).to.include(gridClasses['row--detailPanelExpanded']); }); - expect(virtualScrollerContent).toHaveInlineStyle({ - width: 'auto', - height: `${rowHeight + 100}px`, + await waitFor(() => { + expect(virtualScrollerContent).toHaveInlineStyle({ + width: 'auto', + height: `${rowHeight + 100}px`, + }); }); - const detailPanels = document.querySelector('.MuiDataGrid-detailPanels'); - expect(detailPanels!.children[0]).toHaveComputedStyle({ - top: `${rowHeight}px`, + const detailPanels = $$('.MuiDataGrid-detailPanel'); + expect(detailPanels[0]).toHaveComputedStyle({ height: `100px`, }); fireEvent.click(screen.getByRole('button', { name: 'Increase' })); await waitFor(() => { - expect(getRow(0)).toHaveComputedStyle({ marginBottom: '200px' }); - }); - - expect(virtualScrollerContent).toHaveInlineStyle({ - width: 'auto', - height: `${rowHeight + 200}px`, + expect(virtualScrollerContent).toHaveInlineStyle({ + width: 'auto', + height: `${rowHeight + 200}px`, + }); }); - expect(detailPanels!.children[0]).toHaveComputedStyle({ - top: `${rowHeight}px`, + expect(detailPanels[0]).toHaveComputedStyle({ height: `200px`, }); }); @@ -221,13 +182,11 @@ describe(' - Detail panel', () => { }} />, ); - const detailPanels = document.querySelector('.MuiDataGrid-detailPanels'); - expect(detailPanels!.children[0]).toHaveComputedStyle({ - top: `${rowHeight}px`, + const detailPanels = $$('.MuiDataGrid-detailPanel'); + expect(detailPanels[0]).toHaveComputedStyle({ height: `${evenHeight}px`, }); - expect(detailPanels!.children[1]).toHaveComputedStyle({ - top: `${rowHeight + evenHeight + rowHeight}px`, + expect(detailPanels[1]).toHaveComputedStyle({ height: `${oddHeight}px`, }); }); @@ -436,15 +395,16 @@ describe(' - Detail panel', () => { ); fireEvent.click(screen.getByRole('button', { name: 'Expand' })); - const detailPanel = document.querySelector('.MuiDataGrid-detailPanels')!.firstChild; - expect(detailPanel).toHaveComputedStyle({ top: `52px`, height: `100px` }); - const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; - expect(virtualScroller.scrollHeight).to.equal(100 + 52); + const detailPanel = $$('.MuiDataGrid-detailPanel')[0]; + expect(detailPanel).toHaveComputedStyle({ height: '100px' }); + const virtualScroller = grid('virtualScroller')!; + expect(virtualScroller.scrollHeight).to.equal(208); const getDetailPanelHeight2 = spy(() => 200); setProps({ getDetailPanelHeight: getDetailPanelHeight2 }); - expect(detailPanel).toHaveComputedStyle({ top: `52px`, height: `200px` }); - expect(virtualScroller.scrollHeight).to.equal(200 + 52); + + expect(detailPanel).toHaveComputedStyle({ height: '200px' }); + expect(virtualScroller.scrollHeight).to.equal(200 + 52 + 56); }); it('should only call getDetailPanelHeight on the rows that have detail content', () => { @@ -523,7 +483,7 @@ describe(' - Detail panel', () => { />, ); fireEvent.click(screen.getAllByRole('button', { name: 'Expand' })[0]); - expect(getRow(0)).toHaveComputedStyle({ marginBottom: '502px' }); // 500px + 2px spacing + expect(getRow(0)).toHaveComputedStyle({ marginBottom: '2px' }); }); it('should not reuse detail panel components', () => { @@ -665,7 +625,7 @@ describe(' - Detail panel', () => { }); describe('setExpandedDetailPanels', () => { - it('should update which detail panels are open', () => { + it('should update which detail panels are open', async () => { render(
Row {id}
} @@ -701,7 +661,6 @@ describe(' - Detail panel', () => { fireEvent.click(screen.getByRole('button', { name: 'Expand' })); expect(getRow(0)).toHaveInlineStyle({ color: 'yellow', - marginBottom: '0px', // added when expanded }); }); }); diff --git a/packages/grid/x-data-grid-pro/src/tests/rowPinning.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/rowPinning.DataGridPro.test.tsx index 878eb683e1f4..86ba8435fc92 100644 --- a/packages/grid/x-data-grid-pro/src/tests/rowPinning.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/rowPinning.DataGridPro.test.tsx @@ -20,12 +20,15 @@ import { waitFor, } from '@mui-internal/test-utils'; import { + $, + grid, getActiveCell, getActiveColumnHeader, getCell, getColumnHeaderCell, getColumnValues, getRows, + microtasks, } from 'test/utils/helperFn'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); @@ -34,19 +37,11 @@ describe(' - Row pinning', () => { const { render } = createRenderer(); function getRowById(id: number | string) { - return document.querySelector(`[data-id="${id}"]`); - } - - function getTopPinnedRowsContainer() { - return document.querySelector(`.${gridClasses['pinnedRows--top']}`); - } - function getBottomPinnedRowsContainer() { - return document.querySelector(`.${gridClasses['pinnedRows--bottom']}`); + return $(`[data-id="${id}"]`); } function isRowPinned(row: Element | null, section: 'top' | 'bottom') { - const container = - section === 'top' ? getTopPinnedRowsContainer() : getBottomPinnedRowsContainer(); + const container = section === 'top' ? grid('pinnedRows--top') : grid('pinnedRows--bottom'); if (!row || !container) { return false; } @@ -506,27 +501,34 @@ describe(' - Row pinning', () => { this.skip(); } - render( - { - if (row.id === 0) { - return 100; - } - if (row.id === 1) { - return 20; - } - return undefined; - }} - />, - ); + let apiRef!: React.MutableRefObject; + function TestCase() { + apiRef = useGridApiRef(); + return ( + { + if (row.id === 0) { + return 100; + } + if (row.id === 1) { + return 20; + } + return undefined; + }} + /> + ); + } + + render(); expect(getRowById(0)?.clientHeight).to.equal(100); expect(getRowById(1)?.clientHeight).to.equal(20); }); - it('should always update on `rowHeight` change', function test() { + it('should always update on `rowHeight` change', async function test() { if (isJSDOM) { // Need layouting this.skip(); @@ -534,27 +536,33 @@ describe(' - Row pinning', () => { const defaultRowHeight = 52; - const { setProps } = render( - , - ); + let apiRef!: React.MutableRefObject; + function TestCase({ rowHeight }: { rowHeight?: number }) { + apiRef = useGridApiRef(); + return ( + + ); + } - expect(getRowById(0)?.clientHeight).to.equal(defaultRowHeight); - expect(document.querySelector(`.${gridClasses['pinnedRows--top']}`)?.clientHeight).to.equal( - defaultRowHeight, - ); - expect(getRowById(1)?.clientHeight).to.equal(defaultRowHeight); - expect(document.querySelector(`.${gridClasses['pinnedRows--bottom']}`)?.clientHeight).to.equal( - defaultRowHeight, - ); + const { setProps } = render(); + await microtasks(); + + expect(getRowById(0)!.offsetHeight).to.equal(defaultRowHeight); + expect(grid('pinnedRows--top')!.offsetHeight).to.equal(defaultRowHeight); + expect(getRowById(1)!.clientHeight).to.equal(defaultRowHeight); + expect(grid('pinnedRows--bottom')!.offsetHeight).to.equal(defaultRowHeight); setProps({ rowHeight: 36 }); expect(getRowById(0)?.clientHeight).to.equal(36); - expect(document.querySelector(`.${gridClasses['pinnedRows--top']}`)?.clientHeight).to.equal(36); + expect(grid('pinnedRows--top')!.offsetHeight).to.equal(36); expect(getRowById(1)?.clientHeight).to.equal(36); - expect(document.querySelector(`.${gridClasses['pinnedRows--bottom']}`)?.clientHeight).to.equal( - 36, - ); + expect(grid('pinnedRows--bottom')!.offsetHeight).to.equal(36); }); it('should work with `autoHeight`', function test() { @@ -578,9 +586,7 @@ describe(' - Row pinning', () => { />, ); - expect(document.querySelector(`.${gridClasses.main}`)!.clientHeight).to.equal( - columnHeaderHeight + rowHeight * rowCount, - ); + expect(grid('main')!.clientHeight).to.equal(columnHeaderHeight + rowHeight * rowCount); }); it('should work with `autoPageSize`', function test() { diff --git a/packages/grid/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx index 8f3d464f6356..580356d0c1e5 100644 --- a/packages/grid/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx @@ -3,6 +3,8 @@ import { createRenderer, fireEvent, act, userEvent } from '@mui-internal/test-ut import { spy } from 'sinon'; import { expect } from 'chai'; import { + $, + grid, getCell, getRow, getColumnValues, @@ -442,23 +444,23 @@ describe(' - Rows', () => { />, ); - const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; - const renderingZone = document.querySelector( - '.MuiDataGrid-virtualScrollerRenderZone', - )!; + const root = grid('root')!; + const virtualScroller = grid('virtualScroller')!; + const renderingZone = grid('virtualScrollerRenderZone')!; virtualScroller.scrollTop = 10e6; // scroll to the bottom act(() => virtualScroller.dispatchEvent(new Event('scroll'))); - const lastCell = document.querySelector('[role="row"]:last-child [role="cell"]:first-child')!; + const lastCell = $('[role="row"]:last-child [role="cell"]:first-child')!; expect(lastCell).to.have.text('995'); expect(renderingZone.children.length).to.equal( Math.floor((height - 1) / rowHeight) + rowBuffer, ); // Subtracting 1 is needed because of the column header borders + const scrollbarSize = apiRef.current.state.dimensions.scrollbarSize; const distanceToFirstRow = (nbRows - renderingZone.children.length) * rowHeight; - expect(renderingZone.style.transform).to.equal( - `translate3d(0px, ${distanceToFirstRow}px, 0px)`, - ); - expect(virtualScroller.scrollHeight).to.equal(nbRows * rowHeight); + const styles = getComputedStyle(root); + const offsetTop = parseInt(styles.getPropertyValue('--DataGrid-offsetTop'), 10); + expect(offsetTop).to.equal(distanceToFirstRow); + expect(virtualScroller.scrollHeight - scrollbarSize).to.equal(nbRows * rowHeight); }); it('should have all the rows rendered of the page in the DOM when autoPageSize: true', () => { @@ -473,17 +475,23 @@ describe(' - Rows', () => { it('should render extra columns when the columnBuffer prop is present', () => { const border = 1; - const width = 300 + border * 2; + const width = 300; const columnBuffer = 2; const columnWidth = 100; - render(); + render( + , + ); const firstRow = getRow(0); expect(firstRow.children).to.have.length(Math.floor(width / columnWidth) + columnBuffer); const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; virtualScroller.scrollLeft = 301; act(() => virtualScroller.dispatchEvent(new Event('scroll'))); expect(firstRow.children).to.have.length( - columnBuffer + Math.floor(width / columnWidth) + columnBuffer, + columnBuffer + 1 + Math.floor(width / columnWidth) + columnBuffer, ); }); @@ -537,11 +545,14 @@ describe(' - Rows', () => { virtualScroller.scrollTop = 10e6; // scroll to the bottom act(() => virtualScroller.dispatchEvent(new Event('scroll'))); + const dimensions = apiRef.current.state.dimensions; const lastCell = document.querySelector( '[role="row"]:last-child [role="cell"]:first-child', )!; expect(lastCell).to.have.text('31'); - expect(virtualScroller.scrollHeight).to.equal(nbRows * rowHeight); + expect(virtualScroller.scrollHeight).to.equal( + dimensions.headerHeight + nbRows * rowHeight + dimensions.scrollbarSize, + ); }); it('should not virtualized the last page if smaller than viewport', () => { diff --git a/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts b/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts index 31fee75182bf..a2fcde4f2366 100644 --- a/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts +++ b/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts @@ -1,14 +1,11 @@ -import { GridRowId } from '@mui/x-data-grid'; +import { GridRowId, GridPinnedColumnFields } from '@mui/x-data-grid'; import type { GridRowScrollEndParams, GridRowOrderChangeParams, GridFetchRowsParams, } from '../models'; import type { GridHeaderFilterCellProps } from '../components/headerFiltering/GridHeaderFilterCell'; -import type { - GridColumnPinningInternalCache, - GridPinnedColumns, -} from '../hooks/features/columnPinning/gridColumnPinningInterface'; +import type { GridColumnPinningInternalCache } from '../hooks/features/columnPinning/gridColumnPinningInterface'; import type { GridCanBeReorderedPreProcessingContext } from '../hooks/features/columnReorder/columnReorderInterfaces'; import { GridRowPinningInternalCache } from '../hooks/features/rowPinning/gridRowPinningInterface'; @@ -31,7 +28,7 @@ export interface GridControlledStateEventLookupPro { * Fired when the pinned columns is changed. * @ignore - do not document. */ - pinnedColumnsChange: { params: GridPinnedColumns }; + pinnedColumnsChange: { params: GridPinnedColumnFields }; } export interface GridEventLookupPro { diff --git a/packages/grid/x-data-grid-pro/src/utils/domUtils.ts b/packages/grid/x-data-grid-pro/src/utils/domUtils.ts index 2fdfb674da76..febdbb6e505a 100644 --- a/packages/grid/x-data-grid-pro/src/utils/domUtils.ts +++ b/packages/grid/x-data-grid-pro/src/utils/domUtils.ts @@ -6,8 +6,8 @@ export function getFieldFromHeaderElem(colCellEl: Element): string { return colCellEl.getAttribute('data-field')!; } -export function findHeaderElementFromField(elem: Element, field: string): Element | null { - return elem.querySelector(`[data-field="${field}"]`); +export function findHeaderElementFromField(elem: Element, field: string): HTMLDivElement { + return elem.querySelector(`[data-field="${field}"]`)!; } export function findGroupHeaderElementsFromField(elem: Element, field: string): Element[] { @@ -32,11 +32,7 @@ export function findGridCellElementsFromCol(col: HTMLElement, api: GridPrivateAp return []; } - const renderedRowElements = api.virtualScrollerRef?.current.querySelectorAll( - `:scope > div > div > .${gridClasses.row}`, // Use > to ignore rows from nested data grids (e.g. in detail panel) - ); - - renderedRowElements.forEach((rowElement) => { + queryRows(api).forEach((rowElement) => { const rowId = rowElement.getAttribute('data-id'); if (!rowId) { return; @@ -57,6 +53,62 @@ export function findGridCellElementsFromCol(col: HTMLElement, api: GridPrivateAp return cells; } +export function findGridElement(api: GridPrivateApiPro, klass: keyof typeof gridClasses) { + return api.rootElementRef.current!.querySelector(`.${gridClasses[klass]}`)! as HTMLElement; +} + +export function findLeftPinnedCellsAfterCol(api: GridPrivateApiPro, col: HTMLElement) { + const colIndex = parseCellColIndex(col); + if (colIndex === null) { + return []; + } + + const cells: HTMLElement[] = []; + + queryRows(api).forEach((rowElement) => { + const rowId = rowElement.getAttribute('data-id'); + if (!rowId) { + return; + } + + const rightPinnedCells = rowElement.querySelectorAll(`.${gridClasses['cell--pinnedLeft']}`); + rightPinnedCells.forEach((cell) => { + const currentColIndex = parseCellColIndex(cell); + if (currentColIndex !== null && currentColIndex > colIndex) { + cells.push(cell as HTMLElement); + } + }); + }); + + return cells; +} + +export function findRightPinnedCellsBeforeCol(api: GridPrivateApiPro, col: HTMLElement) { + const colIndex = parseCellColIndex(col); + if (colIndex === null) { + return []; + } + + const cells: HTMLElement[] = []; + + queryRows(api).forEach((rowElement) => { + const rowId = rowElement.getAttribute('data-id'); + if (!rowId) { + return; + } + + const rightPinnedCells = rowElement.querySelectorAll(`.${gridClasses['cell--pinnedRight']}`); + rightPinnedCells.forEach((cell) => { + const currentColIndex = parseCellColIndex(cell); + if (currentColIndex !== null && currentColIndex < colIndex) { + cells.push(cell as HTMLElement); + } + }); + }); + + return cells; +} + export function findGridHeader(api: GridPrivateApiPro, field: string) { const headers = api.columnHeadersContainerElementRef!.current!; return headers.querySelector(`:scope > div > div > [data-field="${field}"][role="columnheader"]`); @@ -71,3 +123,18 @@ export function findGridCells(api: GridPrivateApiPro, field: string) { container.querySelectorAll(`${selectorFor('cell')}, ${selectorFor('gridcell')}`), ); } + +function queryRows(api: GridPrivateApiPro) { + return api.virtualScrollerRef.current!.querySelectorAll( + // Use > to ignore rows from nested data grids (e.g. in detail panel) + `:scope > div > div > .${gridClasses.row}`, + ); +} + +function parseCellColIndex(col: Element) { + const ariaColIndex = col.getAttribute('aria-colindex'); + if (!ariaColIndex) { + return null; + } + return Number(ariaColIndex) - 1; +} diff --git a/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx index e14046173bdf..5030d96c24a5 100644 --- a/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx +++ b/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx @@ -5,7 +5,6 @@ import { DataGridProcessedProps, DataGridProps } from '../models/props/DataGridP import { GridContextProvider } from '../context/GridContextProvider'; import { useDataGridComponent } from './useDataGridComponent'; import { useDataGridProps } from './useDataGridProps'; -import { DataGridVirtualScroller } from '../components/DataGridVirtualScroller'; import { GridValidRowModel } from '../models/gridRows'; import { PropValidator, @@ -47,7 +46,7 @@ const DataGridRaw = React.forwardRef(function DataGrid - + diff --git a/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx b/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx index 17d5b9e59327..411f9d82b41c 100644 --- a/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx +++ b/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx @@ -34,7 +34,10 @@ import { useGridRowSelectionPreProcessors } from '../hooks/features/rowSelection import { useGridSorting, sortingStateInitializer } from '../hooks/features/sorting/useGridSorting'; import { useGridScroll } from '../hooks/features/scroll/useGridScroll'; import { useGridEvents } from '../hooks/features/events/useGridEvents'; -import { useGridDimensions } from '../hooks/features/dimensions/useGridDimensions'; +import { + dimensionsStateInitializer, + useGridDimensions, +} from '../hooks/features/dimensions/useGridDimensions'; import { rowsMetaStateInitializer, useGridRowsMeta } from '../hooks/features/rows/useGridRowsMeta'; import { useGridStatePersistence } from '../hooks/features/statePersistence/useGridStatePersistence'; import { useGridColumnSpanning } from '../hooks/features/columns/useGridColumnSpanning'; @@ -65,6 +68,7 @@ export const useDataGridComponent = ( /** * Register all state initializers here. */ + useGridInitializeState(dimensionsStateInitializer, apiRef, props); useGridInitializeState(rowSelectionStateInitializer, apiRef, props); useGridInitializeState(columnsStateInitializer, apiRef, props); useGridInitializeState(rowsStateInitializer, apiRef, props); diff --git a/packages/grid/x-data-grid/src/components/DataGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/components/DataGridVirtualScroller.tsx deleted file mode 100644 index e10e75dd2724..000000000000 --- a/packages/grid/x-data-grid/src/components/DataGridVirtualScroller.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import { GridVirtualScroller } from './virtualization/GridVirtualScroller'; -import { GridVirtualScrollerContent } from './virtualization/GridVirtualScrollerContent'; -import { GridVirtualScrollerRenderZone } from './virtualization/GridVirtualScrollerRenderZone'; -import { useGridVirtualScroller } from '../hooks/features/virtualization/useGridVirtualScroller'; -import { GridOverlays } from './base/GridOverlays'; - -const DataGridVirtualScroller = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(function DataGridVirtualScroller(props, ref) { - const { className, ...other } = props; - - const { getRootProps, getContentProps, getRenderZoneProps, getRows } = useGridVirtualScroller({ - ref, - }); - - return ( - - - - - {getRows()} - - - - ); -}); - -export { DataGridVirtualScroller }; diff --git a/packages/grid/x-data-grid/src/components/GridColumnHeaders.tsx b/packages/grid/x-data-grid/src/components/GridColumnHeaders.tsx index 835d09014138..ae1bd9bbd060 100644 --- a/packages/grid/x-data-grid/src/components/GridColumnHeaders.tsx +++ b/packages/grid/x-data-grid/src/components/GridColumnHeaders.tsx @@ -9,26 +9,25 @@ import { import { GridBaseColumnHeaders } from './columnHeaders/GridBaseColumnHeaders'; import { GridColumnHeadersInner } from './columnHeaders/GridColumnHeadersInner'; -interface GridColumnHeadersProps +export interface GridColumnHeadersProps extends React.HTMLAttributes, Omit { + ref?: React.Ref; innerRef?: React.Ref; } const GridColumnHeaders = React.forwardRef( - function GridColumnsHeaders(props, ref) { + function GridColumnHeaders(props, ref) { const { innerRef, className, visibleColumns, sortColumnLookup, filterColumnLookup, - columnPositions, columnHeaderTabIndexState, columnGroupHeaderTabIndexState, columnHeaderFocus, columnGroupHeaderFocus, - densityFactor, headerGroupingMaxDepth, columnMenuState, columnVisibility, @@ -37,18 +36,16 @@ const GridColumnHeaders = React.forwardRef + {getColumnGroupHeaders()} {getColumnHeaders()} @@ -98,14 +95,11 @@ GridColumnHeaders.propTypes = { field: PropTypes.string, open: PropTypes.bool.isRequired, }).isRequired, - columnPositions: PropTypes.arrayOf(PropTypes.number).isRequired, columnVisibility: PropTypes.object.isRequired, - densityFactor: PropTypes.number.isRequired, filterColumnLookup: PropTypes.object.isRequired, hasOtherElementInTabSequence: PropTypes.bool.isRequired, headerGroupingMaxDepth: PropTypes.number.isRequired, innerRef: refType, - minColumnIndex: PropTypes.number, sortColumnLookup: PropTypes.object.isRequired, visibleColumns: PropTypes.arrayOf(PropTypes.object).isRequired, } as any; diff --git a/packages/grid/x-data-grid/src/components/GridDetailPanels.tsx b/packages/grid/x-data-grid/src/components/GridDetailPanels.tsx new file mode 100644 index 000000000000..54508a64491f --- /dev/null +++ b/packages/grid/x-data-grid/src/components/GridDetailPanels.tsx @@ -0,0 +1,10 @@ +import type { VirtualScroller } from '../hooks/features/virtualization/useGridVirtualScroller'; + +export interface GridDetailPanelsProps { + virtualScroller: VirtualScroller; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function GridDetailPanels(_: GridDetailPanelsProps) { + return null; +} diff --git a/packages/grid/x-data-grid/src/components/GridHeaders.tsx b/packages/grid/x-data-grid/src/components/GridHeaders.tsx new file mode 100644 index 000000000000..d96d146cc547 --- /dev/null +++ b/packages/grid/x-data-grid/src/components/GridHeaders.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { fastMemo } from '../utils/fastMemo'; +import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext'; +import { useGridSelector } from '../hooks/utils/useGridSelector'; +import { useGridRootProps } from '../hooks/utils/useGridRootProps'; +import { + gridColumnVisibilityModelSelector, + gridVisibleColumnDefinitionsSelector, +} from '../hooks/features/columns/gridColumnsSelector'; +import { gridFilterActiveItemsLookupSelector } from '../hooks/features/filter/gridFilterSelector'; +import { gridSortColumnLookupSelector } from '../hooks/features/sorting/gridSortingSelector'; +import { + gridTabIndexColumnHeaderSelector, + gridTabIndexCellSelector, + gridFocusColumnHeaderSelector, + unstable_gridTabIndexColumnGroupHeaderSelector, + unstable_gridFocusColumnGroupHeaderSelector, +} from '../hooks/features/focus/gridFocusStateSelector'; +import { + gridColumnGroupsHeaderMaxDepthSelector, + gridColumnGroupsHeaderStructureSelector, +} from '../hooks/features/columnGrouping/gridColumnGroupsSelector'; +import { gridColumnMenuSelector } from '../hooks/features/columnMenu/columnMenuSelector'; + +function GridHeaders() { + const apiRef = useGridPrivateApiContext(); + const rootProps = useGridRootProps(); + + const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); + const filterColumnLookup = useGridSelector(apiRef, gridFilterActiveItemsLookupSelector); + const sortColumnLookup = useGridSelector(apiRef, gridSortColumnLookupSelector); + const columnHeaderTabIndexState = useGridSelector(apiRef, gridTabIndexColumnHeaderSelector); + const cellTabIndexState = useGridSelector(apiRef, gridTabIndexCellSelector); + const columnGroupHeaderTabIndexState = useGridSelector( + apiRef, + unstable_gridTabIndexColumnGroupHeaderSelector, + ); + + const columnHeaderFocus = useGridSelector(apiRef, gridFocusColumnHeaderSelector); + const columnGroupHeaderFocus = useGridSelector( + apiRef, + unstable_gridFocusColumnGroupHeaderSelector, + ); + + const headerGroupingMaxDepth = useGridSelector(apiRef, gridColumnGroupsHeaderMaxDepthSelector); + + const columnMenuState = useGridSelector(apiRef, gridColumnMenuSelector); + const columnVisibility = useGridSelector(apiRef, gridColumnVisibilityModelSelector); + const columnGroupsHeaderStructure = useGridSelector( + apiRef, + gridColumnGroupsHeaderStructureSelector, + ); + + const hasOtherElementInTabSequence = !( + columnGroupHeaderTabIndexState === null && + columnHeaderTabIndexState === null && + cellTabIndexState === null + ); + + const columnHeadersRef = React.useRef(null); + const columnsContainerRef = React.useRef(null); + + apiRef.current.register('private', { + columnHeadersContainerElementRef: columnsContainerRef, + columnHeadersElementRef: columnHeadersRef, + }); + + return ( + + ); +} + +const MemoizedGridHeaders = fastMemo(GridHeaders); + +export { MemoizedGridHeaders as GridHeaders }; diff --git a/packages/grid/x-data-grid/src/components/GridPinnedRows.tsx b/packages/grid/x-data-grid/src/components/GridPinnedRows.tsx new file mode 100644 index 000000000000..e895f820b3ab --- /dev/null +++ b/packages/grid/x-data-grid/src/components/GridPinnedRows.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import type { VirtualScroller } from '../hooks/features/virtualization/useGridVirtualScroller'; + +export interface GridPinnedRowsProps extends React.HTMLAttributes { + position: 'top' | 'bottom'; + virtualScroller: VirtualScroller; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function GridPinnedRows(_: GridPinnedRowsProps) { + return null; +} diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index fbccb0e9c859..0dc98c5bcaba 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -13,8 +13,9 @@ import { useGridApiContext } from '../hooks/utils/useGridApiContext'; import { getDataGridUtilityClass, gridClasses } from '../constants/gridClasses'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import type { DataGridProcessedProps } from '../models/props/DataGridProps'; -import { GridStateColDef } from '../models/colDef/gridColDef'; -import { gridColumnsTotalWidthSelector } from '../hooks/features/columns/gridColumnsSelector'; +import type { GridPinnedColumns } from '../hooks/features/columns'; +import type { GridStateColDef } from '../models/colDef/gridColDef'; +import { gridColumnPositionsSelector } from '../hooks/features/columns/gridColumnsSelector'; import { useGridSelector, objectShallowCompare } from '../hooks/utils/useGridSelector'; import { GridRowClassNameParams } from '../models/params/gridRowParams'; import { useGridVisibleRows } from '../hooks/utils/useGridVisibleRows'; @@ -22,13 +23,14 @@ import { findParentElementFromClassName, isEventTargetInPortal } from '../utils/ import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../colDef/gridCheckboxSelectionColDef'; import { GRID_ACTIONS_COLUMN_TYPE } from '../colDef/gridActionsColDef'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../constants/gridDetailPanelToggleField'; +import { type GridDimensions } from '../hooks/features/dimensions'; import { gridSortModelSelector } from '../hooks/features/sorting/gridSortingSelector'; import { gridRowMaximumTreeDepthSelector } from '../hooks/features/rows/gridRowsSelector'; import { gridColumnGroupsHeaderMaxDepthSelector } from '../hooks/features/columnGrouping/gridColumnGroupsSelector'; -import { randomNumberBetween } from '../utils/utils'; -import { GridCellWrapper, GridCellV7 } from './cell/GridCell'; -import type { GridCellProps } from './cell/GridCell'; import { gridEditRowsStateSelector } from '../hooks/features/editing/gridEditingSelectors'; +import { randomNumberBetween } from '../utils/utils'; +import { PinnedPosition } from './cell/GridCell'; +import { GridScrollbarFillerCell as ScrollbarFiller } from './GridScrollbarFillerCell'; export interface GridRowProps extends React.HTMLAttributes { rowId: GridRowId; @@ -39,12 +41,12 @@ export interface GridRowProps extends React.HTMLAttributes { */ index: number; rowHeight: number | 'auto'; - containerWidth: number; + dimensions: GridDimensions; firstColumnToRender: number; lastColumnToRender: number; visibleColumns: GridStateColDef[]; renderedColumns: GridStateColDef[]; - position: 'left' | 'center' | 'right'; + pinnedColumns: GridPinnedColumns; /** * Determines which cell has focus. * If `null`, no cell in this row has focus. @@ -56,7 +58,8 @@ export interface GridRowProps extends React.HTMLAttributes { */ tabbableCell: string | null; row?: GridRowModel; - isLastVisible?: boolean; + isFirstVisible: boolean; + isLastVisible: boolean; focusedCellColumnIndexNotInRange?: number; isNotVisible?: boolean; onClick?: React.MouseEventHandler; @@ -69,22 +72,27 @@ export interface GridRowProps extends React.HTMLAttributes { type OwnerState = Pick & { editable: boolean; editing: boolean; + isFirstVisible: boolean; isLastVisible: boolean; classes?: DataGridProcessedProps['classes']; rowHeight: GridRowProps['rowHeight']; }; const useUtilityClasses = (ownerState: OwnerState) => { - const { editable, editing, selected, isLastVisible, rowHeight, classes } = ownerState; + const { editable, editing, selected, isFirstVisible, isLastVisible, rowHeight, classes } = + ownerState; const slots = { root: [ 'row', selected && 'selected', editable && 'row--editable', editing && 'row--editing', + isFirstVisible && 'row--firstVisible', isLastVisible && 'row--lastVisible', rowHeight === 'auto' && 'row--dynamicHeight', ], + pinnedLeft: ['pinnedLeft'], + pinnedRight: ['pinnedRight'], }; return composeClasses(slots, getDataGridUtilityClass, classes); @@ -95,28 +103,32 @@ function EmptyCell({ width }: { width: number }) { return null; } - const style = { width }; - - return
; // TODO change to .MuiDataGrid-emptyCell or .MuiDataGrid-rowFiller + return ( +
+ ); } const GridRow = React.forwardRef(function GridRow(props, refProp) { const { selected, - hovered, rowId, row, index, style: styleProp, - position, rowHeight, className, visibleColumns, renderedColumns, - containerWidth, + pinnedColumns, + dimensions, firstColumnToRender, lastColumnToRender, - isLastVisible = false, + isFirstVisible, + isLastVisible, focusedCellColumnIndexNotInRange, isNotVisible, focusedCell, @@ -133,18 +145,20 @@ const GridRow = React.forwardRef(function GridRow( const ref = React.useRef(null); const rootProps = useGridRootProps(); const currentPage = useGridVisibleRows(apiRef, rootProps); - const columnsTotalWidth = useGridSelector(apiRef, gridColumnsTotalWidthSelector); const sortModel = useGridSelector(apiRef, gridSortModelSelector); const treeDepth = useGridSelector(apiRef, gridRowMaximumTreeDepthSelector); const headerGroupingMaxDepth = useGridSelector(apiRef, gridColumnGroupsHeaderMaxDepthSelector); + const columnPositions = useGridSelector(apiRef, gridColumnPositionsSelector); const editRowsState = useGridSelector(apiRef, gridEditRowsStateSelector); const handleRef = useForkRef(ref, refProp); + const rowNode = apiRef.current.getRowNode(rowId); + const scrollbarWidth = dimensions.hasScrollY ? dimensions.scrollbarSize : 0; const ariaRowIndex = index + headerGroupingMaxDepth + 2; // 1 for the header row and 1 as it's 1-based const ownerState = { selected, - hovered, + isFirstVisible, isLastVisible, classes: rootProps.classes, editing: apiRef.current.getRowMode(rowId) === GridRowModes.Edit, @@ -157,9 +171,9 @@ const GridRow = React.forwardRef(function GridRow( React.useLayoutEffect(() => { if (rowHeight === 'auto' && ref.current && typeof ResizeObserver === 'undefined') { // Fallback for IE - apiRef.current.unstable_storeRowHeightMeasurement(rowId, ref.current.clientHeight, position); + apiRef.current.unstable_storeRowHeightMeasurement(rowId, ref.current.clientHeight); } - }, [apiRef, rowHeight, rowId, position]); + }, [apiRef, rowHeight, rowId]); React.useLayoutEffect(() => { if (currentPage.range) { @@ -187,13 +201,13 @@ const GridRow = React.forwardRef(function GridRow( entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0].blockSize : entry.contentRect.height; - apiRef.current.unstable_storeRowHeightMeasurement(rowId, height, position); + apiRef.current.unstable_storeRowHeightMeasurement(rowId, height); }); resizeObserver.observe(rootElement); return () => resizeObserver.disconnect(); - }, [apiRef, currentPage.range, index, rowHeight, rowId, position]); + }, [apiRef, currentPage.range, index, rowHeight, rowId]); const publish = React.useCallback( ( @@ -261,56 +275,9 @@ const GridRow = React.forwardRef(function GridRow( ); const { slots, slotProps, disableColumnReorder } = rootProps; - const CellComponent = slots.cell === GridCellV7 ? GridCellV7 : GridCellWrapper; const rowReordering = (rootProps as any).rowReordering as boolean; - const getCell = ( - column: GridStateColDef, - cellProps: Pick< - GridCellProps, - 'width' | 'colSpan' | 'showRightBorder' | 'indexRelativeToAllColumns' - >, - ) => { - // when the cell is a reorder cell we are not allowing to reorder the col - // fixes https://github.com/mui/mui-x/issues/11126 - const isReorderCell = column.field === '__reorder__'; - const isEditingRows = Object.keys(editRowsState).length > 0; - - const canReorderColumn = !(disableColumnReorder || column.disableReorder); - const canReorderRow = rowReordering && !sortModel.length && treeDepth <= 1 && !isEditingRows; - - const disableDragEvents = !(canReorderColumn || (isReorderCell && canReorderRow)); - - const editCellState = editRowsState[rowId]?.[column.field] ?? null; - let cellIsNotVisible = false; - - if ( - focusedCellColumnIndexNotInRange !== undefined && - visibleColumns[focusedCellColumnIndexNotInRange].field === column.field - ) { - cellIsNotVisible = true; - } - - return ( - - ); - }; - const sizes = useGridSelector( apiRef, () => ({ ...apiRef.current.unstable_getRowInternalSizes(rowId) }), @@ -319,18 +286,8 @@ const GridRow = React.forwardRef(function GridRow( let minHeight = rowHeight; if (minHeight === 'auto' && sizes) { - let numberOfBaseSizes = 0; - const maximumSize = Object.entries(sizes).reduce((acc, [key, size]) => { - const isBaseHeight = /^base[A-Z]/.test(key); - if (!isBaseHeight) { - return acc; - } - numberOfBaseSizes += 1; - if (size > acc) { - return size; - } - return acc; - }, 0); + const numberOfBaseSizes = 1; + const maximumSize = sizes.baseCenter ?? 0; if (maximumSize > 0 && numberOfBaseSizes > 1) { minHeight = maximumSize; @@ -387,19 +344,137 @@ const GridRow = React.forwardRef(function GridRow( const randomNumber = randomNumberBetween(10000, 20, 80); - const rowNode = apiRef.current.getRowNode(rowId); + const getCell = ( + column: GridStateColDef, + indexInSection: number, + indexRelativeToAllColumns: number, + sectionLength: number, + pinnedPosition = PinnedPosition.NONE, + ) => { + const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo( + rowId, + indexRelativeToAllColumns, + ); + + if (!cellColSpanInfo || cellColSpanInfo.spannedByColSpan) { + return null; + } + + let pinnedOffset: number; + // FIXME: Why is the switch check exhaustiveness not validated with typescript-eslint? + // eslint-disable-next-line default-case + switch (pinnedPosition) { + case PinnedPosition.LEFT: + pinnedOffset = columnPositions[indexRelativeToAllColumns]; + break; + case PinnedPosition.RIGHT: + pinnedOffset = + dimensions.columnsTotalWidth - + columnPositions[indexRelativeToAllColumns] - + column.computedWidth + + scrollbarWidth; + break; + case PinnedPosition.NONE: + pinnedOffset = 0; + break; + } + + if (rowNode?.type === 'skeletonRow') { + const { width } = cellColSpanInfo.cellProps; + const contentWidth = Math.round(randomNumber()); + + return ( + + ); + } + + const { colSpan, width } = cellColSpanInfo.cellProps; + + const editCellState = editRowsState[rowId]?.[column.field] ?? null; + + // when the cell is a reorder cell we are not allowing to reorder the col + // fixes https://github.com/mui/mui-x/issues/11126 + const isReorderCell = column.field === '__reorder__'; + const isEditingRows = Object.keys(editRowsState).length > 0; + + const canReorderColumn = !(disableColumnReorder || column.disableReorder); + const canReorderRow = rowReordering && !sortModel.length && treeDepth <= 1 && !isEditingRows; + + const disableDragEvents = !(canReorderColumn || (isReorderCell && canReorderRow)); + + let cellIsNotVisible = false; + if ( + focusedCellColumnIndexNotInRange !== undefined && + visibleColumns[focusedCellColumnIndexNotInRange].field === column.field + ) { + cellIsNotVisible = true; + } + + return ( + + ); + }; + + /* Start of rendering */ + if (!rowNode) { return null; } - const rowType = rowNode.type; - const cells: React.JSX.Element[] = []; + const leftCells = pinnedColumns.left.map((column, i) => { + const indexRelativeToAllColumns = i; + return getCell( + column, + i, + indexRelativeToAllColumns, + pinnedColumns.left.length, + PinnedPosition.LEFT, + ); + }); + + const rightCells = pinnedColumns.right.map((column, i) => { + const indexRelativeToAllColumns = visibleColumns.length - pinnedColumns.right.length + i; + return getCell( + column, + i, + indexRelativeToAllColumns, + pinnedColumns.right.length, + PinnedPosition.RIGHT, + ); + }); + + const middleColumnsLength = + visibleColumns.length - pinnedColumns.left.length - pinnedColumns.right.length; + const cells = [] as React.ReactNode[]; for (let i = 0; i < renderedColumns.length; i += 1) { const column = renderedColumns[i]; let indexRelativeToAllColumns = firstColumnToRender + i; - if (focusedCellColumnIndexNotInRange !== undefined && focusedCell) { if (visibleColumns[focusedCellColumnIndexNotInRange].field === column.field) { indexRelativeToAllColumns = focusedCellColumnIndexNotInRange; @@ -408,41 +483,11 @@ const GridRow = React.forwardRef(function GridRow( } } - const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo( - rowId, - indexRelativeToAllColumns, - ); + const indexInSection = indexRelativeToAllColumns - pinnedColumns.left.length; - if (cellColSpanInfo && !cellColSpanInfo.spannedByColSpan) { - if (rowType !== 'skeletonRow') { - const { colSpan, width } = cellColSpanInfo.cellProps; - const cellProps = { - width, - colSpan, - showRightBorder: rootProps.showCellVerticalBorder, - indexRelativeToAllColumns, - }; - - cells.push(getCell(column, cellProps)); - } else { - const { width } = cellColSpanInfo.cellProps; - const contentWidth = Math.round(randomNumber()); - - cells.push( - , - ); - } - } + cells.push(getCell(column, indexInSection, indexRelativeToAllColumns, middleColumnsLength)); } - const emptyCellWidth = containerWidth - columnsTotalWidth; - const eventHandlers = row ? { onClick: publishClick, @@ -454,21 +499,29 @@ const GridRow = React.forwardRef(function GridRow( } : null; + const expandedWidth = + dimensions.viewportOuterSize.width - dimensions.columnsTotalWidth - scrollbarWidth; + const emptyCellWidth = Math.max(0, expandedWidth); + return (
+ {leftCells} {cells} {emptyCellWidth > 0 && } + {rightCells.length > 0 &&
} + {rightCells} + {scrollbarWidth !== 0 && 0} />}
); }); @@ -478,7 +531,41 @@ GridRow.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- - containerWidth: PropTypes.number.isRequired, + dimensions: PropTypes.shape({ + bottomContainerHeight: PropTypes.number.isRequired, + columnsTotalWidth: PropTypes.number.isRequired, + contentSize: PropTypes.shape({ + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + }).isRequired, + hasScrollX: PropTypes.bool.isRequired, + hasScrollY: PropTypes.bool.isRequired, + headerHeight: PropTypes.number.isRequired, + headersTotalHeight: PropTypes.number.isRequired, + isReady: PropTypes.bool.isRequired, + leftPinnedWidth: PropTypes.number.isRequired, + minimumSize: PropTypes.shape({ + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + }).isRequired, + rightPinnedWidth: PropTypes.number.isRequired, + root: PropTypes.shape({ + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + }).isRequired, + rowHeight: PropTypes.number.isRequired, + rowWidth: PropTypes.number.isRequired, + scrollbarSize: PropTypes.number.isRequired, + topContainerHeight: PropTypes.number.isRequired, + viewportInnerSize: PropTypes.shape({ + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + }).isRequired, + viewportOuterSize: PropTypes.shape({ + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + }).isRequired, + }).isRequired, firstColumnToRender: PropTypes.number.isRequired, /** * Determines which cell has focus. @@ -491,14 +578,140 @@ GridRow.propTypes = { * If some rows above have expanded children, this index also take those children into account. */ index: PropTypes.number.isRequired, - isLastVisible: PropTypes.bool, + isFirstVisible: PropTypes.bool.isRequired, + isLastVisible: PropTypes.bool.isRequired, isNotVisible: PropTypes.bool, lastColumnToRender: PropTypes.number.isRequired, onClick: PropTypes.func, onDoubleClick: PropTypes.func, onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, - position: PropTypes.oneOf(['center', 'left', 'right']).isRequired, + pinnedColumns: PropTypes.shape({ + left: PropTypes.arrayOf( + PropTypes.shape({ + align: PropTypes.oneOf(['center', 'left', 'right']), + cellClassName: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + colSpan: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), + computedWidth: PropTypes.number.isRequired, + description: PropTypes.string, + disableColumnMenu: PropTypes.bool, + disableExport: PropTypes.bool, + disableReorder: PropTypes.bool, + editable: PropTypes.bool, + field: PropTypes.string.isRequired, + filterable: PropTypes.bool, + filterOperators: PropTypes.arrayOf( + PropTypes.shape({ + getApplyFilterFn: PropTypes.func.isRequired, + getValueAsString: PropTypes.func, + headerLabel: PropTypes.string, + InputComponent: PropTypes.elementType, + InputComponentProps: PropTypes.object, + label: PropTypes.string, + requiresFilterValue: PropTypes.bool, + value: PropTypes.string.isRequired, + }), + ), + flex: PropTypes.number, + getApplyQuickFilterFn: PropTypes.func, + groupable: PropTypes.bool, + hasBeenResized: PropTypes.bool, + headerAlign: PropTypes.oneOf(['center', 'left', 'right']), + headerClassName: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + headerName: PropTypes.string, + hideable: PropTypes.bool, + hideSortIcons: PropTypes.bool, + maxWidth: PropTypes.number, + minWidth: PropTypes.number, + pinnable: PropTypes.bool, + preProcessEditCellProps: PropTypes.func, + renderCell: PropTypes.func, + renderEditCell: PropTypes.func, + renderHeader: PropTypes.func, + resizable: PropTypes.bool, + sortable: PropTypes.bool, + sortComparator: PropTypes.func, + sortingOrder: PropTypes.arrayOf(PropTypes.oneOf(['asc', 'desc'])), + type: PropTypes.oneOf([ + 'actions', + 'boolean', + 'custom', + 'date', + 'dateTime', + 'number', + 'singleSelect', + 'string', + ]), + valueFormatter: PropTypes.func, + valueGetter: PropTypes.func, + valueParser: PropTypes.func, + valueSetter: PropTypes.func, + width: PropTypes.number, + }), + ).isRequired, + right: PropTypes.arrayOf( + PropTypes.shape({ + align: PropTypes.oneOf(['center', 'left', 'right']), + cellClassName: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + colSpan: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), + computedWidth: PropTypes.number.isRequired, + description: PropTypes.string, + disableColumnMenu: PropTypes.bool, + disableExport: PropTypes.bool, + disableReorder: PropTypes.bool, + editable: PropTypes.bool, + field: PropTypes.string.isRequired, + filterable: PropTypes.bool, + filterOperators: PropTypes.arrayOf( + PropTypes.shape({ + getApplyFilterFn: PropTypes.func.isRequired, + getValueAsString: PropTypes.func, + headerLabel: PropTypes.string, + InputComponent: PropTypes.elementType, + InputComponentProps: PropTypes.object, + label: PropTypes.string, + requiresFilterValue: PropTypes.bool, + value: PropTypes.string.isRequired, + }), + ), + flex: PropTypes.number, + getApplyQuickFilterFn: PropTypes.func, + groupable: PropTypes.bool, + hasBeenResized: PropTypes.bool, + headerAlign: PropTypes.oneOf(['center', 'left', 'right']), + headerClassName: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + headerName: PropTypes.string, + hideable: PropTypes.bool, + hideSortIcons: PropTypes.bool, + maxWidth: PropTypes.number, + minWidth: PropTypes.number, + pinnable: PropTypes.bool, + preProcessEditCellProps: PropTypes.func, + renderCell: PropTypes.func, + renderEditCell: PropTypes.func, + renderHeader: PropTypes.func, + resizable: PropTypes.bool, + sortable: PropTypes.bool, + sortComparator: PropTypes.func, + sortingOrder: PropTypes.arrayOf(PropTypes.oneOf(['asc', 'desc'])), + type: PropTypes.oneOf([ + 'actions', + 'boolean', + 'custom', + 'date', + 'dateTime', + 'number', + 'singleSelect', + 'string', + ]), + valueFormatter: PropTypes.func, + valueGetter: PropTypes.func, + valueParser: PropTypes.func, + valueSetter: PropTypes.func, + width: PropTypes.number, + }), + ).isRequired, + }).isRequired, renderedColumns: PropTypes.arrayOf(PropTypes.object).isRequired, row: PropTypes.object, rowHeight: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired, diff --git a/packages/grid/x-data-grid/src/components/GridScrollbarFillerCell.tsx b/packages/grid/x-data-grid/src/components/GridScrollbarFillerCell.tsx new file mode 100644 index 000000000000..8be0cf5eb44b --- /dev/null +++ b/packages/grid/x-data-grid/src/components/GridScrollbarFillerCell.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled } from '@mui/material/styles'; +import { getDataGridUtilityClass as getClassName } from '../constants'; + +const classes = { + root: getClassName('scrollbarFiller'), + header: getClassName('scrollbarFiller--header'), + borderTop: getClassName('scrollbarFiller--borderTop'), + pinnedRight: getClassName('scrollbarFiller--pinnedRight'), +}; + +const Style = styled('div')({ + minWidth: 'calc(var(--DataGrid-hasScrollY) * var(--DataGrid-scrollbarSize))', + alignSelf: 'stretch', + [`&.${classes.borderTop}`]: { + borderTop: '1px solid var(--DataGrid-rowBorderColor)', + }, + [`&.${classes.pinnedRight}`]: { + backgroundColor: 'var(--DataGrid-pinnedBackground)', + }, + [`&.${classes.pinnedRight}:not(.${classes.header})`]: { + position: 'sticky', + right: 0, + }, + [`&:not(.${classes.header}):not(.${classes.pinnedRight})`]: { + transform: 'translate3d(var(--DataGrid-offsetLeft), 0, 0)', + }, +}); + +function GridScrollbarFillerCell({ + header, + borderTop = true, + pinnedRight, +}: { + header?: boolean; + borderTop?: boolean; + pinnedRight?: boolean; +}) { + return ( +