diff --git a/docs/data/data-grid/column-groups/BasicGroupingDemo.js b/docs/data/data-grid/column-groups/BasicGroupingDemo.js
new file mode 100644
index 0000000000000..e9cc4d0f8046f
--- /dev/null
+++ b/docs/data/data-grid/column-groups/BasicGroupingDemo.js
@@ -0,0 +1,67 @@
+import * as React from 'react';
+import { DataGridPro } from '@mui/x-data-grid-pro';
+
+const columns = [
+ { field: 'id', headerName: 'ID', width: 90 },
+ {
+ field: 'firstName',
+ headerName: 'First name',
+ width: 150,
+ },
+ {
+ field: 'lastName',
+ headerName: 'Last name',
+ width: 150,
+ },
+ {
+ field: 'age',
+ headerName: 'Age',
+ type: 'number',
+ width: 110,
+ },
+];
+
+const rows = [
+ { id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
+ { id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
+ { id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
+ { id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
+ { id: 5, lastName: 'Targaryen', firstName: 'Daenerys', age: null },
+ { id: 6, lastName: 'Melisandre', firstName: null, age: 150 },
+ { id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
+ { id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
+ { id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
+];
+
+const columnGroupingModel = [
+ {
+ groupId: 'Internal',
+ description: '',
+ children: [{ field: 'id' }],
+ },
+ {
+ groupId: 'Basic info',
+ children: [
+ {
+ groupId: 'Full name',
+ children: [{ field: 'lastName' }, { field: 'firstName' }],
+ },
+ { field: 'age' },
+ ],
+ },
+];
+
+export default function BasicGroupingDemo() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/column-groups/BasicGroupingDemo.tsx b/docs/data/data-grid/column-groups/BasicGroupingDemo.tsx
new file mode 100644
index 0000000000000..39b149a79ca26
--- /dev/null
+++ b/docs/data/data-grid/column-groups/BasicGroupingDemo.tsx
@@ -0,0 +1,71 @@
+import * as React from 'react';
+import {
+ DataGridPro,
+ GridColDef,
+ GridColumnGroupingModel,
+} from '@mui/x-data-grid-pro';
+
+const columns: GridColDef[] = [
+ { field: 'id', headerName: 'ID', width: 90 },
+ {
+ field: 'firstName',
+ headerName: 'First name',
+ width: 150,
+ },
+ {
+ field: 'lastName',
+ headerName: 'Last name',
+ width: 150,
+ },
+ {
+ field: 'age',
+ headerName: 'Age',
+ type: 'number',
+ width: 110,
+ },
+];
+
+const rows = [
+ { id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
+ { id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
+ { id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
+ { id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
+ { id: 5, lastName: 'Targaryen', firstName: 'Daenerys', age: null },
+ { id: 6, lastName: 'Melisandre', firstName: null, age: 150 },
+ { id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
+ { id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
+ { id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
+];
+
+const columnGroupingModel: GridColumnGroupingModel = [
+ {
+ groupId: 'Internal',
+ description: '',
+ children: [{ field: 'id' }],
+ },
+ {
+ groupId: 'Basic info',
+ children: [
+ {
+ groupId: 'Full name',
+ children: [{ field: 'lastName' }, { field: 'firstName' }],
+ },
+ { field: 'age' },
+ ],
+ },
+];
+
+export default function BasicGroupingDemo() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/column-groups/BasicGroupingDemo.tsx.preview b/docs/data/data-grid/column-groups/BasicGroupingDemo.tsx.preview
new file mode 100644
index 0000000000000..d421de0b52667
--- /dev/null
+++ b/docs/data/data-grid/column-groups/BasicGroupingDemo.tsx.preview
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/docs/data/data-grid/column-groups/BreakingGroupDemo.js b/docs/data/data-grid/column-groups/BreakingGroupDemo.js
new file mode 100644
index 0000000000000..462958c566b18
--- /dev/null
+++ b/docs/data/data-grid/column-groups/BreakingGroupDemo.js
@@ -0,0 +1,65 @@
+import * as React from 'react';
+import { DataGridPro } from '@mui/x-data-grid-pro';
+
+const columns = [
+ { field: 'id', headerName: 'ID', width: 100 },
+ { field: 'isAdmin', type: 'boolean', headerName: 'is admin', width: 100 },
+ {
+ field: 'firstName',
+ headerName: 'First name',
+ width: 150,
+ },
+ {
+ field: 'lastName',
+ headerName: 'Last name',
+ width: 150,
+ },
+ {
+ field: 'age',
+ headerName: 'Age',
+ type: 'number',
+ width: 110,
+ },
+];
+
+const rows = [
+ { id: 1, isAdmin: false, lastName: 'Snow', firstName: 'Jon', age: 35 },
+ { id: 2, isAdmin: true, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
+ { id: 3, isAdmin: false, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
+ { id: 4, isAdmin: false, lastName: 'Stark', firstName: 'Arya', age: 16 },
+ { id: 5, isAdmin: true, lastName: 'Targaryen', firstName: 'Daenerys', age: null },
+ { id: 6, isAdmin: true, lastName: 'Melisandre', firstName: null, age: 150 },
+ { id: 7, isAdmin: false, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
+ { id: 8, isAdmin: false, lastName: 'Frances', firstName: 'Rossini', age: 36 },
+ { id: 9, isAdmin: false, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
+];
+
+const columnGroupingModel = [
+ {
+ groupId: 'internal_data',
+ headerName: 'Internal (not freeReordering)',
+ description: '',
+ children: [{ field: 'id' }, { field: 'isAdmin' }],
+ },
+ {
+ groupId: 'naming',
+ headerName: 'Full name (freeReordering)',
+ freeReordering: true,
+ children: [{ field: 'lastName' }, { field: 'firstName' }],
+ },
+];
+
+export default function BreakingGroupDemo() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/column-groups/BreakingGroupDemo.tsx b/docs/data/data-grid/column-groups/BreakingGroupDemo.tsx
new file mode 100644
index 0000000000000..385111a7e483d
--- /dev/null
+++ b/docs/data/data-grid/column-groups/BreakingGroupDemo.tsx
@@ -0,0 +1,69 @@
+import * as React from 'react';
+import {
+ DataGridPro,
+ GridColDef,
+ GridColumnGroupingModel,
+} from '@mui/x-data-grid-pro';
+
+const columns: GridColDef[] = [
+ { field: 'id', headerName: 'ID', width: 100 },
+ { field: 'isAdmin', type: 'boolean', headerName: 'is admin', width: 100 },
+ {
+ field: 'firstName',
+ headerName: 'First name',
+ width: 150,
+ },
+ {
+ field: 'lastName',
+ headerName: 'Last name',
+ width: 150,
+ },
+ {
+ field: 'age',
+ headerName: 'Age',
+ type: 'number',
+ width: 110,
+ },
+];
+
+const rows = [
+ { id: 1, isAdmin: false, lastName: 'Snow', firstName: 'Jon', age: 35 },
+ { id: 2, isAdmin: true, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
+ { id: 3, isAdmin: false, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
+ { id: 4, isAdmin: false, lastName: 'Stark', firstName: 'Arya', age: 16 },
+ { id: 5, isAdmin: true, lastName: 'Targaryen', firstName: 'Daenerys', age: null },
+ { id: 6, isAdmin: true, lastName: 'Melisandre', firstName: null, age: 150 },
+ { id: 7, isAdmin: false, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
+ { id: 8, isAdmin: false, lastName: 'Frances', firstName: 'Rossini', age: 36 },
+ { id: 9, isAdmin: false, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
+];
+
+const columnGroupingModel: GridColumnGroupingModel = [
+ {
+ groupId: 'internal_data',
+ headerName: 'Internal (not freeReordering)',
+ description: '',
+ children: [{ field: 'id' }, { field: 'isAdmin' }],
+ },
+ {
+ groupId: 'naming',
+ headerName: 'Full name (freeReordering)',
+ freeReordering: true,
+ children: [{ field: 'lastName' }, { field: 'firstName' }],
+ },
+];
+
+export default function BreakingGroupDemo() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/column-groups/BreakingGroupDemo.tsx.preview b/docs/data/data-grid/column-groups/BreakingGroupDemo.tsx.preview
new file mode 100644
index 0000000000000..75d805bb7b2cf
--- /dev/null
+++ b/docs/data/data-grid/column-groups/BreakingGroupDemo.tsx.preview
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/docs/data/data-grid/column-groups/CustomizationDemo.js b/docs/data/data-grid/column-groups/CustomizationDemo.js
new file mode 100644
index 0000000000000..e0373d6f1662b
--- /dev/null
+++ b/docs/data/data-grid/column-groups/CustomizationDemo.js
@@ -0,0 +1,125 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { styled } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+import { DataGridPro } from '@mui/x-data-grid-pro';
+import BuildIcon from '@mui/icons-material/Build';
+import PersonIcon from '@mui/icons-material/Person';
+
+const columns = [
+ { field: 'id', headerName: 'ID', width: 150 },
+ {
+ field: 'firstName',
+ headerName: 'First name',
+ width: 150,
+ },
+ {
+ field: 'lastName',
+ headerName: 'Last name',
+ width: 150,
+ },
+ {
+ field: 'age',
+ headerName: 'Age',
+ type: 'number',
+ width: 110,
+ },
+];
+
+const rows = [
+ { id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
+ { id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
+ { id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
+ { id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
+ { id: 5, lastName: 'Targaryen', firstName: 'Daenerys', age: null },
+ { id: 6, lastName: 'Melisandre', firstName: null, age: 150 },
+ { id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
+ { id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
+ { id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
+];
+
+const HeaderWithIconRoot = styled('div')(({ theme }) => ({
+ overflow: 'hidden',
+ display: 'flex',
+ alignItems: 'center',
+ '& span': {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ marginRight: theme.spacing(0.5),
+ },
+}));
+
+const HeaderWithIcon = (props) => {
+ const { icon, ...params } = props;
+
+ return (
+
+ {params.headerName ?? params.groupId} {icon}
+
+ );
+};
+
+HeaderWithIcon.propTypes = {
+ /**
+ * A unique string identifying the group.
+ */
+ groupId: PropTypes.oneOfType([PropTypes.oneOf([null]), PropTypes.string])
+ .isRequired,
+ /**
+ * The title of the column rendered in the column header cell.
+ */
+ headerName: PropTypes.string,
+ icon: PropTypes.node,
+};
+
+const columnGroupingModel = [
+ {
+ groupId: 'internal_data',
+ headerName: 'Internal',
+ description: '',
+ renderHeaderGroup: (params) => (
+ } />
+ ),
+ children: [{ field: 'id' }],
+ },
+ {
+ groupId: 'character',
+ description: 'Information about the character',
+ headerName: 'Basic info',
+ renderHeaderGroup: (params) => (
+ } />
+ ),
+ children: [
+ {
+ groupId: 'naming',
+ headerName: 'Names',
+ headerClassName: 'my-super-theme--naming-group',
+ children: [{ field: 'lastName' }, { field: 'firstName' }],
+ },
+ { field: 'age' },
+ ],
+ },
+];
+
+export default function CustomizationDemo() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/column-groups/CustomizationDemo.tsx b/docs/data/data-grid/column-groups/CustomizationDemo.tsx
new file mode 100644
index 0000000000000..4918e926884c9
--- /dev/null
+++ b/docs/data/data-grid/column-groups/CustomizationDemo.tsx
@@ -0,0 +1,120 @@
+import * as React from 'react';
+import { styled } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+import {
+ DataGridPro,
+ GridColDef,
+ GridColumnGroupHeaderParams,
+ GridColumnGroupingModel,
+} from '@mui/x-data-grid-pro';
+import BuildIcon from '@mui/icons-material/Build';
+import PersonIcon from '@mui/icons-material/Person';
+
+const columns: GridColDef[] = [
+ { field: 'id', headerName: 'ID', width: 150 },
+ {
+ field: 'firstName',
+ headerName: 'First name',
+ width: 150,
+ },
+ {
+ field: 'lastName',
+ headerName: 'Last name',
+ width: 150,
+ },
+ {
+ field: 'age',
+ headerName: 'Age',
+ type: 'number',
+ width: 110,
+ },
+];
+
+const rows = [
+ { id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
+ { id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
+ { id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
+ { id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
+ { id: 5, lastName: 'Targaryen', firstName: 'Daenerys', age: null },
+ { id: 6, lastName: 'Melisandre', firstName: null, age: 150 },
+ { id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
+ { id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
+ { id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
+];
+
+interface HeaderWithIconProps extends GridColumnGroupHeaderParams {
+ icon: React.ReactNode;
+}
+
+const HeaderWithIconRoot = styled('div')(({ theme }) => ({
+ overflow: 'hidden',
+ display: 'flex',
+ alignItems: 'center',
+ '& span': {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ marginRight: theme.spacing(0.5),
+ },
+}));
+
+const HeaderWithIcon = (props: HeaderWithIconProps) => {
+ const { icon, ...params } = props;
+
+ return (
+
+ {params.headerName ?? params.groupId} {icon}
+
+ );
+};
+
+const columnGroupingModel: GridColumnGroupingModel = [
+ {
+ groupId: 'internal_data',
+ headerName: 'Internal',
+ description: '',
+ renderHeaderGroup: (params) => (
+ } />
+ ),
+ children: [{ field: 'id' }],
+ },
+ {
+ groupId: 'character',
+ description: 'Information about the character',
+ headerName: 'Basic info',
+ renderHeaderGroup: (params) => (
+ } />
+ ),
+ children: [
+ {
+ groupId: 'naming',
+ headerName: 'Names',
+ headerClassName: 'my-super-theme--naming-group',
+ children: [{ field: 'lastName' }, { field: 'firstName' }],
+ },
+ { field: 'age' },
+ ],
+ },
+];
+
+export default function CustomizationDemo() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/column-groups/CustomizationDemo.tsx.preview b/docs/data/data-grid/column-groups/CustomizationDemo.tsx.preview
new file mode 100644
index 0000000000000..75d805bb7b2cf
--- /dev/null
+++ b/docs/data/data-grid/column-groups/CustomizationDemo.tsx.preview
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/docs/data/data-grid/column-groups/column-groups.md b/docs/data/data-grid/column-groups/column-groups.md
index 5532addd39b6f..1221d196460bd 100644
--- a/docs/data/data-grid/column-groups/column-groups.md
+++ b/docs/data/data-grid/column-groups/column-groups.md
@@ -1,18 +1,99 @@
---
-title: Data Grid - Column groups 🚧
+title: Data Grid - Column groups
---
-# Data grid - Column groups 🚧
+# Data grid - Column groups
Group your columns.
+Grouping columns allows you to have a multi-level hierarchy of columns in your header.
+
+:::warning
+This feature is experimental, it needs to be explicitly activated using the `columnGrouping` experimental feature flag.
+
+```tsx
+
+```
+
+:::
+
+## Define column groups
+
+You can define column groups with the `columnGroupingModel` prop.
+This prop receives an array of column groups.
+
+A column group is defined by at least two properties:
+
+- `groupId`: a string used to identify the group
+- `children`: an array containing the children of the group
+
+The `children` attribute can contain two types of objects:
+
+- leafs with type `{ field: string }`, which add the column with the corresponding `field` to this group.
+- other column groups which allows you to have nested groups.
+
+:::warning
+A column can only be associated with one group.
+:::
+
+```jsx
+
+```
+
+{{"demo": "BasicGroupingDemo.js", "bg": "inline"}}
+
+## Customize column group
+
+In addition to the required `groupId` and `children`, you can use the following optional properties to customize a column group:
+
+- `headerName`: the string displayed as the column's name (instead of `groupId`).
+- `description`: a text for the tooltip.
+- `headerClassName`: a CSS class for styling customization.
+- `renderHeaderGroup`: a function returning custom React component.
+
+{{"demo": "CustomizationDemo.js", "bg": "inline"}}
+
+## Column reordering [](https://mui.com/store/items/mui-x-pro/)
+
+By default, the columns that are part of a group cannot be dragged to outside their group.
+You can customize this behavior on specific groups by setting `freeReordering: true` in a column group object.
+
+In the example below, the `Full name` column group can be divided, but not other column groups.
+
+{{"demo": "BreakingGroupDemo.js", "disableAd": true, "bg": "inline"}}
+
+## Manage group visibility 🚧
+
:::warning
This feature isn't implemented yet. It's coming.
+:::
+
+The column group should allow to switch between an extended/collapsed view which hide/show some columns
-👍 Upvote [issue #195](https://github.com/mui/mui-x/issues/195) if you want to see it land faster.
+## Reordering groups 🚧[](https://mui.com/store/items/mui-x-pro/)
+
+:::warning
+This feature isn't implemented yet. It's coming.
:::
-Grouping columns allows you to have multiple levels of columns in your header and the ability, if needed, to 'open and close' column groups to show and hide additional columns.
+Users could drag and drop group header to move all the group children at once
## API
diff --git a/docs/data/pages.ts b/docs/data/pages.ts
index bd75b5919ccbb..e4efc55274960 100644
--- a/docs/data/pages.ts
+++ b/docs/data/pages.ts
@@ -35,7 +35,7 @@ const pages: MuiPage[] = [
{ pathname: '/x/react-data-grid/column-ordering' },
{ pathname: '/x/react-data-grid/column-pinning', plan: 'pro' },
{ pathname: '/x/react-data-grid/column-spanning' },
- { pathname: '/x/react-data-grid/column-groups', title: 'Column groups 🚧' },
+ { pathname: '/x/react-data-grid/column-groups' },
],
},
{
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 2c23eadd12e3f..52d19949beedd 100644
--- a/docs/pages/x/api/data-grid/data-grid-premium.json
+++ b/docs/pages/x/api/data-grid/data-grid-premium.json
@@ -70,7 +70,7 @@
"experimentalFeatures": {
"type": {
"name": "shape",
- "description": "{ aggregation?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }"
+ "description": "{ aggregation?: bool, columnGrouping?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }"
}
},
"filterMode": {
@@ -279,6 +279,7 @@
"columnHeader--sorted",
"columnHeader--filtered",
"columnHeader",
+ "columnGroupHeader",
"columnHeaderCheckbox",
"columnHeaderDraggableContainer",
"rowReorderCellPlaceholder",
@@ -286,6 +287,9 @@
"columnHeaderTitle",
"columnHeaderTitleContainer",
"columnHeaderTitleContainerContent",
+ "columnHeader--filledGroup",
+ "columnHeader--emptyGroup",
+ "columnHeader--showColumnBorder",
"columnHeaders",
"columnHeadersInner",
"columnHeadersInner--scrollable",
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 71dda53361c76..38615bff95fdb 100644
--- a/docs/pages/x/api/data-grid/data-grid-pro.json
+++ b/docs/pages/x/api/data-grid/data-grid-pro.json
@@ -59,7 +59,7 @@
"experimentalFeatures": {
"type": {
"name": "shape",
- "description": "{ newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }"
+ "description": "{ columnGrouping?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }"
}
},
"filterMode": {
@@ -331,6 +331,7 @@
"columnHeader--sorted",
"columnHeader--filtered",
"columnHeader",
+ "columnGroupHeader",
"columnHeaderCheckbox",
"columnHeaderDraggableContainer",
"rowReorderCellPlaceholder",
@@ -338,6 +339,9 @@
"columnHeaderTitle",
"columnHeaderTitleContainer",
"columnHeaderTitleContainerContent",
+ "columnHeader--filledGroup",
+ "columnHeader--emptyGroup",
+ "columnHeader--showColumnBorder",
"columnHeaders",
"columnHeadersInner",
"columnHeadersInner--scrollable",
diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json
index a9eb815c3b24c..6de4912093c29 100644
--- a/docs/pages/x/api/data-grid/data-grid.json
+++ b/docs/pages/x/api/data-grid/data-grid.json
@@ -39,7 +39,7 @@
"experimentalFeatures": {
"type": {
"name": "shape",
- "description": "{ newEditingApi?: bool, preventCommitWhileValidating?: bool, warnIfFocusStateIsNotSynced?: bool }"
+ "description": "{ columnGrouping?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, warnIfFocusStateIsNotSynced?: bool }"
}
},
"filterMode": {
@@ -285,6 +285,7 @@
"columnHeader--sorted",
"columnHeader--filtered",
"columnHeader",
+ "columnGroupHeader",
"columnHeaderCheckbox",
"columnHeaderDraggableContainer",
"rowReorderCellPlaceholder",
@@ -292,6 +293,9 @@
"columnHeaderTitle",
"columnHeaderTitleContainer",
"columnHeaderTitleContainerContent",
+ "columnHeader--filledGroup",
+ "columnHeader--emptyGroup",
+ "columnHeader--showColumnBorder",
"columnHeaders",
"columnHeadersInner",
"columnHeadersInner--scrollable",
diff --git a/docs/pages/x/api/data-grid/grid-api.md b/docs/pages/x/api/data-grid/grid-api.md
index 5f9f9cf226607..c63564268ae43 100644
--- a/docs/pages/x/api/data-grid/grid-api.md
+++ b/docs/pages/x/api/data-grid/grid-api.md
@@ -86,7 +86,7 @@ import { GridApi } from '@mui/x-data-grid-pro';
| setColumnVisibility | (field: string, isVisible: boolean) => void | Changes the visibility of the column referred by `field`. |
| setColumnVisibilityModel | (model: GridColumnVisibilityModel) => void | Sets the column visibility model to the one given by `model`. |
| setColumnWidth | (field: string, width: number) => void | Updates the width of a column. |
-| setDensity | (density: GridDensity, headerHeight?: number, rowHeight?: number) => void | Sets the density of the grid. |
+| setDensity | (density: GridDensity, headerHeight?: number, rowHeight?: number, maxDepth?: number) => void | Sets the density of the grid. |
| setEditCellValue | (params: GridEditCellValueParams, event?: MuiBaseEvent) => Promise<boolean> \| void | Sets the value of the edit cell.
Commonly used inside the edit cell component. |
| setEditRowsModel | (model: GridEditRowsModel) => void | Set the edit rows model of the grid. |
| setExpandedDetailPanels [](/x/introduction/licensing/#pro-plan) | (ids: GridRowId[]) => void | Changes which rows to expand the detail panel. |
@@ -118,6 +118,8 @@ import { GridApi } from '@mui/x-data-grid-pro';
| toggleColumnMenu | (field: string) => void | Toggles the column menu under the `field` column. |
| toggleDetailPanel [](/x/introduction/licensing/#pro-plan) | (id: GridRowId) => void | Expands or collapses the detail panel of a row. |
| unpinColumn [](/x/introduction/licensing/#pro-plan) | (field: string) => void | Unpins a column. |
+| unstable_getAllGroupDetails | () => GridColumnGroupLookup | Returns the column group lookup. |
+| unstable_getColumnGroupPath | (field: string) => GridColumnGroup['groupId'][] | Returns the id of the groups leading to the requested column.
The array is ordered by increasing depth (the last element is the direct parent of the column). |
| unstable_setPinnedRows [](/x/introduction/licensing/#pro-plan) | (pinnedRows?: GridPinnedRowsProp) => void | Changes the pinned rows. |
| updateColumn | (col: GridColDef) => void | Updates the definition of a column. |
| updateColumns | (cols: GridColDef[]) => void | Updates the definition of multiple columns at the same time. |
diff --git a/docs/pages/x/api/data-grid/selectors.json b/docs/pages/x/api/data-grid/selectors.json
index 1f0955ae154ce..17845cffa07ca 100644
--- a/docs/pages/x/api/data-grid/selectors.json
+++ b/docs/pages/x/api/data-grid/selectors.json
@@ -25,6 +25,12 @@
"description": "Get the field of each column.",
"supportsApiRef": true
},
+ {
+ "name": "gridColumnGroupsLookupSelector",
+ "returnType": "GridColumnGroupLookup",
+ "description": "",
+ "supportsApiRef": true
+ },
{
"name": "gridColumnLookupSelector",
"returnType": "GridColumnLookup",
@@ -83,6 +89,12 @@
"description": "",
"supportsApiRef": true
},
+ {
+ "name": "gridDensityHeaderGroupingMaxDepthSelector",
+ "returnType": "number",
+ "description": "",
+ "supportsApiRef": true
+ },
{
"name": "gridDensityHeaderHeightSelector",
"returnType": "number",
@@ -101,6 +113,12 @@
"description": "",
"supportsApiRef": false
},
+ {
+ "name": "gridDensityTotalHeaderHeightSelector",
+ "returnType": "number",
+ "description": "",
+ "supportsApiRef": true
+ },
{
"name": "gridDensityValueSelector",
"returnType": "GridDensity",
diff --git a/docs/scripts/generateProptypes.ts b/docs/scripts/generateProptypes.ts
index 37661c037c0fe..9a6ad47e3734b 100644
--- a/docs/scripts/generateProptypes.ts
+++ b/docs/scripts/generateProptypes.ts
@@ -34,6 +34,7 @@ async function generateProptypes(program: ttp.ts.Program, sourceFile: string) {
'groupingColDef',
'rowNode',
'localeText',
+ 'columnGroupingModel',
];
if (propsToNotResolve.includes(name)) {
return false;
diff --git a/docs/translations/api-docs/data-grid/data-grid-premium-pt.json b/docs/translations/api-docs/data-grid/data-grid-premium-pt.json
index 58b85bb7d5161..2086662587c46 100644
--- a/docs/translations/api-docs/data-grid/data-grid-premium-pt.json
+++ b/docs/translations/api-docs/data-grid/data-grid-premium-pt.json
@@ -279,6 +279,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -305,6 +309,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
diff --git a/docs/translations/api-docs/data-grid/data-grid-premium-zh.json b/docs/translations/api-docs/data-grid/data-grid-premium-zh.json
index 58b85bb7d5161..2086662587c46 100644
--- a/docs/translations/api-docs/data-grid/data-grid-premium-zh.json
+++ b/docs/translations/api-docs/data-grid/data-grid-premium-zh.json
@@ -279,6 +279,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -305,6 +309,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
diff --git a/docs/translations/api-docs/data-grid/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium.json
index 58b85bb7d5161..2086662587c46 100644
--- a/docs/translations/api-docs/data-grid/data-grid-premium.json
+++ b/docs/translations/api-docs/data-grid/data-grid-premium.json
@@ -279,6 +279,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -305,6 +309,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
diff --git a/docs/translations/api-docs/data-grid/data-grid-pro-pt.json b/docs/translations/api-docs/data-grid/data-grid-pro-pt.json
index ef43b2e3c26ee..6e06b5e2898d5 100644
--- a/docs/translations/api-docs/data-grid/data-grid-pro-pt.json
+++ b/docs/translations/api-docs/data-grid/data-grid-pro-pt.json
@@ -269,6 +269,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -295,6 +299,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
diff --git a/docs/translations/api-docs/data-grid/data-grid-pro-zh.json b/docs/translations/api-docs/data-grid/data-grid-pro-zh.json
index ef43b2e3c26ee..6e06b5e2898d5 100644
--- a/docs/translations/api-docs/data-grid/data-grid-pro-zh.json
+++ b/docs/translations/api-docs/data-grid/data-grid-pro-zh.json
@@ -269,6 +269,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -295,6 +299,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
diff --git a/docs/translations/api-docs/data-grid/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro.json
index ef43b2e3c26ee..6e06b5e2898d5 100644
--- a/docs/translations/api-docs/data-grid/data-grid-pro.json
+++ b/docs/translations/api-docs/data-grid/data-grid-pro.json
@@ -269,6 +269,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -295,6 +299,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
diff --git a/docs/translations/api-docs/data-grid/data-grid-pt.json b/docs/translations/api-docs/data-grid/data-grid-pt.json
index cbf150db637c1..20c31c00daabc 100644
--- a/docs/translations/api-docs/data-grid/data-grid-pt.json
+++ b/docs/translations/api-docs/data-grid/data-grid-pt.json
@@ -239,6 +239,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -265,6 +269,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
diff --git a/docs/translations/api-docs/data-grid/data-grid-zh.json b/docs/translations/api-docs/data-grid/data-grid-zh.json
index cbf150db637c1..20c31c00daabc 100644
--- a/docs/translations/api-docs/data-grid/data-grid-zh.json
+++ b/docs/translations/api-docs/data-grid/data-grid-zh.json
@@ -239,6 +239,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -265,6 +269,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
diff --git a/docs/translations/api-docs/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid.json
index cbf150db637c1..20c31c00daabc 100644
--- a/docs/translations/api-docs/data-grid/data-grid.json
+++ b/docs/translations/api-docs/data-grid/data-grid.json
@@ -239,6 +239,10 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header element"
},
+ "columnGroupHeader": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the column group header element"
+ },
"columnHeaderCheckbox": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the header checkbox cell element"
@@ -265,6 +269,20 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column header's title excepted buttons"
},
+ "columnHeader--filledGroup": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "not empty"
+ },
+ "columnHeader--emptyGroup": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the empty column group header cell"
+ },
+ "columnHeader--showColumnBorder": {
+ "description": "Styles applied to {{nodeName}} when {{conditions}}.",
+ "nodeName": "the column group header cell",
+ "conditions": "show column border"
+ },
"columnHeaders": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the column headers"
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 e62c89ceb2ad4..bbdfc8f442b2f 100644
--- a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
+++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
@@ -134,6 +134,7 @@ DataGridPremiumRaw.propTypes = {
* @default 3
*/
columnBuffer: PropTypes.number,
+ columnGroupingModel: PropTypes.arrayOf(PropTypes.object),
/**
* Set of columns of type [[GridColumns]].
*/
@@ -286,6 +287,7 @@ DataGridPremiumRaw.propTypes = {
*/
experimentalFeatures: PropTypes.shape({
aggregation: PropTypes.bool,
+ columnGrouping: PropTypes.bool,
newEditingApi: PropTypes.bool,
preventCommitWhileValidating: PropTypes.bool,
rowPinning: PropTypes.bool,
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 1f34b5363eb8a..18f424aa07d79 100644
--- a/packages/grid/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx
+++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx
@@ -55,6 +55,7 @@ import {
useGridColumnSpanning,
useGridRowReorder,
useGridRowReorderPreProcessors,
+ useGridColumnGroupingPreProcessors,
useGridRowPinning,
useGridRowPinningPreProcessors,
rowPinningStateInitializer,
@@ -83,6 +84,7 @@ export const useDataGridPremiumComponent = (
/**
* Register all pre-processors called during state initialization here.
*/
+ useGridColumnGroupingPreProcessors(apiRef, props);
useGridSelectionPreProcessors(apiRef, props);
useGridRowReorderPreProcessors(apiRef, props);
useGridRowGroupingPreProcessors(apiRef, props);
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 5715c7910a03b..b36812c31aa73 100644
--- a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
+++ b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
@@ -115,6 +115,7 @@ DataGridProRaw.propTypes = {
* @default 3
*/
columnBuffer: PropTypes.number,
+ columnGroupingModel: PropTypes.arrayOf(PropTypes.object),
/**
* Set of columns of type [[GridColumns]].
*/
@@ -256,6 +257,7 @@ DataGridProRaw.propTypes = {
* For each feature, if the flag is not explicitly set to `true`, the feature will be fully disabled and any property / method call will not have any effect.
*/
experimentalFeatures: PropTypes.shape({
+ columnGrouping: PropTypes.bool,
newEditingApi: PropTypes.bool,
preventCommitWhileValidating: PropTypes.bool,
rowPinning: PropTypes.bool,
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 f27af1021f70f..c633ae78f5076 100644
--- a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx
+++ b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx
@@ -40,6 +40,9 @@ import {
preferencePanelStateInitializer,
rowsMetaStateInitializer,
selectionStateInitializer,
+ useGridColumnGrouping,
+ columnGroupsStateInitializer,
+ useGridColumnGroupingPreProcessors,
} from '@mui/x-data-grid/internals';
import { GridApiPro } from '../models/gridApiPro';
import { DataGridProProcessedProps } from '../models/dataGridProProps';
@@ -82,6 +85,7 @@ export const useDataGridProComponent = (
/**
* Register all pre-processors called during state initialization here.
*/
+ useGridColumnGroupingPreProcessors(apiRef, props);
useGridSelectionPreProcessors(apiRef, props);
useGridRowReorderPreProcessors(apiRef, props);
useGridTreeDataPreProcessors(apiRef, props);
@@ -118,6 +122,7 @@ export const useDataGridProComponent = (
useGridInitializeState(paginationStateInitializer, apiRef, props);
useGridInitializeState(rowsMetaStateInitializer, apiRef, props);
useGridInitializeState(columnMenuStateInitializer, apiRef, props);
+ useGridInitializeState(columnGroupsStateInitializer, apiRef, props);
useGridTreeData(apiRef);
useGridKeyboardNavigation(apiRef, props);
@@ -129,6 +134,7 @@ export const useDataGridProComponent = (
useGridParamsApi(apiRef);
useGridDetailPanel(apiRef, props);
useGridColumnSpanning(apiRef);
+ useGridColumnGrouping(apiRef, props);
const useGridEditing = props.experimentalFeatures?.newEditingApi
? useGridEditing_new
diff --git a/packages/grid/x-data-grid-pro/src/components/DataGridProColumnHeaders.tsx b/packages/grid/x-data-grid-pro/src/components/DataGridProColumnHeaders.tsx
index 28059d10d5acd..6112388a963ad 100644
--- a/packages/grid/x-data-grid-pro/src/components/DataGridProColumnHeaders.tsx
+++ b/packages/grid/x-data-grid-pro/src/components/DataGridProColumnHeaders.tsx
@@ -77,6 +77,7 @@ const GridColumnHeadersPinnedColumnHeaders = styled('div', {
height: '100%',
zIndex: 1,
display: 'flex',
+ flexDirection: 'column',
boxShadow: theme.shadows[2],
backgroundColor: theme.palette.background.default,
...(theme.palette.mode === 'dark' && {
@@ -120,11 +121,17 @@ export const DataGridProColumnHeaders = React.forwardRef<
const pinnedColumns = useGridSelector(apiRef, gridPinnedColumnsSelector);
const [leftPinnedColumns, rightPinnedColumns] = filterColumns(pinnedColumns, visibleColumnFields);
- const { isDragging, renderContext, getRootProps, getInnerProps, getColumns } =
- useGridColumnHeaders({
- innerRef,
- minColumnIndex: leftPinnedColumns.length,
- });
+ const {
+ isDragging,
+ renderContext,
+ getRootProps,
+ getInnerProps,
+ getColumnHeaders,
+ getColumnGroupHeaders,
+ } = useGridColumnHeaders({
+ innerRef,
+ minColumnIndex: leftPinnedColumns.length,
+ });
const ownerState = { leftPinnedColumns, rightPinnedColumns, classes: rootProps.classes };
const classes = useUtilityClasses(ownerState);
@@ -151,7 +158,6 @@ export const DataGridProColumnHeaders = React.forwardRef<
const pinnedColumnHeadersProps = {
role: innerProps.role,
- 'aria-rowindex': innerProps['aria-rowindex'],
};
return (
@@ -162,7 +168,12 @@ export const DataGridProColumnHeaders = React.forwardRef<
ownerState={{ side: GridPinnedPosition.left }}
{...pinnedColumnHeadersProps}
>
- {getColumns(
+ {getColumnGroupHeaders({
+ renderContext: leftRenderContext,
+ minFirstColumn: leftRenderContext.firstColumnIndex,
+ maxLastColumn: leftRenderContext.lastColumnIndex,
+ })}
+ {getColumnHeaders(
{
renderContext: leftRenderContext,
minFirstColumn: leftRenderContext.firstColumnIndex,
@@ -173,7 +184,12 @@ export const DataGridProColumnHeaders = React.forwardRef<
)}
- {getColumns({
+ {getColumnGroupHeaders({
+ renderContext,
+ minFirstColumn: leftPinnedColumns.length,
+ maxLastColumn: visibleColumnFields.length - rightPinnedColumns.length,
+ })}
+ {getColumnHeaders({
renderContext,
minFirstColumn: leftPinnedColumns.length,
maxLastColumn: visibleColumnFields.length - rightPinnedColumns.length,
@@ -186,7 +202,12 @@ export const DataGridProColumnHeaders = React.forwardRef<
style={{ paddingRight: scrollbarSize }}
{...pinnedColumnHeadersProps}
>
- {getColumns(
+ {getColumnGroupHeaders({
+ renderContext: rightRenderContext,
+ minFirstColumn: rightRenderContext.firstColumnIndex,
+ maxLastColumn: rightRenderContext.lastColumnIndex,
+ })}
+ {getColumnHeaders(
{
renderContext: rightRenderContext,
minFirstColumn: rightRenderContext.firstColumnIndex,
diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/columnReorder/useGridColumnReorder.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/columnReorder/useGridColumnReorder.tsx
index 673afb7bd7655..baf3674e8df61 100644
--- a/packages/grid/x-data-grid-pro/src/hooks/features/columnReorder/useGridColumnReorder.tsx
+++ b/packages/grid/x-data-grid-pro/src/hooks/features/columnReorder/useGridColumnReorder.tsx
@@ -62,6 +62,7 @@ export const useGridColumnReorder = (
y: 0,
});
const originColumnIndex = React.useRef(null);
+ const forbiddenIndexes = React.useRef<{ [key: number]: boolean }>({});
const removeDnDStylesTimeout = React.useRef();
const ownerState = { classes: props.classes };
const classes = useUtilityClasses(ownerState);
@@ -97,6 +98,66 @@ export const useGridColumnReorder = (
});
originColumnIndex.current = apiRef.current.getColumnIndex(params.field, false);
+
+ const draggingColumnGroupPath = apiRef.current.unstable_getColumnGroupPath(params.field);
+
+ const columnIndex = originColumnIndex.current;
+ const allColumns = apiRef.current.getAllColumns();
+ const groupsLookup = apiRef.current.unstable_getAllGroupDetails();
+
+ // The limitingGroupId is the id of the group from which the dragged column should not escape
+ let limitingGroupId: string | null = null;
+
+ draggingColumnGroupPath.forEach((groupId) => {
+ if (!groupsLookup[groupId]?.freeReordering) {
+ // Only consider group that are made of more than one column
+ if (columnIndex > 0 && allColumns[columnIndex - 1].groupPath?.includes(groupId)) {
+ limitingGroupId = groupId;
+ } else if (
+ columnIndex + 1 < allColumns.length &&
+ allColumns[columnIndex + 1].groupPath?.includes(groupId)
+ ) {
+ limitingGroupId = groupId;
+ }
+ }
+ });
+
+ forbiddenIndexes.current = {};
+
+ for (let indexToForbid = 0; indexToForbid < allColumns.length; indexToForbid += 1) {
+ const leftIndex = indexToForbid <= columnIndex ? indexToForbid - 1 : indexToForbid;
+ const rightIndex = indexToForbid < columnIndex ? indexToForbid : indexToForbid + 1;
+
+ if (limitingGroupId !== null) {
+ // verify this indexToForbid will be linked to the limiting group. Otherwise forbid it
+ let allowIndex = false;
+ if (leftIndex >= 0 && allColumns[leftIndex].groupPath?.includes(limitingGroupId)) {
+ allowIndex = true;
+ } else if (
+ rightIndex < allColumns.length &&
+ allColumns[rightIndex].groupPath?.includes(limitingGroupId)
+ ) {
+ allowIndex = true;
+ }
+ if (!allowIndex) {
+ forbiddenIndexes.current[indexToForbid] = true;
+ }
+ }
+
+ // Verify we are not splitting another group
+ if (leftIndex >= 0 && rightIndex < allColumns.length) {
+ allColumns[rightIndex]?.groupPath?.forEach((groupId) => {
+ if (allColumns[leftIndex].groupPath?.includes(groupId)) {
+ if (!draggingColumnGroupPath.includes(groupId)) {
+ // moving here split the group groupId in two distincts chunks
+ if (!groupsLookup[groupId]?.freeReordering) {
+ forbiddenIndexes.current[indexToForbid] = true;
+ }
+ }
+ }
+ });
+ }
+ }
},
[props.disableColumnReorder, classes.columnHeaderDragging, logger, apiRef],
);
@@ -136,6 +197,7 @@ export const useGridColumnReorder = (
const targetCol = apiRef.current.getColumn(params.field);
const dragColIndex = apiRef.current.getColumnIndex(dragColField, false);
const visibleColumns = apiRef.current.getVisibleColumns();
+ const allColumns = apiRef.current.getAllColumns();
const cursorMoveDirectionX = getCursorMoveDirectionX(cursorPosition.current, coordinates);
const hasMovedLeft =
@@ -145,15 +207,55 @@ export const useGridColumnReorder = (
if (hasMovedLeft || hasMovedRight) {
let canBeReordered: boolean;
+ let indexOffsetInHiddenColumns = 0;
if (!targetCol.disableReorder) {
canBeReordered = true;
} else if (hasMovedLeft) {
canBeReordered =
- targetColIndex > 0 && !visibleColumns[targetColIndex - 1].disableReorder;
+ targetColVisibleIndex > 0 &&
+ !visibleColumns[targetColVisibleIndex - 1].disableReorder;
} else {
canBeReordered =
- targetColIndex < visibleColumns.length - 1 &&
- !visibleColumns[targetColIndex + 1].disableReorder;
+ targetColVisibleIndex < visibleColumns.length - 1 &&
+ !visibleColumns[targetColVisibleIndex + 1].disableReorder;
+ }
+
+ if (forbiddenIndexes.current[targetColIndex]) {
+ let nextVisibleColumnField: string | null;
+ let indexWithOffset = targetColIndex + indexOffsetInHiddenColumns;
+ if (hasMovedLeft) {
+ nextVisibleColumnField =
+ targetColVisibleIndex > 0 ? visibleColumns[targetColVisibleIndex - 1].field : null;
+ while (
+ indexWithOffset > 0 &&
+ allColumns[indexWithOffset].field !== nextVisibleColumnField &&
+ forbiddenIndexes.current[indexWithOffset]
+ ) {
+ indexOffsetInHiddenColumns -= 1;
+ indexWithOffset = targetColIndex + indexOffsetInHiddenColumns;
+ }
+ } else {
+ nextVisibleColumnField =
+ targetColVisibleIndex + 1 < visibleColumns.length
+ ? visibleColumns[targetColVisibleIndex + 1].field
+ : null;
+ while (
+ indexWithOffset < allColumns.length - 1 &&
+ allColumns[indexWithOffset].field !== nextVisibleColumnField &&
+ forbiddenIndexes.current[indexWithOffset]
+ ) {
+ indexOffsetInHiddenColumns += 1;
+ indexWithOffset = targetColIndex + indexOffsetInHiddenColumns;
+ }
+ }
+
+ if (
+ forbiddenIndexes.current[indexWithOffset] ||
+ allColumns[indexWithOffset].field === nextVisibleColumnField
+ ) {
+ // If we ended up on a visible column, or a forbidden one, we can not do the reorder
+ canBeReordered = false;
+ }
}
const canBeReorderedProcessed = apiRef.current.unstable_applyPipeProcessors(
@@ -163,7 +265,10 @@ export const useGridColumnReorder = (
);
if (canBeReorderedProcessed) {
- apiRef.current.setColumnIndex(dragColField, targetColIndex);
+ apiRef.current.setColumnIndex(
+ dragColField,
+ targetColIndex + indexOffsetInHiddenColumns,
+ );
}
}
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 adb2fd329b9f2..ed2e4480bda88 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
@@ -22,6 +22,7 @@ import {
findGridCellElementsFromCol,
getFieldFromHeaderElem,
findHeaderElementFromField,
+ findGroupHeaderElementsFromField,
} from '../../../utils/domUtils';
import { GridApiPro } from '../../../models/gridApiPro';
import { DataGridProProcessedProps } from '../../../models/dataGridProProps';
@@ -132,6 +133,7 @@ export const useGridColumnResize = (
const colDefRef = React.useRef();
const colElementRef = React.useRef();
+ const colGroupingElementRef = React.useRef();
const colCellElementsRef = React.useRef();
const theme = useTheme();
@@ -158,7 +160,7 @@ export const useGridColumnResize = (
colElementRef.current!.style.minWidth = `${newWidth}px`;
colElementRef.current!.style.maxWidth = `${newWidth}px`;
- colCellElementsRef.current!.forEach((element) => {
+ [...colCellElementsRef.current!, ...colGroupingElementRef.current!].forEach((element) => {
const div = element as HTMLDivElement;
let finalWidth: `${number}px`;
@@ -252,6 +254,11 @@ export const useGridColumnResize = (
`[data-field="${colDef.field}"]`,
)!;
+ colGroupingElementRef.current = findGroupHeaderElementsFromField(
+ apiRef.current.columnHeadersContainerElementRef?.current!,
+ colDef.field,
+ );
+
colCellElementsRef.current = findGridCellElementsFromCol(
colElementRef.current,
apiRef.current,
@@ -351,6 +358,10 @@ export const useGridColumnResize = (
const field = getFieldFromHeaderElem(colElementRef.current!);
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);
diff --git a/packages/grid/x-data-grid-pro/src/tests/columnReorder.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/columnReorder.DataGridPro.test.tsx
index 8e128217449ec..194c17c26dffc 100644
--- a/packages/grid/x-data-grid-pro/src/tests/columnReorder.DataGridPro.test.tsx
+++ b/packages/grid/x-data-grid-pro/src/tests/columnReorder.DataGridPro.test.tsx
@@ -424,4 +424,426 @@ describe(' - Columns reorder', () => {
expect(handleDragOver.callCount).to.equal(0);
expect(handleDragEnd.callCount).to.equal(0);
});
+
+ describe('reorder with column grouping', () => {
+ it('should not allow to drag column outside of its group', () => {
+ const rows = [{ id: 0 }];
+ const columns = [{ field: 'col1' }, { field: 'col2' }, { field: 'col3' }];
+
+ const columnGroupingModel = [
+ { groupId: 'col12', children: [{ field: 'col1' }, { field: 'col2' }] },
+ ];
+
+ const Test = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col12', '', 'col1', 'col2', 'col3']);
+ const dragCol = getColumnHeaderCell(0, 1).firstChild!;
+ const targetCol = getColumnHeaderCell(2, 1).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent2 = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent2);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col12', '', 'col1', 'col2', 'col3']);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col12', '', 'col1', 'col2', 'col3']);
+ });
+
+ describe('column - hidden', () => {
+ it('should use the correct start and end index', () => {
+ const rows = [{ id: 0 }];
+ const columns = [
+ { field: 'col1' },
+ { field: 'col2' },
+ { field: 'col3' },
+ { field: 'col4' },
+ ];
+
+ const columnGroupingModel = [
+ { groupId: 'col23', children: [{ field: 'col2' }, { field: 'col3' }] },
+ ];
+
+ const Test = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col23', '', 'col2', 'col3', 'col4']);
+ const dragCol = getColumnHeaderCell(0, 1).firstChild!;
+ const col3 = getColumnHeaderCell(1, 1).firstChild!;
+ const col4 = getColumnHeaderCell(2, 1).firstChild!;
+
+ // Do not allow to move col2 after col4
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(col3);
+ const dragOverEvent1 = createDragOverEvent(col3);
+ fireEvent(col3, dragOverEvent1);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col23', '', 'col3', 'col2', 'col4']);
+
+ // Allow to move col2 after col3
+ fireEvent.dragEnter(col4);
+ const dragOverEvent2 = createDragOverEvent(col4);
+ fireEvent(col4, dragOverEvent2);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col23', '', 'col3', 'col2', 'col4']);
+ });
+
+ it('should consider moving the column between hidden columns if it respect group constraint and visible behavior', () => {
+ const rows = [{ id: 0 }];
+ const columns = [{ field: 'col1' }, { field: 'col2' }, { field: 'col3' }];
+
+ const columnGroupingModel = [
+ { groupId: 'col23', children: [{ field: 'col2' }, { field: 'col3' }] },
+ ];
+
+ const Test = (props: any) => {
+ return (
+
+
+
+ );
+ };
+
+ const { setProps } = render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['', 'col23', 'col1', 'col2']);
+ const dragCol = getColumnHeaderCell(0, 1).firstChild!;
+ const targetCol = getColumnHeaderCell(1, 1).firstChild!;
+
+ // Move col 1 after col 3 to respect column grouping consistency even if col3 is hidden
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col23', '', 'col2', 'col1']);
+
+ setProps({ columnVisibilityModel: {} });
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col23', '', 'col2', 'col3', 'col1']);
+ });
+ });
+
+ it('should not allow to drag column inside a group', () => {
+ const rows = [{ id: 0 }];
+ const columns = [{ field: 'col1' }, { field: 'col2' }, { field: 'col3' }];
+
+ const columnGroupingModel = [
+ { groupId: 'col12', children: [{ field: 'col1' }, { field: 'col2' }] },
+ ];
+
+ const Test = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col12', '', 'col1', 'col2', 'col3']);
+ const dragCol = getColumnHeaderCell(2, 1).firstChild!;
+ const targetCol = getColumnHeaderCell(1, 1).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent2 = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent2);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col12', '', 'col1', 'col2', 'col3']);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col12', '', 'col1', 'col2', 'col3']);
+ });
+
+ it('should allow to drag column outside of its group if it allows freeReordering', () => {
+ const rows = [{ id: 0 }];
+ const columns = [{ field: 'col1' }, { field: 'col2' }, { field: 'col3' }];
+
+ const columnGroupingModel = [
+ {
+ groupId: 'col12',
+ children: [{ field: 'col1' }, { field: 'col2' }],
+ freeReordering: true,
+ },
+ ];
+
+ const Test = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['col12', '', 'col1', 'col2', 'col3']);
+ const dragCol = getColumnHeaderCell(0, 1).firstChild!;
+ const targetCol = getColumnHeaderCell(2, 1).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent2 = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent2);
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col12',
+ '',
+ 'col12',
+ 'col2',
+ 'col3',
+ 'col1',
+ ]);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col12',
+ '',
+ 'col12',
+ 'col2',
+ 'col3',
+ 'col1',
+ ]);
+ });
+
+ it('should allow to drag column inside a group if it allows freeReordering', () => {
+ // TODO: I observed columns are always moved from left to right
+ // The reason being that is:
+ // - when event.clientX does not change we consider that column is moving to the right
+ // - fireEvent.dragStart always set event.clientX = 1 (did not managed to modify this behavior)
+ const rows = [{ id: 0 }];
+ const columns = [{ field: 'col1' }, { field: 'col2' }, { field: 'col3' }];
+
+ const columnGroupingModel = [
+ {
+ groupId: 'col23',
+ children: [{ field: 'col2' }, { field: 'col3' }],
+ freeReordering: true,
+ },
+ ];
+
+ const Test = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['', 'col23', 'col1', 'col2', 'col3']);
+ const dragCol = getColumnHeaderCell(0, 1).firstChild!;
+ const targetCol = getColumnHeaderCell(1, 1).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent2 = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent2);
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col23',
+ '',
+ 'col23',
+ 'col2',
+ 'col1',
+ 'col3',
+ ]);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col23',
+ '',
+ 'col23',
+ 'col2',
+ 'col1',
+ 'col3',
+ ]);
+ });
+
+ it('should allow to split a group with freeReordering in another group', () => {
+ const rows = [{ id: 0 }];
+ const columns = [{ field: 'col1' }, { field: 'col2' }, { field: 'col3' }];
+
+ const columnGroupingModel = [
+ {
+ groupId: 'col123',
+ children: [
+ { field: 'col1' },
+ {
+ groupId: 'col23',
+ children: [{ field: 'col2' }, { field: 'col3' }],
+ freeReordering: true,
+ },
+ ],
+ },
+ ];
+
+ const Test = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col123',
+ '',
+ 'col23',
+ 'col1',
+ 'col2',
+ 'col3',
+ ]);
+ const dragCol = getColumnHeaderCell(0, 2).firstChild!;
+ const targetCol = getColumnHeaderCell(1, 2).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent2 = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent2);
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col123',
+ 'col23',
+ '',
+ 'col23',
+ 'col2',
+ 'col1',
+ 'col3',
+ ]);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col123',
+ 'col23',
+ '',
+ 'col23',
+ 'col2',
+ 'col1',
+ 'col3',
+ ]);
+ });
+
+ it('should block dragging outside of a group even at deeper level', () => {
+ const rows = [{ id: 0 }];
+ const columns = [{ field: 'col1' }, { field: 'col2' }, { field: 'col3' }];
+
+ const columnGroupingModel = [
+ {
+ groupId: 'col12',
+ children: [
+ { field: 'col1' },
+ {
+ groupId: 'col2',
+ children: [{ field: 'col2' }],
+ freeReordering: true,
+ },
+ ],
+ },
+ ];
+
+ const Test = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col12',
+ '',
+ '',
+ 'col2',
+ '',
+ 'col1',
+ 'col2',
+ 'col3',
+ ]);
+ const dragCol = getColumnHeaderCell(0, 1).firstChild!;
+ const targetCol = getColumnHeaderCell(2, 1).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent2 = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent2);
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col12',
+ '',
+ '',
+ 'col2',
+ '',
+ 'col1',
+ 'col2',
+ 'col3',
+ ]);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal([
+ 'col12',
+ '',
+ '',
+ 'col2',
+ '',
+ 'col1',
+ 'col2',
+ 'col3',
+ ]);
+ });
+ });
});
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 ef33671ad9dd6..7b03c91493eac 100644
--- a/packages/grid/x-data-grid-pro/src/utils/domUtils.ts
+++ b/packages/grid/x-data-grid-pro/src/utils/domUtils.ts
@@ -10,6 +10,10 @@ export function findHeaderElementFromField(elem: Element, field: string): Elemen
return elem.querySelector(`[data-field="${field}"]`);
}
+export function findGroupHeaderElementsFromField(elem: Element, field: string): Element[] {
+ return Array.from(elem.querySelectorAll(`[data-fields*="|-${field}-|"]`) ?? []);
+}
+
export function findGridCellElementsFromCol(col: HTMLElement, api: GridApiPro) {
const root = findParentElementFromClassName(col, 'MuiDataGrid-root');
if (!root) {
diff --git a/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx
index bb76a2496ce7f..7058730b11b52 100644
--- a/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx
+++ b/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx
@@ -89,6 +89,7 @@ DataGridRaw.propTypes = {
* @default 3
*/
columnBuffer: PropTypes.number,
+ columnGroupingModel: PropTypes.arrayOf(PropTypes.object),
/**
* Set of columns of type [[GridColumns]].
*/
@@ -191,6 +192,7 @@ DataGridRaw.propTypes = {
* For each feature, if the flag is not explicitly set to `true`, the feature will be fully disabled and any property / method call will not have any effect.
*/
experimentalFeatures: PropTypes.shape({
+ columnGrouping: PropTypes.bool,
newEditingApi: PropTypes.bool,
preventCommitWhileValidating: PropTypes.bool,
warnIfFocusStateIsNotSynced: PropTypes.bool,
diff --git a/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx b/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx
index 491e46132f8fc..3eed1efbf06ea 100644
--- a/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx
+++ b/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx
@@ -45,6 +45,11 @@ import { useGridDimensions } from '../hooks/features/dimensions/useGridDimension
import { rowsMetaStateInitializer, useGridRowsMeta } from '../hooks/features/rows/useGridRowsMeta';
import { useGridStatePersistence } from '../hooks/features/statePersistence/useGridStatePersistence';
import { useGridColumnSpanning } from '../hooks/features/columns/useGridColumnSpanning';
+import {
+ useGridColumnGrouping,
+ columnGroupsStateInitializer,
+} from '../hooks/features/columnGrouping/useGridColumnGrouping';
+import { useGridColumnGroupingPreProcessors } from '../hooks/features/columnGrouping/useGridColumnGroupingPreProcessors';
export const useDataGridComponent = (props: DataGridProcessedProps) => {
const apiRef = useGridInitialization(undefined, props);
@@ -52,6 +57,7 @@ export const useDataGridComponent = (props: DataGridProcessedProps) => {
/**
* Register all pre-processors called during state initialization here.
*/
+ useGridColumnGroupingPreProcessors(apiRef, props);
useGridSelectionPreProcessors(apiRef, props);
useGridRowsPreProcessors(apiRef);
@@ -60,6 +66,7 @@ export const useDataGridComponent = (props: DataGridProcessedProps) => {
*/
useGridInitializeState(selectionStateInitializer, apiRef, props);
useGridInitializeState(columnsStateInitializer, apiRef, props);
+ useGridInitializeState(columnGroupsStateInitializer, apiRef, props);
useGridInitializeState(rowsStateInitializer, apiRef, props);
useGridInitializeState(
props.experimentalFeatures?.newEditingApi
@@ -83,6 +90,7 @@ export const useDataGridComponent = (props: DataGridProcessedProps) => {
useGridRows(apiRef, props);
useGridParamsApi(apiRef);
useGridColumnSpanning(apiRef);
+ useGridColumnGrouping(apiRef, props);
const useGridEditing = props.experimentalFeatures?.newEditingApi
? useGridEditing_new
diff --git a/packages/grid/x-data-grid/src/components/DataGridColumnHeaders.tsx b/packages/grid/x-data-grid/src/components/DataGridColumnHeaders.tsx
index 5ceac2efb1ae2..0888cb7a3d165 100644
--- a/packages/grid/x-data-grid/src/components/DataGridColumnHeaders.tsx
+++ b/packages/grid/x-data-grid/src/components/DataGridColumnHeaders.tsx
@@ -12,15 +12,17 @@ export const DataGridColumnHeaders = React.forwardRef
- {getColumns()}
+ {getColumnGroupHeaders()}
+ {getColumnHeaders()}
diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx
index fc837b9568fe2..f570c14f170c7 100644
--- a/packages/grid/x-data-grid/src/components/GridRow.tsx
+++ b/packages/grid/x-data-grid/src/components/GridRow.tsx
@@ -29,6 +29,7 @@ import { GridRenderEditCellParams } from '../models/params/gridCellParams';
import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../constants/gridDetailPanelToggleField';
import { gridSortModelSelector } from '../hooks/features/sorting/gridSortingSelector';
import { gridRowTreeDepthSelector } from '../hooks/features/rows/gridRowsSelector';
+import { gridDensityHeaderGroupingMaxDepthSelector } from '../hooks/features/density/densitySelector';
export interface GridRowProps {
rowId: GridRowId;
@@ -113,7 +114,6 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) {
onMouseLeave,
...other
} = props;
- const ariaRowIndex = index + 2; // 1 for the header row and 1 as it's 1-based
const apiRef = useGridApiContext();
const ref = React.useRef(null);
const rootProps = useGridRootProps();
@@ -121,6 +121,9 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) {
const columnsTotalWidth = useGridSelector(apiRef, gridColumnsTotalWidthSelector);
const sortModel = useGridSelector(apiRef, gridSortModelSelector);
const treeDepth = useGridSelector(apiRef, gridRowTreeDepthSelector);
+ const headerGroupingMaxDepth = useGridSelector(apiRef, gridDensityHeaderGroupingMaxDepthSelector);
+
+ const ariaRowIndex = index + headerGroupingMaxDepth + 2; // 1 for the header row and 1 as it's 1-based
const { hasScrollX, hasScrollY } = apiRef.current.getRootDimensions() ?? {
hasScrollX: false,
hasScrollY: false,
diff --git a/packages/grid/x-data-grid/src/components/base/GridBody.tsx b/packages/grid/x-data-grid/src/components/base/GridBody.tsx
index 7150071e596ed..83061a9b324f5 100644
--- a/packages/grid/x-data-grid/src/components/base/GridBody.tsx
+++ b/packages/grid/x-data-grid/src/components/base/GridBody.tsx
@@ -7,7 +7,7 @@ import { GridAutoSizer } from '../GridAutoSizer';
import { GridOverlays } from './GridOverlays';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { useGridSelector } from '../../hooks/utils/useGridSelector';
-import { gridDensityHeaderHeightSelector } from '../../hooks/features/density/densitySelector';
+import { gridDensityTotalHeaderHeightSelector } from '../../hooks/features/density/densitySelector';
interface GridBodyProps {
children?: React.ReactNode;
@@ -30,7 +30,7 @@ function GridBody(props: GridBodyProps) {
const { children, VirtualScrollerComponent, ColumnHeadersComponent } = props;
const apiRef = useGridApiContext();
const rootProps = useGridRootProps();
- const headerHeight = useGridSelector(apiRef, gridDensityHeaderHeightSelector);
+ const totalHeaderHeight = useGridSelector(apiRef, gridDensityTotalHeaderHeightSelector);
const [isVirtualizationDisabled, setIsVirtualizationDisabled] = React.useState(
rootProps.disableVirtualization,
);
@@ -83,8 +83,8 @@ function GridBody(props: GridBodyProps) {
width: size.width,
// If `autoHeight` is on, there will be no height value.
// In this case, let the container to grow whatever it needs.
- height: size.height ? size.height - headerHeight : 'auto',
- marginTop: headerHeight,
+ height: size.height ? size.height - totalHeaderHeight : 'auto',
+ marginTop: totalHeaderHeight,
} as React.CSSProperties;
return (
diff --git a/packages/grid/x-data-grid/src/components/base/GridOverlays.tsx b/packages/grid/x-data-grid/src/components/base/GridOverlays.tsx
index ce3a618e5796b..ea5f54c56dc3a 100644
--- a/packages/grid/x-data-grid/src/components/base/GridOverlays.tsx
+++ b/packages/grid/x-data-grid/src/components/base/GridOverlays.tsx
@@ -8,12 +8,12 @@ import {
} from '../../hooks/features/rows/gridRowsSelector';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
-import { gridDensityHeaderHeightSelector } from '../../hooks/features/density/densitySelector';
+import { gridDensityTotalHeaderHeightSelector } from '../../hooks/features/density/densitySelector';
function GridOverlayWrapper(props: React.PropsWithChildren<{}>) {
const apiRef = useGridApiContext();
const rootProps = useGridRootProps();
- const headerHeight = useGridSelector(apiRef, gridDensityHeaderHeightSelector);
+ const totalHeaderHeight = useGridSelector(apiRef, gridDensityTotalHeaderHeightSelector);
const [viewportInnerSize, setViewportInnerSize] = React.useState(
() => apiRef.current.getRootDimensions()?.viewportInnerSize ?? null,
@@ -42,7 +42,7 @@ function GridOverlayWrapper(props: React.PropsWithChildren<{}>) {
height,
width: viewportInnerSize?.width ?? 0,
position: 'absolute',
- top: headerHeight,
+ top: totalHeaderHeight,
bottom: height === 'auto' ? 0 : undefined,
}}
{...props}
diff --git a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnGroupHeader.tsx b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnGroupHeader.tsx
new file mode 100644
index 0000000000000..2e9f74064462c
--- /dev/null
+++ b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnGroupHeader.tsx
@@ -0,0 +1,157 @@
+import * as React from 'react';
+import { unstable_useId as useId } from '@mui/utils';
+import { unstable_composeClasses as composeClasses } from '@mui/material';
+import { GridAlignment } from '../../models/colDef/gridColDef';
+import { getDataGridUtilityClass } from '../../constants/gridClasses';
+import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
+import { DataGridProcessedProps } from '../../models/props/DataGridProps';
+import { gridColumnGroupsLookupSelector } from '../../hooks/features/columnGrouping/gridColumnGroupsSelector';
+import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
+import { useGridSelector } from '../../hooks/utils/useGridSelector';
+import { GridGenericColumnHeaderItem } from './GridGenericColumnHeaderItem';
+import { GridColumnGroup } from '../../models/gridColumnGrouping';
+
+interface GridColumnGroupHeaderProps {
+ groupId: string | null;
+ width: number;
+ fields: string[];
+ colIndex: number; // TODO: use this prop to get accessible column group
+ isLastColumn: boolean;
+ extendRowFullWidth: boolean;
+ depth: number;
+ maxDepth: number;
+ height: number;
+}
+
+type OwnerState = {
+ groupId: GridColumnGroupHeaderProps['groupId'];
+ showRightBorder: boolean;
+ showColumnBorder: boolean;
+ isDragging: boolean;
+ headerAlign?: GridAlignment;
+ classes?: DataGridProcessedProps['classes'];
+};
+
+const useUtilityClasses = (ownerState: OwnerState) => {
+ const { classes, headerAlign, isDragging, showRightBorder, showColumnBorder, groupId } =
+ ownerState;
+
+ const slots = {
+ root: [
+ 'columnHeader',
+ headerAlign === 'left' && 'columnHeader--alignLeft',
+ headerAlign === 'center' && 'columnHeader--alignCenter',
+ headerAlign === 'right' && 'columnHeader--alignRight',
+ isDragging && 'columnHeader--moving',
+ showRightBorder && 'withBorder',
+ showColumnBorder && 'columnHeader--showColumnBorder',
+ groupId === null ? 'columnHeader--emptyGroup' : 'columnHeader--filledGroup',
+ ],
+ draggableContainer: ['columnHeaderDraggableContainer'],
+ titleContainer: ['columnHeaderTitleContainer'],
+ titleContainerContent: ['columnHeaderTitleContainerContent'],
+ };
+
+ return composeClasses(slots, getDataGridUtilityClass, classes);
+};
+
+function GridColumnGroupHeader(props: GridColumnGroupHeaderProps) {
+ const {
+ groupId,
+ width,
+ depth,
+ maxDepth,
+ fields,
+ height,
+ colIndex,
+ isLastColumn,
+ extendRowFullWidth,
+ } = props;
+
+ const rootProps = useGridRootProps();
+
+ const apiRef = useGridApiContext();
+ const columnGroupsLookup = useGridSelector(apiRef, gridColumnGroupsLookupSelector);
+ const { hasScrollX, hasScrollY } = apiRef.current.getRootDimensions() ?? {
+ hasScrollX: false,
+ hasScrollY: false,
+ };
+
+ const group: Partial = groupId ? columnGroupsLookup[groupId] : {};
+
+ const { headerName = groupId ?? '', description = '', headerAlign = undefined } = group;
+
+ let headerComponent: React.ReactNode;
+
+ const render = groupId && columnGroupsLookup[groupId]?.renderHeaderGroup;
+ const renderParams = {
+ groupId,
+ headerName,
+ description,
+ depth,
+ maxDepth,
+ fields,
+ colIndex,
+ isLastColumn,
+ };
+ if (groupId && render) {
+ headerComponent = render(renderParams);
+ }
+
+ const removeLastBorderRight = isLastColumn && hasScrollX && !hasScrollY;
+ const showRightBorder = !isLastColumn
+ ? rootProps.showColumnRightBorder
+ : !removeLastBorderRight && !extendRowFullWidth;
+
+ const showColumnBorder = rootProps.showColumnRightBorder;
+
+ const ownerState = {
+ ...props,
+ classes: rootProps.classes,
+ showRightBorder,
+ showColumnBorder,
+ headerAlign,
+ depth,
+ isDragging: false,
+ };
+
+ const label = headerName ?? groupId;
+
+ const id = useId();
+ const elementId = groupId === null ? `empty-group-cell-${id}` : groupId;
+ const classes = useUtilityClasses(ownerState);
+
+ const headerClassName =
+ typeof group.headerClassName === 'function'
+ ? group.headerClassName(renderParams)
+ : group.headerClassName;
+
+ return (
+
+ );
+}
+
+export { GridColumnGroupHeader };
diff --git a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaderItem.tsx b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaderItem.tsx
index 83cf862460b9d..65b0e010d5884 100644
--- a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaderItem.tsx
+++ b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeaderItem.tsx
@@ -1,23 +1,19 @@
import * as React from 'react';
import PropTypes from 'prop-types';
-import clsx from 'clsx';
import { unstable_composeClasses as composeClasses } from '@mui/material';
import { unstable_useId as useId } from '@mui/material/utils';
-import { GridColumnHeaderEventLookup } from '../../models/events';
import { GridStateColDef } from '../../models/colDef/gridColDef';
import { GridSortDirection } from '../../models/gridSortModel';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
import { GridColumnHeaderSortIcon } from './GridColumnHeaderSortIcon';
-import { GridColumnHeaderTitle } from './GridColumnHeaderTitle';
-import {
- GridColumnHeaderSeparator,
- GridColumnHeaderSeparatorProps,
-} from './GridColumnHeaderSeparator';
+import { GridColumnHeaderSeparatorProps } from './GridColumnHeaderSeparator';
import { ColumnHeaderMenuIcon } from './ColumnHeaderMenuIcon';
import { GridColumnHeaderMenu } from '../menu/columnMenu/GridColumnHeaderMenu';
import { getDataGridUtilityClass } from '../../constants/gridClasses';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { DataGridProcessedProps } from '../../models/props/DataGridProps';
+import { GridGenericColumnHeaderItem } from './GridGenericColumnHeaderItem';
+import { GridColumnHeaderEventLookup } from '../../models/events';
interface GridColumnHeaderItemProps {
colIndex: number;
@@ -106,11 +102,24 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) {
[rootProps.disableColumnReorder, disableReorder, column.disableReorder],
);
- let headerComponent: React.ReactNode = null;
+ let headerComponent: React.ReactNode;
if (column.renderHeader) {
headerComponent = column.renderHeader(apiRef.current.getColumnHeaderParams(column.field));
}
+ const removeLastBorderRight = isLastColumn && hasScrollX && !hasScrollY;
+ const showRightBorder = !isLastColumn
+ ? rootProps.showColumnRightBorder
+ : !removeLastBorderRight && !extendRowFullWidth;
+
+ const ownerState = {
+ ...props,
+ classes: rootProps.classes,
+ showRightBorder,
+ };
+
+ const classes = useUtilityClasses(ownerState);
+
const publish = React.useCallback(
(eventName: keyof GridColumnHeaderEventLookup) => (event: React.SyntheticEvent) => {
// Ignore portal
@@ -127,46 +136,38 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) {
[apiRef, column.field],
);
- const mouseEventsHandlers = {
- onClick: publish('columnHeaderClick'),
- onDoubleClick: publish('columnHeaderDoubleClick'),
- onMouseOver: publish('columnHeaderOver'), // TODO remove as it's not used
- onMouseOut: publish('columnHeaderOut'), // TODO remove as it's not used
- onMouseEnter: publish('columnHeaderEnter'), // TODO remove as it's not used
- onMouseLeave: publish('columnHeaderLeave'), // TODO remove as it's not used
- onKeyDown: publish('columnHeaderKeyDown'),
- onFocus: publish('columnHeaderFocus'),
- onBlur: publish('columnHeaderBlur'),
- };
-
- const draggableEventHandlers = isDraggable
- ? {
- onDragStart: publish('columnHeaderDragStart'),
- onDragEnter: publish('columnHeaderDragEnter'),
- onDragOver: publish('columnHeaderDragOver'),
- onDragEnd: publish('columnHeaderDragEnd'),
- }
- : null;
-
- const removeLastBorderRight = isLastColumn && hasScrollX && !hasScrollY;
- const showRightBorder = !isLastColumn
- ? rootProps.showColumnRightBorder
- : !removeLastBorderRight && !extendRowFullWidth;
-
- const ownerState = {
- ...props,
- classes: rootProps.classes,
- showRightBorder,
- };
-
- const classes = useUtilityClasses(ownerState);
+ const mouseEventsHandlers = React.useMemo(
+ () => ({
+ onClick: publish('columnHeaderClick'),
+ onDoubleClick: publish('columnHeaderDoubleClick'),
+ onMouseOver: publish('columnHeaderOver'), // TODO remove as it's not used
+ onMouseOut: publish('columnHeaderOut'), // TODO remove as it's not used
+ onMouseEnter: publish('columnHeaderEnter'), // TODO remove as it's not used
+ onMouseLeave: publish('columnHeaderLeave'), // TODO remove as it's not used
+ onKeyDown: publish('columnHeaderKeyDown'),
+ onFocus: publish('columnHeaderFocus'),
+ onBlur: publish('columnHeaderBlur'),
+ }),
+ [publish],
+ );
- const width = column.computedWidth;
+ const draggableEventHandlers = React.useMemo(
+ () =>
+ isDraggable
+ ? {
+ onDragStart: publish('columnHeaderDragStart'),
+ onDragEnter: publish('columnHeaderDragEnter'),
+ onDragOver: publish('columnHeaderDragOver'),
+ onDragEnd: publish('columnHeaderDragEnd'),
+ }
+ : {},
+ [isDraggable, publish],
+ );
- let ariaSort: 'ascending' | 'descending' | 'none' = 'none';
- if (sortDirection != null) {
- ariaSort = sortDirection === 'asc' ? 'ascending' : 'descending';
- }
+ const columnHeaderSeparatorProps = React.useMemo(
+ () => ({ onMouseDown: publish('columnSeparatorMouseDown') }),
+ [publish],
+ );
React.useEffect(() => {
if (!showColumnMenuIcon) {
@@ -188,6 +189,19 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) {
/>
);
+ const columnMenu = (
+
+ );
+
const sortingOrder: GridSortDirection[] = column.sortingOrder ?? rootProps.sortingOrder;
const columnTitleIconButtons = (
@@ -228,61 +242,33 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) {
const label = column.headerName ?? column.field;
return (
-
-
-
-
- {column.renderHeader ? (
- headerComponent
- ) : (
-
- )}
-
- {columnTitleIconButtons}
-
- {columnMenuIconButton}
-
-
-
-
+ />
);
}
diff --git a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeadersInner.tsx b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeadersInner.tsx
index ff12b3c2b7d41..df6a1ec961d93 100644
--- a/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeadersInner.tsx
+++ b/packages/grid/x-data-grid/src/components/columnHeaders/GridColumnHeadersInner.tsx
@@ -36,7 +36,8 @@ const GridColumnHeadersInnerRoot = styled('div', {
],
})(() => ({
display: 'flex',
- alignItems: 'center',
+ alignItems: 'flex-start',
+ flexDirection: 'column',
[`&.${gridClasses.columnHeaderDropZone} .${gridClasses.columnHeaderDraggableContainer}`]: {
cursor: 'move',
},
diff --git a/packages/grid/x-data-grid/src/components/columnHeaders/GridGenericColumnHeaderItem.tsx b/packages/grid/x-data-grid/src/components/columnHeaders/GridGenericColumnHeaderItem.tsx
new file mode 100644
index 0000000000000..54a51f5da338a
--- /dev/null
+++ b/packages/grid/x-data-grid/src/components/columnHeaders/GridGenericColumnHeaderItem.tsx
@@ -0,0 +1,152 @@
+import * as React from 'react';
+import clsx from 'clsx';
+import { useForkRef } from '@mui/material/utils';
+import { GridStateColDef } from '../../models/colDef/gridColDef';
+import { GridSortDirection } from '../../models/gridSortModel';
+import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
+import { GridColumnHeaderTitle } from './GridColumnHeaderTitle';
+import {
+ GridColumnHeaderSeparator,
+ GridColumnHeaderSeparatorProps,
+} from './GridColumnHeaderSeparator';
+import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
+import { GridColumnGroup } from '../../models/gridColumnGrouping';
+
+interface GridGenericColumnHeaderItemProps
+ extends Pick {
+ classes: Record<
+ 'root' | 'draggableContainer' | 'titleContainer' | 'titleContainerContent',
+ string
+ >;
+ colIndex: number;
+ columnMenuOpen: boolean;
+ height: number;
+ isResizing: boolean;
+ sortDirection: GridSortDirection;
+ sortIndex?: number;
+ filterItemsCounter?: number;
+ hasFocus?: boolean;
+ tabIndex: 0 | -1;
+ disableReorder?: boolean;
+ separatorSide?: GridColumnHeaderSeparatorProps['side'];
+ headerComponent?: React.ReactNode;
+ elementId: GridStateColDef['field'] | GridColumnGroup['groupId'];
+ isDraggable: boolean;
+ width: number;
+ columnMenuIconButton?: React.ReactNode;
+ columnMenu?: React.ReactNode;
+ columnTitleIconButtons?: React.ReactNode;
+ label: string;
+ draggableContainerProps?: Partial>;
+ columnHeaderSeparatorProps?: Partial;
+ disableHeaderSeparator?: boolean;
+}
+
+const GridGenericColumnHeaderItem = React.forwardRef(function GridGenericColumnHeaderItem(
+ props: GridGenericColumnHeaderItemProps,
+ ref,
+) {
+ const {
+ classes,
+ columnMenuOpen,
+ colIndex,
+ height,
+ isResizing,
+ sortDirection,
+ hasFocus,
+ tabIndex,
+ separatorSide,
+ isDraggable,
+ headerComponent,
+ description,
+ elementId,
+ width,
+ columnMenuIconButton = null,
+ columnMenu = null,
+ columnTitleIconButtons = null,
+ headerClassName,
+ label,
+ resizable,
+ draggableContainerProps,
+ columnHeaderSeparatorProps,
+ disableHeaderSeparator,
+ ...other
+ } = props;
+
+ const apiRef = useGridApiContext();
+ const rootProps = useGridRootProps();
+ const headerCellRef = React.useRef(null);
+ const [showColumnMenuIcon, setShowColumnMenuIcon] = React.useState(columnMenuOpen);
+
+ const handleRef = useForkRef(headerCellRef, ref);
+
+ let ariaSort: 'ascending' | 'descending' | 'none' = 'none';
+ if (sortDirection != null) {
+ ariaSort = sortDirection === 'asc' ? 'ascending' : 'descending';
+ }
+
+ React.useEffect(() => {
+ if (!showColumnMenuIcon) {
+ setShowColumnMenuIcon(columnMenuOpen);
+ }
+ }, [showColumnMenuIcon, columnMenuOpen]);
+
+ React.useLayoutEffect(() => {
+ const columnMenuState = apiRef.current.state.columnMenu;
+ if (hasFocus && !columnMenuState.open) {
+ const focusableElement = headerCellRef.current!.querySelector('[tabindex="0"]');
+ const elementToFocus = focusableElement || headerCellRef.current;
+ elementToFocus?.focus();
+ apiRef.current.columnHeadersContainerElementRef!.current!.scrollLeft = 0;
+ }
+ }, [apiRef, hasFocus]);
+
+ return (
+
+
+
+
+ {headerComponent !== undefined ? (
+ headerComponent
+ ) : (
+
+ )}
+
+ {columnTitleIconButtons}
+
+ {columnMenuIconButton}
+
+ {!disableHeaderSeparator && (
+
+ )}
+ {columnMenu}
+
+ );
+});
+
+export { GridGenericColumnHeaderItem };
diff --git a/packages/grid/x-data-grid/src/components/containers/GridRoot.tsx b/packages/grid/x-data-grid/src/components/containers/GridRoot.tsx
index 7b3971d05007d..9f5c85b859ea8 100644
--- a/packages/grid/x-data-grid/src/components/containers/GridRoot.tsx
+++ b/packages/grid/x-data-grid/src/components/containers/GridRoot.tsx
@@ -16,11 +16,14 @@ import { useGridSelector } from '../../hooks/utils/useGridSelector';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { getDataGridUtilityClass } from '../../constants/gridClasses';
+import {
+ gridDensityHeaderGroupingMaxDepthSelector,
+ gridDensityValueSelector,
+} from '../../hooks/features/density/densitySelector';
import {
gridPinnedRowsCountSelector,
gridRowCountSelector,
} from '../../hooks/features/rows/gridRowsSelector';
-import { gridDensityValueSelector } from '../../hooks/features/density/densitySelector';
import { DataGridProcessedProps } from '../../models/props/DataGridProps';
import { GridDensity } from '../../models/gridDensity';
@@ -54,6 +57,7 @@ const GridRoot = React.forwardRef(function GridRo
const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector);
const totalRowCount = useGridSelector(apiRef, gridRowCountSelector);
const densityValue = useGridSelector(apiRef, gridDensityValueSelector);
+ const headerGroupingMaxDepth = useGridSelector(apiRef, gridDensityHeaderGroupingMaxDepthSelector);
const rootContainerRef: GridRootContainerRef = React.useRef(null);
const handleRef = useForkRef(rootContainerRef, ref);
const pinnedRowsCount = useGridSelector(apiRef, gridPinnedRowsCountSelector);
@@ -90,7 +94,7 @@ const GridRoot = React.forwardRef(function GridRo
className={clsx(className, classes.root)}
role="grid"
aria-colcount={visibleColumns.length}
- aria-rowcount={totalRowCount + pinnedRowsCount + 1} // +1 for the header row
+ aria-rowcount={headerGroupingMaxDepth + 1 + pinnedRowsCount + totalRowCount}
aria-multiselectable={!rootProps.disableMultipleSelection}
aria-label={rootProps['aria-label']}
aria-labelledby={rootProps['aria-labelledby']}
diff --git a/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts b/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts
index 499f6fae91963..f1f9bd738b751 100644
--- a/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts
+++ b/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts
@@ -145,13 +145,26 @@ export const GridRootStyles = styled('div', {
minWidth: 0,
flex: 1,
whiteSpace: 'nowrap',
- overflowX: 'hidden',
+ overflow: 'hidden',
},
[`& .${gridClasses.columnHeaderTitleContainerContent}`]: {
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
},
+ [`& .${gridClasses['columnHeader--filledGroup']} .${gridClasses.columnHeaderTitleContainer}`]: {
+ borderBottom: `solid ${borderColor} 1px`,
+ boxSizing: 'border-box',
+ },
+ [`& .${gridClasses['columnHeader--filledGroup']}.${gridClasses['columnHeader--showColumnBorder']} .${gridClasses.columnHeaderTitleContainer}`]:
+ {
+ borderBottom: `none`,
+ },
+ [`& .${gridClasses['columnHeader--filledGroup']}.${gridClasses['columnHeader--showColumnBorder']}`]:
+ {
+ borderBottom: `solid ${borderColor} 1px`,
+ boxSizing: 'border-box',
+ },
[`& .${gridClasses.sortIcon}, & .${gridClasses.filterIcon}`]: {
fontSize: 'inherit',
},
@@ -340,6 +353,7 @@ export const GridRootStyles = styled('div', {
[`& .${gridClasses.columnHeaderDraggableContainer}`]: {
display: 'flex',
width: '100%',
+ height: '100%',
},
[`& .${gridClasses.rowReorderCellPlaceholder}`]: {
display: 'none',
diff --git a/packages/grid/x-data-grid/src/constants/gridClasses.ts b/packages/grid/x-data-grid/src/constants/gridClasses.ts
index 982b7eb2eb1d7..ecda4f4d810a3 100644
--- a/packages/grid/x-data-grid/src/constants/gridClasses.ts
+++ b/packages/grid/x-data-grid/src/constants/gridClasses.ts
@@ -113,6 +113,10 @@ export interface GridClasses {
* Styles applied to the column header element.
*/
columnHeader: string;
+ /**
+ * Styles applied to the column group header element.
+ */
+ columnGroupHeader: string;
/**
* Styles applied to the header checkbox cell element.
*/
@@ -141,6 +145,18 @@ export interface GridClasses {
* Styles applied to the column header's title excepted buttons.
*/
columnHeaderTitleContainerContent: string;
+ /**
+ * Styles applied to the column group header cell if not empty.
+ */
+ 'columnHeader--filledGroup': string;
+ /**
+ * Styles applied to the empty column group header cell.
+ */
+ 'columnHeader--emptyGroup': string;
+ /**
+ * Styles applied to the column group header cell when show column border.
+ */
+ 'columnHeader--showColumnBorder': string;
/**
* Styles applied to the column headers.
*/
@@ -504,6 +520,10 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [
'columnHeaderTitle',
'columnHeaderTitleContainer',
'columnHeaderTitleContainerContent',
+ 'columnGroupHeader',
+ 'columnHeader--filledGroup',
+ 'columnHeader--emptyGroup',
+ 'columnHeader--showColumnBorder',
'columnHeaders',
'columnHeadersInner',
'columnHeadersInner--scrollable',
diff --git a/packages/grid/x-data-grid/src/hooks/features/columnGrouping/gridColumnGroupsInterfaces.ts b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/gridColumnGroupsInterfaces.ts
new file mode 100644
index 0000000000000..f34a26ca45ec6
--- /dev/null
+++ b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/gridColumnGroupsInterfaces.ts
@@ -0,0 +1,9 @@
+import { GridColumnGroup } from '../../../models/gridColumnGrouping';
+
+export type GridColumnGroupLookup = {
+ [field: string]: Omit;
+};
+
+export interface GridColumnsGroupingState {
+ lookup: GridColumnGroupLookup;
+}
diff --git a/packages/grid/x-data-grid/src/hooks/features/columnGrouping/gridColumnGroupsSelector.ts b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/gridColumnGroupsSelector.ts
new file mode 100644
index 0000000000000..d97a7a2274b6c
--- /dev/null
+++ b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/gridColumnGroupsSelector.ts
@@ -0,0 +1,13 @@
+import { createSelector } from '../../../utils/createSelector';
+import { GridStateCommunity } from '../../../models/gridStateCommunity';
+
+/**
+ * @category ColumnGrouping
+ * @ignore - do not document.
+ */
+export const gridColumnGroupingSelector = (state: GridStateCommunity) => state.columnGrouping;
+
+export const gridColumnGroupsLookupSelector = createSelector(
+ gridColumnGroupingSelector,
+ (columnGrouping) => columnGrouping.lookup,
+);
diff --git a/packages/grid/x-data-grid/src/hooks/features/columnGrouping/index.ts b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/index.ts
new file mode 100644
index 0000000000000..dbdd20cca9b13
--- /dev/null
+++ b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/index.ts
@@ -0,0 +1,2 @@
+export * from './gridColumnGroupsSelector';
+export type { GridColumnsGroupingState } from './gridColumnGroupsInterfaces';
diff --git a/packages/grid/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGrouping.ts b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGrouping.ts
new file mode 100644
index 0000000000000..5b2e107a8309c
--- /dev/null
+++ b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGrouping.ts
@@ -0,0 +1,172 @@
+import * as React from 'react';
+import { GridApiCommunity } from '../../../models/api/gridApiCommunity';
+import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
+import { GridStateInitializer } from '../../utils/useGridInitializeState';
+import {
+ GridColumnGroupingModel,
+ GridColumnNode,
+ GridColumnGroup,
+ isLeaf,
+} from '../../../models/gridColumnGrouping';
+import { gridColumnGroupsLookupSelector } from './gridColumnGroupsSelector';
+import { gridColumnLookupSelector } from '../columns/gridColumnsSelector';
+import { GridColumnGroupLookup } from './gridColumnGroupsInterfaces';
+import { GridColumnGroupingApi } from '../../../models/api/gridColumnGroupingApi';
+import { useGridApiMethod } from '../../utils/useGridApiMethod';
+import { GridStateColDef, GridColDef } from '../../../models/colDef';
+
+export function hasGroupPath(
+ lookupElement: GridColDef | GridStateColDef,
+): lookupElement is GridStateColDef {
+ return (lookupElement).groupPath !== undefined;
+}
+
+type UnwrappedGroupingModel = { [key: GridColDef['field']]: GridColumnGroup['groupId'][] };
+
+// This is the recurrence function that help writing `unwrapGroupingColumnModel()`
+const recurrentUnwrapGroupingColumnModel = (
+ columnGroupNode: GridColumnNode,
+ parents: GridColumnGroup['groupId'][],
+ unwrappedGroupingModelToComplet: UnwrappedGroupingModel,
+): void => {
+ if (isLeaf(columnGroupNode)) {
+ if (unwrappedGroupingModelToComplet[columnGroupNode.field] !== undefined) {
+ throw new Error(
+ [
+ `MUI: columnGroupingModel contains duplicated field`,
+ `column field ${columnGroupNode.field} occurrs two times in the grouping model:`,
+ `- ${unwrappedGroupingModelToComplet[columnGroupNode.field].join(' > ')}`,
+ `- ${parents.join(' > ')}`,
+ ].join('\n'),
+ );
+ }
+ unwrappedGroupingModelToComplet[columnGroupNode.field] = parents;
+ return;
+ }
+
+ const { groupId, children } = columnGroupNode;
+ children.forEach((child) => {
+ recurrentUnwrapGroupingColumnModel(
+ child,
+ [...parents, groupId],
+ unwrappedGroupingModelToComplet,
+ );
+ });
+};
+
+/**
+ * This is a function that provide for each column the array of its parents.
+ * Parents are ordered from the root to the leaf.
+ * @param columnGroupingModel The model such as provided in DataGrid props
+ * @returns An object `{[field]: groupIds}` where `groupIds` is the parents of the column `field`
+ */
+export const unwrapGroupingColumnModel = (
+ columnGroupingModel?: GridColumnGroupingModel,
+): UnwrappedGroupingModel => {
+ if (!columnGroupingModel) {
+ return {};
+ }
+
+ const unwrappedSubTree: UnwrappedGroupingModel = {};
+ columnGroupingModel.forEach((columnGroupNode) => {
+ recurrentUnwrapGroupingColumnModel(columnGroupNode, [], unwrappedSubTree);
+ });
+
+ return unwrappedSubTree;
+};
+
+const createGroupLookup = (columnGroupingModel: GridColumnNode[]): GridColumnGroupLookup => {
+ let groupLookup: GridColumnGroupLookup = {};
+
+ columnGroupingModel.forEach((node) => {
+ if (isLeaf(node)) {
+ return;
+ }
+ const { groupId, children, ...other } = node;
+ if (!groupId) {
+ throw new Error(
+ 'MUI: An element of the columnGroupingModel does not have either `field` or `groupId`.',
+ );
+ }
+ if (!children) {
+ console.warn(`MUI: group groupId=${groupId} has no children.`);
+ }
+ const groupParam = { ...other, groupId };
+ const subTreeLookup = createGroupLookup(children);
+ if (subTreeLookup[groupId] !== undefined || groupLookup[groupId] !== undefined) {
+ throw new Error(
+ `MUI: The groupId ${groupId} is used multiple times in the columnGroupingModel.`,
+ );
+ }
+ groupLookup = { ...groupLookup, ...subTreeLookup, [groupId]: groupParam };
+ });
+
+ return { ...groupLookup };
+};
+export const columnGroupsStateInitializer: GridStateInitializer<
+ Pick
+> = (state, props) => {
+ const groupLookup = createGroupLookup(props.columnGroupingModel ?? []);
+ return {
+ ...state,
+ columnGrouping: { lookup: groupLookup, groupCollapsedModel: {} },
+ };
+};
+
+/**
+ * @requires useGridColumns (method, event)
+ * @requires useGridParamsApi (method)
+ */
+export const useGridColumnGrouping = (
+ apiRef: React.MutableRefObject,
+ props: Pick,
+) => {
+ /**
+ * API METHODS
+ */
+ const getColumnGroupPath = React.useCallback<
+ GridColumnGroupingApi['unstable_getColumnGroupPath']
+ >(
+ (field) => {
+ const columnLookup = gridColumnLookupSelector(apiRef);
+
+ return columnLookup[field]?.groupPath ?? [];
+ },
+ [apiRef],
+ );
+
+ const getAllGroupDetails = React.useCallback<
+ GridColumnGroupingApi['unstable_getAllGroupDetails']
+ >(() => {
+ const columnGroupLookup = gridColumnGroupsLookupSelector(apiRef);
+ return columnGroupLookup;
+ }, [apiRef]);
+
+ const columnGroupingApi: GridColumnGroupingApi = {
+ unstable_getColumnGroupPath: getColumnGroupPath,
+ unstable_getAllGroupDetails: getAllGroupDetails,
+ };
+
+ useGridApiMethod(apiRef, columnGroupingApi, 'GridColumnGroupingApi');
+
+ /**
+ * EFFECTS
+ */
+ // The effect does not track any value defined synchronously during the 1st render by hooks called after `useGridColumns`
+ // As a consequence, the state generated by the 1st run of this useEffect will always be equal to the initialization one
+ const isFirstRender = React.useRef(true);
+ React.useEffect(() => {
+ if (isFirstRender.current) {
+ isFirstRender.current = false;
+ return;
+ }
+ if (!props.experimentalFeatures?.columnGrouping) {
+ return;
+ }
+ const groupLookup = createGroupLookup(props.columnGroupingModel ?? []);
+ apiRef.current.setState((state) => ({
+ ...state,
+ columnGrouping: { ...state.columnGrouping, lookup: groupLookup },
+ }));
+ }, [apiRef, props.columnGroupingModel, props.experimentalFeatures?.columnGrouping]);
+};
diff --git a/packages/grid/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGroupingPreProcessors.ts b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGroupingPreProcessors.ts
new file mode 100644
index 0000000000000..efda8bc2f9ad7
--- /dev/null
+++ b/packages/grid/x-data-grid/src/hooks/features/columnGrouping/useGridColumnGroupingPreProcessors.ts
@@ -0,0 +1,38 @@
+import * as React from 'react';
+import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing';
+import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
+import { GridApiCommunity } from '../../../models/api/gridApiCommunity';
+import { isDeepEqual } from '../../../utils/utils';
+import { unwrapGroupingColumnModel, hasGroupPath } from './useGridColumnGrouping';
+
+export const useGridColumnGroupingPreProcessors = (
+ apiRef: React.MutableRefObject,
+ props: DataGridProcessedProps,
+) => {
+ const addHeaderGroups = React.useCallback>(
+ (columnsState) => {
+ if (!props.experimentalFeatures?.columnGrouping) {
+ return columnsState;
+ }
+ const unwrappedGroupingModel = unwrapGroupingColumnModel(props.columnGroupingModel);
+
+ columnsState.all.forEach((field) => {
+ const newGroupPath = unwrappedGroupingModel[field] ?? [];
+
+ const lookupElement = columnsState.lookup[field];
+ if (hasGroupPath(lookupElement) && isDeepEqual(newGroupPath, lookupElement?.groupPath)) {
+ // Avoid modifying the pointer to allow shadow comparison in https://github.com/mui/mui-x/blob/f90afbf10a1264ee8b453d7549dd7cdd6110a4ed/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts#L446:L453
+ return;
+ }
+ columnsState.lookup[field] = {
+ ...columnsState.lookup[field],
+ groupPath: unwrappedGroupingModel[field] ?? [],
+ };
+ });
+ return columnsState;
+ },
+ [props.columnGroupingModel, props.experimentalFeatures?.columnGrouping],
+ );
+
+ useGridRegisterPipeProcessor(apiRef, 'hydrateColumns', addHeaderGroups);
+};
diff --git a/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx b/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx
index a1944563c1bfc..e08ce80f4c087 100644
--- a/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx
+++ b/packages/grid/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { useForkRef } from '@mui/material/utils';
+import { styled } from '@mui/material/styles';
import { defaultMemoize } from 'reselect';
import { useGridApiContext } from '../../utils/useGridApiContext';
import { useGridSelector } from '../../utils/useGridSelector';
@@ -13,7 +14,11 @@ import {
gridTabIndexCellSelector,
gridFocusColumnHeaderSelector,
} from '../focus/gridFocusStateSelector';
-import { gridDensityHeaderHeightSelector } from '../density/densitySelector';
+import {
+ gridDensityHeaderHeightSelector,
+ gridDensityHeaderGroupingMaxDepthSelector,
+ gridDensityTotalHeaderHeightSelector,
+} from '../density/densitySelector';
import { gridFilterActiveItemsLookupSelector } from '../filter/gridFilterSelector';
import { gridSortColumnLookupSelector } from '../sorting/gridSortingSelector';
import { gridColumnMenuSelector } from '../columnMenu/columnMenuSelector';
@@ -25,12 +30,41 @@ import { GridColumnHeaderItem } from '../../../components/columnHeaders/GridColu
import { getFirstColumnIndexToRender } from '../columns/gridColumnsUtils';
import { useGridVisibleRows } from '../../utils/useGridVisibleRows';
import { getRenderableIndexes } from '../virtualization/useGridVirtualScroller';
+import { GridColumnGroupHeader } from '../../../components/columnHeaders/GridColumnGroupHeader';
+import { GridColumnGroup } from '../../../models/gridColumnGrouping';
+import { isDeepEqual } from '../../../utils/utils';
+
+// TODO: add the possibility to switch this value if needed for customization
+const MERGE_EMPTY_CELLS = true;
+
+const GridColumnHeaderRow = styled('div', {
+ name: 'MuiDataGrid',
+ slot: 'ColumnHeaderRow',
+ overridesResolver: (props, styles) => styles.columnHeaderRow,
+})(() => ({
+ display: 'flex',
+}));
+
+interface HeaderInfo {
+ groupId: GridColumnGroup['groupId'] | null;
+ groupParents: GridColumnGroup['groupId'][];
+ width: number;
+ fields: string[];
+ colIndex: number;
+ description?: string;
+}
interface UseGridColumnHeadersProps {
innerRef?: React.Ref;
minColumnIndex?: number;
}
+interface GetHeadersParams {
+ renderContext: GridRenderContext | null;
+ minFirstColumn?: number;
+ maxLastColumn?: number;
+}
+
function isUIEvent(event: any): event is React.UIEvent {
return !!event.target;
}
@@ -48,6 +82,8 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => {
const cellTabIndexState = useGridSelector(apiRef, gridTabIndexCellSelector);
const columnHeaderFocus = useGridSelector(apiRef, gridFocusColumnHeaderSelector);
const headerHeight = useGridSelector(apiRef, gridDensityHeaderHeightSelector);
+ const headerGroupingMaxDepth = useGridSelector(apiRef, gridDensityHeaderGroupingMaxDepthSelector);
+ const totalHeaderHeight = useGridSelector(apiRef, gridDensityTotalHeaderHeightSelector);
const filterColumnLookup = useGridSelector(apiRef, gridFilterActiveItemsLookupSelector);
const sortColumnLookup = useGridSelector(apiRef, gridSortColumnLookupSelector);
const columnMenuState = useGridSelector(apiRef, gridColumnMenuSelector);
@@ -187,14 +223,8 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => {
useGridApiEventHandler(apiRef, 'rowsScroll', handleScroll);
- const getColumns = (
- params?: {
- renderContext: GridRenderContext | null;
- minFirstColumn?: number;
- maxLastColumn?: number;
- },
- other = {},
- ) => {
+ // Helper for computation common between getColumnHeaders and getColumnGroupHeaders
+ const getColumnsToRender = (params?: GetHeadersParams) => {
const {
renderContext: nextRenderContext = renderContext,
minFirstColumn = minColumnIndex,
@@ -205,8 +235,6 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => {
return null;
}
- const columns: JSX.Element[] = [];
-
const [firstRowToRender, lastRowToRender] = getRenderableIndexes({
firstIndex: nextRenderContext.firstRowIndex,
lastIndex: nextRenderContext.lastRowIndex,
@@ -232,6 +260,25 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => {
const renderedColumns = visibleColumns.slice(firstColumnToRender, lastColumnToRender);
+ return {
+ renderedColumns,
+ firstColumnToRender,
+ lastColumnToRender,
+ minFirstColumn,
+ maxLastColumn,
+ };
+ };
+
+ const getColumnHeaders = (params?: GetHeadersParams, other = {}) => {
+ const columnsToRender = getColumnsToRender(params);
+
+ if (columnsToRender == null) {
+ return null;
+ }
+
+ const { renderedColumns, firstColumnToRender } = columnsToRender;
+
+ const columns: JSX.Element[] = [];
for (let i = 0; i < renderedColumns.length; i += 1) {
const column = renderedColumns[i];
@@ -268,20 +315,203 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => {
);
}
+ return (
+
+ {columns}
+
+ );
+ };
+
+ const getParents = (path: string[] = [], depth: number) => path.slice(0, depth + 1);
+
+ const getColumnGroupHeaders = (params?: GetHeadersParams) => {
+ if (headerGroupingMaxDepth === 0) {
+ return null;
+ }
+ const columnsToRender = getColumnsToRender(params);
+
+ if (columnsToRender == null) {
+ return null;
+ }
+
+ const { renderedColumns, firstColumnToRender, lastColumnToRender, maxLastColumn } =
+ columnsToRender;
+
+ const columns: JSX.Element[] = [];
+
+ const headerToRender: {
+ leftOverflow: number;
+ elements: HeaderInfo[];
+ }[] = [];
+
+ for (let depth = 0; depth < headerGroupingMaxDepth; depth += 1) {
+ // Initialize the header line with a grouping item containing all the columns on the left of the virtualization which are in the same group as the first group to render
+ const initialHeader: HeaderInfo[] = [];
+ let leftOverflow = 0;
+
+ let columnIndex = firstColumnToRender - 1;
+ const firstColumnToRenderGroup = visibleColumns[firstColumnToRender]?.groupPath?.[depth]!;
+
+ // The array of parent is used to manage empty grouping cell
+ // When two empty grouping cell are next to each other, we merge them if the belong to the same group.
+ const firstColumnToRenderGroupParents = getParents(
+ visibleColumns[firstColumnToRender]?.groupPath,
+ depth,
+ );
+ while (
+ firstColumnToRenderGroup !== null &&
+ columnIndex >= minColumnIndex &&
+ visibleColumns[columnIndex]?.groupPath &&
+ isDeepEqual(
+ getParents(visibleColumns[columnIndex]?.groupPath, depth),
+ firstColumnToRenderGroupParents,
+ )
+ ) {
+ const column = visibleColumns[columnIndex];
+
+ leftOverflow += column.computedWidth ?? 0;
+
+ if (initialHeader.length === 0) {
+ initialHeader.push({
+ width: column.computedWidth ?? 0,
+ fields: [column.field],
+ groupId: firstColumnToRenderGroup,
+ groupParents: firstColumnToRenderGroupParents,
+ colIndex: columnIndex,
+ });
+ } else {
+ initialHeader[0].width += column.computedWidth ?? 0;
+ initialHeader[0].fields.push(column.field);
+ initialHeader[0].colIndex = columnIndex;
+ }
+
+ columnIndex -= 1;
+ }
+
+ const depthInfo = renderedColumns.reduce((aggregated, column, i) => {
+ const lastItem: HeaderInfo | undefined = aggregated[aggregated.length - 1];
+
+ if (column.groupPath && column.groupPath.length > depth) {
+ if (lastItem && lastItem.groupId === column.groupPath[depth]) {
+ // Merge with the previous columns
+ return [
+ ...aggregated.slice(0, aggregated.length - 1),
+ {
+ ...lastItem,
+ width: lastItem.width + (column.computedWidth ?? 0),
+ fields: [...lastItem.fields, column.field],
+ },
+ ];
+ }
+ // Create a new grouping
+ return [
+ ...aggregated,
+ {
+ groupId: column.groupPath[depth],
+ groupParents: getParents(column.groupPath, depth),
+ width: column.computedWidth ?? 0,
+ fields: [column.field],
+ colIndex: firstColumnToRender + i,
+ },
+ ];
+ }
+
+ if (
+ MERGE_EMPTY_CELLS &&
+ lastItem &&
+ lastItem.groupId === null &&
+ isDeepEqual(getParents(column.groupPath, depth), lastItem.groupParents)
+ ) {
+ // We merge with previous column
+ return [
+ ...aggregated.slice(0, aggregated.length - 1),
+ {
+ ...lastItem,
+ width: lastItem.width + (column.computedWidth ?? 0),
+ fields: [...lastItem.fields, column.field],
+ },
+ ];
+ }
+ // We create new empty cell
+ return [
+ ...aggregated,
+ {
+ groupId: null,
+ groupParents: getParents(column.groupPath, depth),
+ width: column.computedWidth ?? 0,
+ fields: [column.field],
+ colIndex: firstColumnToRender + i,
+ },
+ ];
+ }, initialHeader);
+
+ columnIndex = lastColumnToRender;
+ const lastColumnToRenderGroup = depthInfo[depthInfo.length - 1].groupId;
+
+ while (
+ lastColumnToRenderGroup !== null &&
+ columnIndex < maxLastColumn &&
+ visibleColumns[columnIndex]?.groupPath &&
+ visibleColumns[columnIndex]?.groupPath?.[depth] === lastColumnToRenderGroup
+ ) {
+ const column = visibleColumns[columnIndex];
+
+ depthInfo[depthInfo.length - 1].width += column.computedWidth ?? 0;
+ depthInfo[depthInfo.length - 1].fields.push(column.field);
+ columnIndex += 1;
+ }
+
+ headerToRender.push({ leftOverflow, elements: [...depthInfo] });
+ }
+
+ headerToRender.forEach((depthInfo, depthIndex) => {
+ columns.push(
+
+ {depthInfo.elements.map(({ groupId, width, fields, colIndex }, groupIndex) => {
+ return (
+
+ );
+ })}
+ ,
+ );
+ });
return columns;
};
const rootStyle = {
- minHeight: headerHeight,
- maxHeight: headerHeight,
+ minHeight: totalHeaderHeight,
+ maxHeight: totalHeaderHeight,
lineHeight: `${headerHeight}px`,
};
return {
renderContext,
- getColumns,
+ getColumnHeaders,
+ getColumnGroupHeaders,
isDragging: !!dragCol,
getRootProps: (other = {}) => ({ style: rootStyle, ...other }),
- getInnerProps: () => ({ ref: handleInnerRef, 'aria-rowindex': 1, role: 'row' }),
+ getInnerProps: () => ({
+ ref: handleInnerRef,
+ role: 'rowgroup',
+ }),
};
};
diff --git a/packages/grid/x-data-grid/src/hooks/features/density/densitySelector.ts b/packages/grid/x-data-grid/src/hooks/features/density/densitySelector.ts
index 6e6cf2756939b..37e089b0aee22 100644
--- a/packages/grid/x-data-grid/src/hooks/features/density/densitySelector.ts
+++ b/packages/grid/x-data-grid/src/hooks/features/density/densitySelector.ts
@@ -18,7 +18,17 @@ export const gridDensityHeaderHeightSelector = createSelector(
(density) => density.headerHeight,
);
+export const gridDensityHeaderGroupingMaxDepthSelector = createSelector(
+ gridDensitySelector,
+ (density) => density.headerGroupingMaxDepth,
+);
+
export const gridDensityFactorSelector = createSelector(
gridDensitySelector,
(density) => density.factor,
);
+
+export const gridDensityTotalHeaderHeightSelector = createSelector(
+ gridDensitySelector,
+ (density) => density.headerHeight * (1 + density.headerGroupingMaxDepth),
+);
diff --git a/packages/grid/x-data-grid/src/hooks/features/density/densityState.ts b/packages/grid/x-data-grid/src/hooks/features/density/densityState.ts
index b924f6461377d..c79f750f471eb 100644
--- a/packages/grid/x-data-grid/src/hooks/features/density/densityState.ts
+++ b/packages/grid/x-data-grid/src/hooks/features/density/densityState.ts
@@ -4,5 +4,6 @@ export interface GridDensityState {
value: GridDensity;
rowHeight: number;
headerHeight: number;
+ headerGroupingMaxDepth: number;
factor: number;
}
diff --git a/packages/grid/x-data-grid/src/hooks/features/density/useGridDensity.tsx b/packages/grid/x-data-grid/src/hooks/features/density/useGridDensity.tsx
index 6ea806b48e106..556d524acfb6c 100644
--- a/packages/grid/x-data-grid/src/hooks/features/density/useGridDensity.tsx
+++ b/packages/grid/x-data-grid/src/hooks/features/density/useGridDensity.tsx
@@ -9,6 +9,10 @@ import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
import { gridDensitySelector } from './densitySelector';
import { isDeepEqual } from '../../../utils/utils';
import { GridStateInitializer } from '../../utils/useGridInitializeState';
+import { useGridSelector } from '../../utils/useGridSelector';
+import { gridVisibleColumnDefinitionsSelector } from '../columns';
+import { unwrapGroupingColumnModel } from '../columnGrouping/useGridColumnGrouping';
+import { GridStateCommunity } from '../../../models/gridStateCommunity';
export const COMPACT_DENSITY_FACTOR = 0.7;
export const COMFORTABLE_DENSITY_FACTOR = 1.3;
@@ -18,6 +22,7 @@ const getUpdatedDensityState = (
newDensity: GridDensity,
newHeaderHeight: number,
newRowHeight: number,
+ newMaxDepth: number,
): GridDensityState => {
switch (newDensity) {
case GridDensityTypes.Compact:
@@ -25,6 +30,7 @@ const getUpdatedDensityState = (
value: newDensity,
headerHeight: Math.floor(newHeaderHeight * COMPACT_DENSITY_FACTOR),
rowHeight: Math.floor(newRowHeight * COMPACT_DENSITY_FACTOR),
+ headerGroupingMaxDepth: newMaxDepth,
factor: COMPACT_DENSITY_FACTOR,
};
case GridDensityTypes.Comfortable:
@@ -32,6 +38,7 @@ const getUpdatedDensityState = (
value: newDensity,
headerHeight: Math.floor(newHeaderHeight * COMFORTABLE_DENSITY_FACTOR),
rowHeight: Math.floor(newRowHeight * COMFORTABLE_DENSITY_FACTOR),
+ headerGroupingMaxDepth: newMaxDepth,
factor: COMFORTABLE_DENSITY_FACTOR,
};
default:
@@ -39,30 +46,71 @@ const getUpdatedDensityState = (
value: newDensity,
headerHeight: newHeaderHeight,
rowHeight: newRowHeight,
+ headerGroupingMaxDepth: newMaxDepth,
factor: 1,
};
}
};
export const densityStateInitializer: GridStateInitializer<
- Pick
-> = (state, props) => ({
- ...state,
- density: getUpdatedDensityState(props.density, props.headerHeight, props.rowHeight),
-});
+ Pick
+> = (state, props) => {
+ // TODO: think about improving this initialization. Could it be done in the useColumn initializer?
+ // TODO: manage to remove ts-ignore
+ let maxDepth: number;
+ if (props.columnGroupingModel == null || Object.keys(props.columnGroupingModel).length === 0) {
+ maxDepth = 0;
+ } else {
+ const unwrappedGroupingColumnModel = unwrapGroupingColumnModel(props.columnGroupingModel);
+
+ const columnsState = state.columns as GridStateCommunity['columns'];
+ const visibleColumns = columnsState.all.filter(
+ (field) => columnsState.columnVisibilityModel[field] !== false,
+ );
+
+ if (visibleColumns.length === 0) {
+ maxDepth = 0;
+ } else {
+ maxDepth = Math.max(
+ ...visibleColumns.map((field) => unwrappedGroupingColumnModel[field!]?.length ?? 0),
+ );
+ }
+ }
+ return {
+ ...state,
+ density: getUpdatedDensityState(props.density, props.headerHeight, props.rowHeight, maxDepth),
+ };
+};
export const useGridDensity = (
apiRef: React.MutableRefObject,
props: Pick,
): void => {
+ const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector);
+
+ const maxDepth =
+ visibleColumns.length > 0
+ ? Math.max(...visibleColumns.map((column) => column.groupPath?.length ?? 0))
+ : 0;
+
const logger = useGridLogger(apiRef, 'useDensity');
const setDensity = React.useCallback(
- (newDensity, newHeaderHeight = props.headerHeight, newRowHeight = props.rowHeight): void => {
+ (
+ newDensity,
+ newHeaderHeight = props.headerHeight,
+ newRowHeight = props.rowHeight,
+ newMaxDepth = maxDepth,
+ ): void => {
logger.debug(`Set grid density to ${newDensity}`);
apiRef.current.setState((state) => {
const currentDensityState = gridDensitySelector(state);
- const newDensityState = getUpdatedDensityState(newDensity, newHeaderHeight, newRowHeight);
+ const newDensityState = getUpdatedDensityState(
+ newDensity,
+ newHeaderHeight,
+ newRowHeight,
+ newMaxDepth,
+ );
if (isDeepEqual(currentDensityState, newDensityState)) {
return state;
@@ -75,12 +123,12 @@ export const useGridDensity = (
});
apiRef.current.forceUpdate();
},
- [logger, apiRef, props.headerHeight, props.rowHeight],
+ [logger, apiRef, props.headerHeight, props.rowHeight, maxDepth],
);
React.useEffect(() => {
- apiRef.current.setDensity(props.density, props.headerHeight, props.rowHeight);
- }, [apiRef, props.density, props.rowHeight, props.headerHeight]);
+ apiRef.current.setDensity(props.density, props.headerHeight, props.rowHeight, maxDepth);
+ }, [apiRef, props.density, props.rowHeight, props.headerHeight, maxDepth]);
const densityApi: GridDensityApi = {
setDensity,
diff --git a/packages/grid/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts b/packages/grid/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts
index 173b50c6c9a3b..0b2f13ae50488 100644
--- a/packages/grid/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts
+++ b/packages/grid/x-data-grid/src/hooks/features/dimensions/useGridDimensions.ts
@@ -16,7 +16,7 @@ import { useGridLogger } from '../../utils/useGridLogger';
import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
import { GridDimensions, GridDimensionsApi } from './gridDimensionsApi';
import { gridColumnsTotalWidthSelector } from '../columns';
-import { gridDensityHeaderHeightSelector, gridDensityRowHeightSelector } from '../density';
+import { gridDensityTotalHeaderHeightSelector, gridDensityRowHeightSelector } from '../density';
import { useGridSelector } from '../../utils';
import { getVisibleRows } from '../../utils/useGridVisibleRows';
import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector';
@@ -63,7 +63,7 @@ export function useGridDimensions(
const rootDimensionsRef = React.useRef(null);
const fullDimensionsRef = React.useRef(null);
const rowsMeta = useGridSelector(apiRef, gridRowsMetaSelector);
- const headerHeight = useGridSelector(apiRef, gridDensityHeaderHeightSelector);
+ const totalHeaderHeight = useGridSelector(apiRef, gridDensityTotalHeaderHeightSelector);
const updateGridDimensionsRef = React.useCallback(() => {
const rootElement = apiRef.current.rootElementRef?.current;
@@ -107,7 +107,7 @@ export function useGridDimensions(
} else {
viewportOuterSize = {
width: rootDimensionsRef.current.width,
- height: rootDimensionsRef.current.height - headerHeight,
+ height: rootDimensionsRef.current.height - totalHeaderHeight,
};
const scrollInformation = hasScroll({
@@ -149,7 +149,7 @@ export function useGridDimensions(
apiRef,
props.scrollbarSize,
props.autoHeight,
- headerHeight,
+ totalHeaderHeight,
rowsMeta.currentPageTotalHeight,
]);
diff --git a/packages/grid/x-data-grid/src/hooks/features/export/useGridPrintExport.tsx b/packages/grid/x-data-grid/src/hooks/features/export/useGridPrintExport.tsx
index 32032a57ce2a3..3ba78776ab55c 100644
--- a/packages/grid/x-data-grid/src/hooks/features/export/useGridPrintExport.tsx
+++ b/packages/grid/x-data-grid/src/hooks/features/export/useGridPrintExport.tsx
@@ -11,7 +11,7 @@ import {
gridColumnDefinitionsSelector,
gridColumnVisibilityModelSelector,
} from '../columns/gridColumnsSelector';
-import { gridDensityHeaderHeightSelector } from '../density/densitySelector';
+import { gridDensityTotalHeaderHeightSelector } from '../density/densitySelector';
import { gridClasses } from '../../../constants/gridClasses';
import { useGridApiMethod } from '../../utils/useGridApiMethod';
import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector';
@@ -111,7 +111,7 @@ export const useGridPrintExport = (
return;
}
- const headerHeight = gridDensityHeaderHeightSelector(apiRef);
+ const totalHeaderHeight = gridDensityTotalHeaderHeightSelector(apiRef);
const rowsMeta = gridRowsMetaSelector(apiRef.current.state);
const gridRootElement = apiRef.current.rootElementRef!.current;
@@ -153,7 +153,7 @@ export const useGridPrintExport = (
// Expand container height to accommodate all rows
gridClone.style.height = `${
rowsMeta.currentPageTotalHeight +
- headerHeight +
+ totalHeaderHeight +
gridToolbarElementHeight +
gridFooterElementHeight
}px`;
diff --git a/packages/grid/x-data-grid/src/hooks/features/index.ts b/packages/grid/x-data-grid/src/hooks/features/index.ts
index 701c2018f4652..c98af58ad5f24 100644
--- a/packages/grid/x-data-grid/src/hooks/features/index.ts
+++ b/packages/grid/x-data-grid/src/hooks/features/index.ts
@@ -1,6 +1,7 @@
// Only export the variable and types that should be publicly exposed and re-exported from `@mui/x-data-grid-pro`
export * from './columnMenu';
export * from './columns';
+export * from './columnGrouping';
export * from './density';
export * from './editRows';
export * from './filter';
diff --git a/packages/grid/x-data-grid/src/internals/index.ts b/packages/grid/x-data-grid/src/internals/index.ts
index 7be746eb1a35e..40e0de4cf50ae 100644
--- a/packages/grid/x-data-grid/src/internals/index.ts
+++ b/packages/grid/x-data-grid/src/internals/index.ts
@@ -21,6 +21,11 @@ export {
} from '../hooks/features/columnMenu/useGridColumnMenu';
export { useGridColumns, columnsStateInitializer } from '../hooks/features/columns/useGridColumns';
export { useGridColumnSpanning } from '../hooks/features/columns/useGridColumnSpanning';
+export {
+ useGridColumnGrouping,
+ columnGroupsStateInitializer,
+} from '../hooks/features/columnGrouping/useGridColumnGrouping';
+export { useGridColumnGroupingPreProcessors } from '../hooks/features/columnGrouping/useGridColumnGroupingPreProcessors';
export type {
GridColumnRawLookup,
GridColumnsRawState,
diff --git a/packages/grid/x-data-grid/src/models/api/gridApiCommon.ts b/packages/grid/x-data-grid/src/models/api/gridApiCommon.ts
index 46d0ca6c19626..aa1cbe00fc919 100644
--- a/packages/grid/x-data-grid/src/models/api/gridApiCommon.ts
+++ b/packages/grid/x-data-grid/src/models/api/gridApiCommon.ts
@@ -26,6 +26,7 @@ import type { GridStrategyProcessingApi } from '../../hooks/core/strategyProcess
import type { GridDimensionsApi } from '../../hooks/features/dimensions';
import type { GridPaginationApi } from '../../hooks/features/pagination';
import type { GridStatePersistenceApi } from '../../hooks/features/statePersistence';
+import { GridColumnGroupingApi } from './gridColumnGroupingApi';
type GridStateApiUntyped = {
[key in keyof (GridStateApi & GridStatePersistenceApi)]: any;
@@ -58,4 +59,5 @@ export interface GridApiCommon
GridClipboardApi,
GridScrollApi,
GridColumnSpanningApi,
- GridStateApiUntyped {}
+ GridStateApiUntyped,
+ GridColumnGroupingApi {}
diff --git a/packages/grid/x-data-grid/src/models/api/gridColumnGroupingApi.ts b/packages/grid/x-data-grid/src/models/api/gridColumnGroupingApi.ts
new file mode 100644
index 0000000000000..cb66d54d7b3e7
--- /dev/null
+++ b/packages/grid/x-data-grid/src/models/api/gridColumnGroupingApi.ts
@@ -0,0 +1,20 @@
+import { GridColumnGroupLookup } from '../../hooks/features/columnGrouping/gridColumnGroupsInterfaces';
+import { GridColumnGroup } from '../gridColumnGrouping';
+
+/**
+ * The column grouping API interface that is available in the grid [[apiRef]].
+ */
+export interface GridColumnGroupingApi {
+ /**
+ * Returns the id of the groups leading to the requested column.
+ * The array is ordered by increasing depth (the last element is the direct parent of the column).
+ * @param {string} field The field of of the column requested.
+ * @returns {string[]} The id of the groups leading to the requested column.
+ */
+ unstable_getColumnGroupPath: (field: string) => GridColumnGroup['groupId'][];
+ /**
+ * Returns the column group lookup.
+ * @returns {GridColumnGroupLookup} The column group lookup.
+ */
+ unstable_getAllGroupDetails: () => GridColumnGroupLookup;
+}
diff --git a/packages/grid/x-data-grid/src/models/api/gridDensityApi.ts b/packages/grid/x-data-grid/src/models/api/gridDensityApi.ts
index a405c913b0837..2a02f78e8b51d 100644
--- a/packages/grid/x-data-grid/src/models/api/gridDensityApi.ts
+++ b/packages/grid/x-data-grid/src/models/api/gridDensityApi.ts
@@ -7,6 +7,7 @@ export interface GridDensityOption {
value: GridDensityTypes;
}
+// TODO v6: turns `setDensity` parameters in an object
/**
* The density API interface that is available in the grid `apiRef`.
*/
@@ -16,6 +17,12 @@ export interface GridDensityApi {
* @param {string} density Can be: `"compact"`, `"standard"`, `"comfortable"`.
* @param {number} headerHeight The new header height.
* @param {number} rowHeight The new row height.
+ * @param {number} maxDepth The depth of maximal depth column header grouping tree.
*/
- setDensity: (density: GridDensity, headerHeight?: number, rowHeight?: number) => void;
+ setDensity: (
+ density: GridDensity,
+ headerHeight?: number,
+ rowHeight?: number,
+ maxDepth?: number,
+ ) => void;
}
diff --git a/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts b/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts
index e1843540dab2b..d46d1f3798757 100644
--- a/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts
+++ b/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts
@@ -20,7 +20,7 @@ import { GridActionsCellItemProps } from '../../components/cell/GridActionsCellI
import { GridEditCellProps } from '../gridEditRowModel';
import type { GridValidRowModel } from '../gridRows';
import { GridApiCommunity } from '../api/gridApiCommunity';
-
+import type { GridColumnGroup } from '../gridColumnGrouping';
/**
* Alignment used in position elements in Cells.
*/
@@ -284,6 +284,13 @@ export type GridStateColDef =
* If `true`, it means that at least one of the dimension's property of this column has been modified since the last time the column prop has changed.
*/
hasBeenResized?: boolean;
+ /**
+ * The id of the groups leading to the column.
+ * The array is ordered by increasing depth (the last element is the direct parent of the column).
+ * If not defined, the column is in no group (equivalent to a path equal to `[]`).
+ * This parameter is computed from the `columnGroupingModel` prop.
+ */
+ groupPath?: GridColumnGroup['groupId'][];
};
/**
diff --git a/packages/grid/x-data-grid/src/models/gridColumnGrouping.ts b/packages/grid/x-data-grid/src/models/gridColumnGrouping.ts
new file mode 100644
index 0000000000000..e0add69ee507e
--- /dev/null
+++ b/packages/grid/x-data-grid/src/models/gridColumnGrouping.ts
@@ -0,0 +1,78 @@
+import { GridColDef } from './colDef';
+
+export interface GridLeafColumn {
+ field: GridColDef['field'];
+}
+
+export type GridColumnNode = GridColumnGroup | GridLeafColumn;
+
+export function isLeaf(node: GridColumnNode): node is GridLeafColumn {
+ return (node).field !== undefined;
+}
+
+/**
+ * A function used to process headerClassName params.
+ */
+export type GridColumnGroupHeaderClassFn = (params: GridColumnGroupHeaderParams) => string;
+
+/**
+ * The union type representing the [[GridColDef]] column header class type.
+ */
+export type GridColumnGroupHeaderClassNamePropType = string | GridColumnGroupHeaderClassFn;
+
+export interface GridColumnGroupHeaderParams
+ extends Pick {
+ /**
+ * A unique string identifying the group.
+ */
+ groupId: GridColumnGroup['groupId'] | null;
+ /**
+ * The number parent the group have.
+ */
+ depth: number;
+ /**
+ * The maximal depth among visible columns.
+ */
+ maxDepth: number;
+ /**
+ * The column fields included in the group (including nested ones).
+ */
+ fields: string[];
+ /**
+ * The column index (0 based).
+ */
+ colIndex: number;
+ /**
+ * Indicate if the group is the last one for the given depth.
+ */
+ isLastColumn: boolean;
+}
+
+export interface GridColumnGroup
+ extends Pick {
+ /**
+ * A unique string identifying the group.
+ */
+ groupId: string;
+ /**
+ * The groups and columns included in this group.
+ */
+ children: GridColumnNode[];
+ /**
+ * If `true`, allows reordering columns outside of the group.
+ * @default false
+ */
+ freeReordering?: boolean;
+ /**
+ * Allows to render a component in the column group header cell.
+ * @param {GridColumnGroupHeaderParams} params Object containing parameters for the renderer.
+ * @returns {React.ReactNode} The element to be rendered.
+ */
+ renderHeaderGroup?: (params: GridColumnGroupHeaderParams) => React.ReactNode;
+ /**
+ * Class name that will be added in the column group header cell.
+ */
+ headerClassName?: GridColumnGroupHeaderClassNamePropType;
+}
+
+export type GridColumnGroupingModel = GridColumnGroup[];
diff --git a/packages/grid/x-data-grid/src/models/gridStateCommunity.ts b/packages/grid/x-data-grid/src/models/gridStateCommunity.ts
index 5880d31a6f3ea..d4bff06c21cd1 100644
--- a/packages/grid/x-data-grid/src/models/gridStateCommunity.ts
+++ b/packages/grid/x-data-grid/src/models/gridStateCommunity.ts
@@ -2,6 +2,7 @@ import type {
GridColumnMenuState,
GridColumnsInitialState,
GridColumnsState,
+ GridColumnsGroupingState,
GridDensityState,
GridFilterInitialState,
GridFilterState,
@@ -28,6 +29,7 @@ export interface GridStateCommunity {
editRows: GridEditRowsModel;
pagination: GridPaginationState;
columns: GridColumnsState;
+ columnGrouping: GridColumnsGroupingState;
columnMenu: GridColumnMenuState;
sorting: GridSortingState;
focus: GridFocusState;
diff --git a/packages/grid/x-data-grid/src/models/index.ts b/packages/grid/x-data-grid/src/models/index.ts
index debf8deff6c64..49e72e4a84a12 100644
--- a/packages/grid/x-data-grid/src/models/index.ts
+++ b/packages/grid/x-data-grid/src/models/index.ts
@@ -22,6 +22,7 @@ export * from './logger';
export * from './muiEvent';
export * from './events';
export * from './gridSortModel';
+export * from './gridColumnGrouping';
// Do not export GridExportFormat and GridExportExtension which are override in pro package
export type {
diff --git a/packages/grid/x-data-grid/src/models/props/DataGridProps.ts b/packages/grid/x-data-grid/src/models/props/DataGridProps.ts
index 7e14aed27a076..a8549cacb212c 100644
--- a/packages/grid/x-data-grid/src/models/props/DataGridProps.ts
+++ b/packages/grid/x-data-grid/src/models/props/DataGridProps.ts
@@ -30,6 +30,7 @@ import { GridInitialStateCommunity } from '../gridStateCommunity';
import { GridSlotsComponentsProps } from '../gridSlotsComponentsProps';
import { GridColumnVisibilityModel } from '../../hooks/features/columns/gridColumnsInterfaces';
import { GridCellModesModel, GridRowModesModel } from '../api/gridEditingApi';
+import { GridColumnGroupingModel } from '../gridColumnGrouping';
export interface GridExperimentalFeatures {
/**
@@ -40,6 +41,10 @@ export interface GridExperimentalFeatures {
* Enables the new API for cell editing and row editing.
*/
newEditingApi: boolean;
+ /**
+ * Enables the column grouping.
+ */
+ columnGrouping: boolean;
/**
* Emits a warning if the cell receives focus without also syncing the focus state.
* Only works if NODE_ENV=test.
@@ -784,4 +789,5 @@ export interface DataGridPropsWithoutDefaultValue void;
+ columnGroupingModel?: GridColumnGroupingModel;
}
diff --git a/packages/grid/x-data-grid/src/tests/columnsGrouping.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/columnsGrouping.DataGrid.test.tsx
new file mode 100644
index 0000000000000..9c6e896e9fb2a
--- /dev/null
+++ b/packages/grid/x-data-grid/src/tests/columnsGrouping.DataGrid.test.tsx
@@ -0,0 +1,366 @@
+import * as React from 'react';
+import { expect } from 'chai';
+// @ts-ignore Remove once the test utils are typed
+import { createRenderer, ErrorBoundary, screen } from '@mui/monorepo/test/utils';
+import { DataGrid, DataGridProps, GridRowModel, GridColumns } from '@mui/x-data-grid';
+
+const isJSDOM = /jsdom/.test(window.navigator.userAgent);
+
+const getDefaultProps = (nbColumns: number) => {
+ const columns: GridColumns = [];
+ const row: GridRowModel = {};
+
+ for (let i = 1; i <= nbColumns; i += 1) {
+ columns.push({ field: `col${i}` });
+ row[`col${i}`] = i;
+ }
+
+ return {
+ disableVirtualization: true,
+ columns,
+ rows: [{ id: 0, ...row }],
+ autoHeight: isJSDOM,
+ experimentalFeatures: { columnGrouping: true },
+ };
+};
+
+type TestDataGridProps = Omit &
+ Partial> & { nbColumns: number };
+
+describe(' - Column grouping', () => {
+ const { render } = createRenderer();
+
+ const TestDataGrid = (props: TestDataGridProps) => {
+ const { nbColumns, ...other } = props;
+ return (
+
+
+
+ );
+ };
+
+ describe('Header grouping columns', () => {
+ it('should add one header row when columns have a group', () => {
+ render(
+ ,
+ );
+ expect(screen.queryAllByRole('row')).to.have.length(3);
+ });
+
+ it('should add header rows to match max depth oc column groups', () => {
+ render(
+ ,
+ );
+ expect(screen.queryAllByRole('row')).to.have.length(4);
+ });
+
+ it('should add correct aria-colspan, aria-colindex on headers', () => {
+ render(
+ ,
+ );
+
+ const row1Headers = document.querySelectorAll(
+ '[aria-rowindex="1"] [role="columnheader"]',
+ );
+ const row2Headers = document.querySelectorAll(
+ '[aria-rowindex="2"] [role="columnheader"]',
+ );
+
+ expect(
+ Array.from(row1Headers).map((header) => header.getAttribute('aria-colspan')),
+ ).to.deep.equal(['3']);
+ expect(
+ Array.from(row1Headers).map((header) => header.getAttribute('aria-colindex')),
+ ).to.deep.equal(['1']);
+
+ expect(
+ Array.from(row2Headers).map((header) => header.getAttribute('aria-colspan')),
+ ).to.deep.equal(['2', '1']);
+ expect(
+ Array.from(row2Headers).map((header) => header.getAttribute('aria-colindex')),
+ ).to.deep.equal(['1', '3']);
+ });
+
+ it('should support non connexe groups', () => {
+ render(
+ ,
+ );
+
+ const row1Headers = document.querySelectorAll(
+ '[aria-rowindex="1"] [role="columnheader"]',
+ );
+ const row2Headers = document.querySelectorAll(
+ '[aria-rowindex="2"] [role="columnheader"]',
+ );
+
+ expect(
+ Array.from(row1Headers).map((header) => header.getAttribute('aria-colspan')),
+ ).to.deep.equal(['1', '1', '2']);
+ expect(
+ Array.from(row1Headers).map((header) => header.getAttribute('aria-colindex')),
+ ).to.deep.equal(['1', '2', '3']);
+
+ expect(
+ Array.from(row2Headers).map((header) => header.getAttribute('aria-colspan')),
+ ).to.deep.equal(['1', '1', '1', '1']);
+ expect(
+ Array.from(row2Headers).map((header) => header.getAttribute('aria-colindex')),
+ ).to.deep.equal(['1', '2', '3', '4']);
+ });
+
+ it('should only consider visible columns non connexe groups', () => {
+ const { setProps } = render(
+ ,
+ );
+
+ // 2 header groups, 1 header, 1 row
+ expect(screen.queryAllByRole('row')).to.have.length(4);
+
+ // hide the column with 2 nested groups
+ setProps({ columnVisibilityModel: { col2: false } });
+ expect(screen.queryAllByRole('row')).to.have.length(3);
+
+ // hide the last column with a group
+ setProps({ columnVisibilityModel: { col1: false, col2: false } });
+ expect(screen.queryAllByRole('row')).to.have.length(2);
+ });
+
+ it('should update headers when `columnGroupingModel` is modified', () => {
+ const { setProps } = render(
+ ,
+ );
+
+ // 2 header groups, 1 header, 1 row
+ expect(screen.queryAllByRole('row')).to.have.length(4);
+
+ // remove the top group
+ setProps({ columnGroupingModel: [{ groupId: 'col2', children: [{ field: 'col2' }] }] });
+ expect(screen.queryAllByRole('row')).to.have.length(3);
+ });
+
+ it('should split empty group cell if they are children of different group', () => {
+ render(
+ ,
+ );
+
+ const row2Headers = document.querySelectorAll(
+ '[aria-rowindex="2"] [role="columnheader"]',
+ );
+
+ expect(
+ Array.from(row2Headers).map((header) => header.getAttribute('aria-colspan')),
+ ).to.deep.equal(['1', '1', '1']);
+ expect(
+ Array.from(row2Headers).map((header) => header.getAttribute('aria-colindex')),
+ ).to.deep.equal(['1', '2', '3']);
+ });
+
+ it('should merge empty group cell if they are children of the group', () => {
+ render(
+ ,
+ );
+
+ const row2Headers = document.querySelectorAll(
+ '[aria-rowindex="2"] [role="columnheader"]',
+ );
+
+ expect(
+ Array.from(row2Headers).map((header) => header.getAttribute('aria-colspan')),
+ ).to.deep.equal(['2', '1']);
+ expect(
+ Array.from(row2Headers).map((header) => header.getAttribute('aria-colindex')),
+ ).to.deep.equal(['1', '3']);
+ });
+ });
+
+ // TODO: remove the skip. I failed to test if an error is thrown
+ // eslint-disable-next-line mocha/no-skipped-tests
+ describe.skip('error messages', () => {
+ const TestWithError = (props: TestDataGridProps) => (
+
+
+
+ );
+
+ it('should log an error if two groups have the same id', () => {
+ expect(() => {
+ render(
+ ,
+ );
+ }).toErrorDev();
+ });
+
+ it('should log an error if a columns is referenced in two groups', () => {
+ expect(() => {
+ render(
+ ,
+ );
+ }).toErrorDev();
+ });
+
+ it('should log an error if a group have no id', () => {
+ expect(() => {
+ try {
+ render(
+ ,
+ );
+ } catch (error) {
+ console.error(error);
+ }
+ }).toErrorDev(
+ 'MUI-DataGrid: an element of the columnGroupingModel does not have either `field` or `groupId`',
+ );
+ });
+
+ it('should log a warning if a group has no children', () => {
+ expect(() => {
+ render(
+ ,
+ );
+ }).toWarnDev('MUI-DataGrid: group groupId=col12 has no children.');
+ expect(() => {
+ render(
+ ,
+ );
+ }).toWarnDev('MUI-DataGrid: group groupId=col12 has no children.');
+ });
+ });
+});
diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json
index 5b1f0589fd148..e800c6440221a 100644
--- a/scripts/x-data-grid-premium.exports.json
+++ b/scripts/x-data-grid-premium.exports.json
@@ -126,6 +126,13 @@
{ "name": "GridColumnApi", "kind": "Interface" },
{ "name": "gridColumnDefinitionsSelector", "kind": "Variable" },
{ "name": "gridColumnFieldsSelector", "kind": "Variable" },
+ { "name": "GridColumnGroup", "kind": "Interface" },
+ { "name": "GridColumnGroupHeaderClassFn", "kind": "TypeAlias" },
+ { "name": "GridColumnGroupHeaderClassNamePropType", "kind": "TypeAlias" },
+ { "name": "GridColumnGroupHeaderParams", "kind": "Interface" },
+ { "name": "GridColumnGroupingModel", "kind": "TypeAlias" },
+ { "name": "gridColumnGroupingSelector", "kind": "Variable" },
+ { "name": "gridColumnGroupsLookupSelector", "kind": "Variable" },
{ "name": "GridColumnHeaderClassFn", "kind": "TypeAlias" },
{ "name": "GridColumnHeaderClassNamePropType", "kind": "TypeAlias" },
{ "name": "GridColumnHeaderEventLookup", "kind": "Interface" },
@@ -152,6 +159,7 @@
{ "name": "GridColumnMenuProps", "kind": "Interface" },
{ "name": "gridColumnMenuSelector", "kind": "Variable" },
{ "name": "GridColumnMenuState", "kind": "Interface" },
+ { "name": "GridColumnNode", "kind": "TypeAlias" },
{ "name": "GridColumnOrderChangeParams", "kind": "Interface" },
{ "name": "GridColumnPinningApi", "kind": "Interface" },
{ "name": "GridColumnPinningInternalCache", "kind": "Interface" },
@@ -165,6 +173,7 @@
{ "name": "gridColumnResizeSelector", "kind": "Variable" },
{ "name": "GridColumnResizeState", "kind": "Interface" },
{ "name": "GridColumns", "kind": "TypeAlias" },
+ { "name": "GridColumnsGroupingState", "kind": "Interface" },
{ "name": "GridColumnsInitialState", "kind": "Interface" },
{ "name": "GridColumnsMenuItem", "kind": "Variable" },
{ "name": "GridColumnsMeta", "kind": "Interface" },
@@ -195,11 +204,13 @@
{ "name": "GridDensity", "kind": "TypeAlias" },
{ "name": "GridDensityApi", "kind": "Interface" },
{ "name": "gridDensityFactorSelector", "kind": "Variable" },
+ { "name": "gridDensityHeaderGroupingMaxDepthSelector", "kind": "Variable" },
{ "name": "gridDensityHeaderHeightSelector", "kind": "Variable" },
{ "name": "GridDensityOption", "kind": "Interface" },
{ "name": "gridDensityRowHeightSelector", "kind": "Variable" },
{ "name": "gridDensitySelector", "kind": "Variable" },
{ "name": "GridDensityState", "kind": "Interface" },
+ { "name": "gridDensityTotalHeaderHeightSelector", "kind": "Variable" },
{ "name": "GridDensityTypes", "kind": "Enum" },
{ "name": "gridDensityValueSelector", "kind": "Variable" },
{ "name": "GridDetailPanelApi", "kind": "Interface" },
@@ -312,6 +323,7 @@
{ "name": "GridInputSelectionModel", "kind": "TypeAlias" },
{ "name": "GridKeyboardArrowRight", "kind": "Variable" },
{ "name": "GridKeyValue", "kind": "TypeAlias" },
+ { "name": "GridLeafColumn", "kind": "Interface" },
{ "name": "GridLinkOperator", "kind": "Enum" },
{ "name": "GridLoadIcon", "kind": "Variable" },
{ "name": "GridLoadingOverlay", "kind": "Variable" },
@@ -515,6 +527,7 @@
{ "name": "heIL", "kind": "Variable" },
{ "name": "HideGridColMenuItem", "kind": "Variable" },
{ "name": "huHU", "kind": "Variable" },
+ { "name": "isLeaf", "kind": "Function" },
{ "name": "itIT", "kind": "Variable" },
{ "name": "jaJP", "kind": "Variable" },
{ "name": "koKR", "kind": "Variable" },
diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json
index 87839fbdd6469..d995dfb8337f9 100644
--- a/scripts/x-data-grid-pro.exports.json
+++ b/scripts/x-data-grid-pro.exports.json
@@ -105,6 +105,13 @@
{ "name": "GridColumnApi", "kind": "Interface" },
{ "name": "gridColumnDefinitionsSelector", "kind": "Variable" },
{ "name": "gridColumnFieldsSelector", "kind": "Variable" },
+ { "name": "GridColumnGroup", "kind": "Interface" },
+ { "name": "GridColumnGroupHeaderClassFn", "kind": "TypeAlias" },
+ { "name": "GridColumnGroupHeaderClassNamePropType", "kind": "TypeAlias" },
+ { "name": "GridColumnGroupHeaderParams", "kind": "Interface" },
+ { "name": "GridColumnGroupingModel", "kind": "TypeAlias" },
+ { "name": "gridColumnGroupingSelector", "kind": "Variable" },
+ { "name": "gridColumnGroupsLookupSelector", "kind": "Variable" },
{ "name": "GridColumnHeaderClassFn", "kind": "TypeAlias" },
{ "name": "GridColumnHeaderClassNamePropType", "kind": "TypeAlias" },
{ "name": "GridColumnHeaderEventLookup", "kind": "Interface" },
@@ -131,6 +138,7 @@
{ "name": "GridColumnMenuProps", "kind": "Interface" },
{ "name": "gridColumnMenuSelector", "kind": "Variable" },
{ "name": "GridColumnMenuState", "kind": "Interface" },
+ { "name": "GridColumnNode", "kind": "TypeAlias" },
{ "name": "GridColumnOrderChangeParams", "kind": "Interface" },
{ "name": "GridColumnPinningApi", "kind": "Interface" },
{ "name": "GridColumnPinningInternalCache", "kind": "Interface" },
@@ -144,6 +152,7 @@
{ "name": "gridColumnResizeSelector", "kind": "Variable" },
{ "name": "GridColumnResizeState", "kind": "Interface" },
{ "name": "GridColumns", "kind": "TypeAlias" },
+ { "name": "GridColumnsGroupingState", "kind": "Interface" },
{ "name": "GridColumnsInitialState", "kind": "Interface" },
{ "name": "GridColumnsMenuItem", "kind": "Variable" },
{ "name": "GridColumnsMeta", "kind": "Interface" },
@@ -174,11 +183,13 @@
{ "name": "GridDensity", "kind": "TypeAlias" },
{ "name": "GridDensityApi", "kind": "Interface" },
{ "name": "gridDensityFactorSelector", "kind": "Variable" },
+ { "name": "gridDensityHeaderGroupingMaxDepthSelector", "kind": "Variable" },
{ "name": "gridDensityHeaderHeightSelector", "kind": "Variable" },
{ "name": "GridDensityOption", "kind": "Interface" },
{ "name": "gridDensityRowHeightSelector", "kind": "Variable" },
{ "name": "gridDensitySelector", "kind": "Variable" },
{ "name": "GridDensityState", "kind": "Interface" },
+ { "name": "gridDensityTotalHeaderHeightSelector", "kind": "Variable" },
{ "name": "GridDensityTypes", "kind": "Enum" },
{ "name": "gridDensityValueSelector", "kind": "Variable" },
{ "name": "GridDetailPanelApi", "kind": "Interface" },
@@ -285,6 +296,7 @@
{ "name": "GridInputSelectionModel", "kind": "TypeAlias" },
{ "name": "GridKeyboardArrowRight", "kind": "Variable" },
{ "name": "GridKeyValue", "kind": "TypeAlias" },
+ { "name": "GridLeafColumn", "kind": "Interface" },
{ "name": "GridLinkOperator", "kind": "Enum" },
{ "name": "GridLoadIcon", "kind": "Variable" },
{ "name": "GridLoadingOverlay", "kind": "Variable" },
@@ -480,6 +492,7 @@
{ "name": "heIL", "kind": "Variable" },
{ "name": "HideGridColMenuItem", "kind": "Variable" },
{ "name": "huHU", "kind": "Variable" },
+ { "name": "isLeaf", "kind": "Function" },
{ "name": "itIT", "kind": "Variable" },
{ "name": "jaJP", "kind": "Variable" },
{ "name": "koKR", "kind": "Variable" },
diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json
index 36fd3258ba005..494758be7b848 100644
--- a/scripts/x-data-grid.exports.json
+++ b/scripts/x-data-grid.exports.json
@@ -100,6 +100,13 @@
{ "name": "GridColumnApi", "kind": "Interface" },
{ "name": "gridColumnDefinitionsSelector", "kind": "Variable" },
{ "name": "gridColumnFieldsSelector", "kind": "Variable" },
+ { "name": "GridColumnGroup", "kind": "Interface" },
+ { "name": "GridColumnGroupHeaderClassFn", "kind": "TypeAlias" },
+ { "name": "GridColumnGroupHeaderClassNamePropType", "kind": "TypeAlias" },
+ { "name": "GridColumnGroupHeaderParams", "kind": "Interface" },
+ { "name": "GridColumnGroupingModel", "kind": "TypeAlias" },
+ { "name": "gridColumnGroupingSelector", "kind": "Variable" },
+ { "name": "gridColumnGroupsLookupSelector", "kind": "Variable" },
{ "name": "GridColumnHeaderClassFn", "kind": "TypeAlias" },
{ "name": "GridColumnHeaderClassNamePropType", "kind": "TypeAlias" },
{ "name": "GridColumnHeaderEventLookup", "kind": "Interface" },
@@ -126,10 +133,12 @@
{ "name": "GridColumnMenuProps", "kind": "Interface" },
{ "name": "gridColumnMenuSelector", "kind": "Variable" },
{ "name": "GridColumnMenuState", "kind": "Interface" },
+ { "name": "GridColumnNode", "kind": "TypeAlias" },
{ "name": "GridColumnOrderChangeParams", "kind": "Interface" },
{ "name": "gridColumnPositionsSelector", "kind": "Variable" },
{ "name": "GridColumnResizeParams", "kind": "Interface" },
{ "name": "GridColumns", "kind": "TypeAlias" },
+ { "name": "GridColumnsGroupingState", "kind": "Interface" },
{ "name": "GridColumnsInitialState", "kind": "Interface" },
{ "name": "GridColumnsMenuItem", "kind": "Variable" },
{ "name": "GridColumnsMeta", "kind": "Interface" },
@@ -160,11 +169,13 @@
{ "name": "GridDensity", "kind": "TypeAlias" },
{ "name": "GridDensityApi", "kind": "Interface" },
{ "name": "gridDensityFactorSelector", "kind": "Variable" },
+ { "name": "gridDensityHeaderGroupingMaxDepthSelector", "kind": "Variable" },
{ "name": "gridDensityHeaderHeightSelector", "kind": "Variable" },
{ "name": "GridDensityOption", "kind": "Interface" },
{ "name": "gridDensityRowHeightSelector", "kind": "Variable" },
{ "name": "gridDensitySelector", "kind": "Variable" },
{ "name": "GridDensityState", "kind": "Interface" },
+ { "name": "gridDensityTotalHeaderHeightSelector", "kind": "Variable" },
{ "name": "GridDensityTypes", "kind": "Enum" },
{ "name": "gridDensityValueSelector", "kind": "Variable" },
{ "name": "GridDimensions", "kind": "Interface" },
@@ -262,6 +273,7 @@
{ "name": "GridInputSelectionModel", "kind": "TypeAlias" },
{ "name": "GridKeyboardArrowRight", "kind": "Variable" },
{ "name": "GridKeyValue", "kind": "TypeAlias" },
+ { "name": "GridLeafColumn", "kind": "Interface" },
{ "name": "GridLinkOperator", "kind": "Enum" },
{ "name": "GridLoadIcon", "kind": "Variable" },
{ "name": "GridLoadingOverlay", "kind": "Variable" },
@@ -447,6 +459,7 @@
{ "name": "heIL", "kind": "Variable" },
{ "name": "HideGridColMenuItem", "kind": "Variable" },
{ "name": "huHU", "kind": "Variable" },
+ { "name": "isLeaf", "kind": "Function" },
{ "name": "itIT", "kind": "Variable" },
{ "name": "jaJP", "kind": "Variable" },
{ "name": "koKR", "kind": "Variable" },
diff --git a/test/utils/helperFn.ts b/test/utils/helperFn.ts
index c24e9ab00d0bd..c5b06dea84f25 100644
--- a/test/utils/helperFn.ts
+++ b/test/utils/helperFn.ts
@@ -64,9 +64,12 @@ export function getColumnValues(colIndex: number) {
);
}
-export function getColumnHeaderCell(colIndex: number): HTMLElement {
+export function getColumnHeaderCell(colIndex: number, rowIndex?: number): HTMLElement {
+ const headerRowSelector =
+ rowIndex === undefined ? '' : `[role="row"][aria-rowindex="${rowIndex + 1}"] `;
+ const headerCellSelector = `[role="columnheader"][aria-colindex="${colIndex + 1}"]`;
const columnHeader = document.querySelector(
- `[role="columnheader"][aria-colindex="${colIndex + 1}"]`,
+ `${headerRowSelector}${headerCellSelector}`,
);
if (columnHeader == null) {
throw new Error(`columnheader ${colIndex} not found`);