diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js
new file mode 100644
index 000000000000..5baa5839b6f8
--- /dev/null
+++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js
@@ -0,0 +1,72 @@
+import * as React from 'react';
+import {
+ DataGridPremium,
+ useGridApiRef,
+ useKeepGroupedColumnsHidden,
+} from '@mui/x-data-grid-premium';
+import { useMovieData } from '@mui/x-data-grid-generator';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Checkbox from '@mui/material/Checkbox';
+import Stack from '@mui/material/Stack';
+
+export default function RowGroupingPropagateSelection() {
+ const data = useMovieData();
+ const apiRef = useGridApiRef();
+ const [rowSelectionPropagation, setRowSelectionPropagation] = React.useState({
+ parents: true,
+ descendants: true,
+ });
+
+ const initialState = useKeepGroupedColumnsHidden({
+ apiRef,
+ initialState: {
+ rowGrouping: {
+ model: ['company', 'director'],
+ },
+ },
+ });
+
+ return (
+
+
+
+ setRowSelectionPropagation((prev) => ({
+ ...prev,
+ descendants: event.target.checked,
+ }))
+ }
+ />
+ }
+ label="Auto select descendants"
+ />
+
+ setRowSelectionPropagation((prev) => ({
+ ...prev,
+ parents: event.target.checked,
+ }))
+ }
+ />
+ }
+ label="Auto select parents"
+ />
+
+
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx
new file mode 100644
index 000000000000..9229e086666a
--- /dev/null
+++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import {
+ DataGridPremium,
+ useGridApiRef,
+ useKeepGroupedColumnsHidden,
+ GridRowSelectionPropagation,
+} from '@mui/x-data-grid-premium';
+import { useMovieData } from '@mui/x-data-grid-generator';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Checkbox from '@mui/material/Checkbox';
+import Stack from '@mui/material/Stack';
+
+export default function RowGroupingPropagateSelection() {
+ const data = useMovieData();
+ const apiRef = useGridApiRef();
+ const [rowSelectionPropagation, setRowSelectionPropagation] =
+ React.useState({
+ parents: true,
+ descendants: true,
+ });
+
+ const initialState = useKeepGroupedColumnsHidden({
+ apiRef,
+ initialState: {
+ rowGrouping: {
+ model: ['company', 'director'],
+ },
+ },
+ });
+
+ return (
+
+
+
+ setRowSelectionPropagation((prev) => ({
+ ...prev,
+ descendants: event.target.checked,
+ }))
+ }
+ />
+ }
+ label="Auto select descendants"
+ />
+
+ setRowSelectionPropagation((prev) => ({
+ ...prev,
+ parents: event.target.checked,
+ }))
+ }
+ />
+ }
+ label="Auto select parents"
+ />
+
+
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md
index 8b387aa8d42b..7e234f5c3632 100644
--- a/docs/data/data-grid/row-grouping/row-grouping.md
+++ b/docs/data/data-grid/row-grouping/row-grouping.md
@@ -305,6 +305,50 @@ In the example below:
If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the sorting and filtering models will not be cleaned up automatically, and the sorting/filtering will not be re-applied.
:::
+## Automatic parents and children selection
+
+By default, selecting a parent row does not select its children.
+You can override this behavior by using the `rowSelectionPropagation` prop.
+
+Here's how it's structured:
+
+```ts
+type GridRowSelectionPropagation = {
+ descendants?: boolean; // default: false
+ parents?: boolean; // default: false
+};
+```
+
+When `rowSelectionPropagation.descendants` is set to `true`.
+
+- Selecting a parent would auto-select all its filtered descendants.
+- Deselecting a parent row would auto-deselect all its filtered descendants.
+
+When `rowSelectionPropagation.parents` is set to `true`.
+
+- Selecting all the filtered descendants of a parent would auto-select the parent.
+- Deselecting a descendant of a selected parent would auto-deselect the parent.
+
+The example below demonstrates the usage of the `rowSelectionPropagation` prop.
+
+{{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}}
+
+:::info
+The row selection propagation also affects the "Select all" checkbox like any other group checkbox.
+:::
+
+:::info
+The selected rows that do not pass the filtering criteria are automatically deselected when the filter is applied. Row selection propagation is not applied to the unfiltered rows.
+:::
+
+:::warning
+If `props.disableMultipleRowSelection` is set to `true`, the row selection propagation doesn't apply.
+:::
+
+:::warning
+Row selection propagation is a client-side feature and is not supported with the [server-side data source](/x/react-data-grid/server-side-data/).
+:::
+
## Get the rows in a group
You can use the `apiRef.current.getRowGroupChildren` method to get the id of all rows contained in a group.
diff --git a/docs/data/data-grid/tree-data/tree-data.md b/docs/data/data-grid/tree-data/tree-data.md
index 3af238f688a3..b6c67e6528cb 100644
--- a/docs/data/data-grid/tree-data/tree-data.md
+++ b/docs/data/data-grid/tree-data/tree-data.md
@@ -87,6 +87,10 @@ If you want to access the grouping column field, for instance, to use it with co
Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#group-expansion).
+## Automatic parents and children selection
+
+Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#automatic-parents-and-children-selection).
+
## Gaps in the tree
If some entries are missing to build the full tree, the `DataGridPro` will automatically create rows to fill those gaps.
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 5e824d9e015d..832c3d4eec29 100644
--- a/docs/pages/x/api/data-grid/data-grid-premium.json
+++ b/docs/pages/x/api/data-grid/data-grid-premium.json
@@ -594,6 +594,10 @@
"description": "Array<number
| string>
| number
| string"
}
},
+ "rowSelectionPropagation": {
+ "type": { "name": "shape", "description": "{ descendants?: bool, parents?: bool }" },
+ "default": "{ parents: false, descendants: false }"
+ },
"rowsLoadingMode": {
"type": { "name": "enum", "description": "'client'
| 'server'" }
},
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 fa8a22c24d5e..ee04a078942e 100644
--- a/docs/pages/x/api/data-grid/data-grid-pro.json
+++ b/docs/pages/x/api/data-grid/data-grid-pro.json
@@ -529,6 +529,10 @@
"description": "Array<number
| string>
| number
| string"
}
},
+ "rowSelectionPropagation": {
+ "type": { "name": "shape", "description": "{ descendants?: bool, parents?: bool }" },
+ "default": "{ parents: false, descendants: false }"
+ },
"rowsLoadingMode": {
"type": { "name": "enum", "description": "'client'
| 'server'" }
},
diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json
index 6ad9d6baa7aa..a5f876df50d1 100644
--- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json
+++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json
@@ -187,7 +187,7 @@
"groupingColDef": { "description": "The grouping column used by the tree data." },
"headerFilterHeight": { "description": "Override the height of the header filters." },
"headerFilters": {
- "description": "If true
, enables the data grid filtering on header feature."
+ "description": "If true
, the header filters feature is enabled."
},
"hideFooter": { "description": "If true
, the footer component is hidden." },
"hideFooterPagination": {
@@ -610,6 +610,9 @@
"rows": { "description": "Set of rows of type GridRowsProp." },
"rowSelection": { "description": "If false
, the row selection mode is disabled." },
"rowSelectionModel": { "description": "Sets the row selection model of the Data Grid." },
+ "rowSelectionPropagation": {
+ "description": "When rowSelectionPropagation.descendants
is set to true
. - Selecting a parent will auto-select all its filtered descendants. - Deselecting a parent will auto-deselect all its filtered descendants.
When rowSelectionPropagation.parents=true
- Selecting all descendants of a parent would auto-select it. - Deselecting a descendant of a selected parent would deselect the parent.
Works with tree data and row grouping on the client-side only."
+ },
"rowsLoadingMode": {
"description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client""
},
diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json
index dd8849adea65..3e24702b10cd 100644
--- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json
+++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json
@@ -168,7 +168,7 @@
"groupingColDef": { "description": "The grouping column used by the tree data." },
"headerFilterHeight": { "description": "Override the height of the header filters." },
"headerFilters": {
- "description": "If true
, enables the data grid filtering on header feature."
+ "description": "If true
, the header filters feature is enabled."
},
"hideFooter": { "description": "If true
, the footer component is hidden." },
"hideFooterPagination": {
@@ -552,6 +552,9 @@
"rows": { "description": "Set of rows of type GridRowsProp." },
"rowSelection": { "description": "If false
, the row selection mode is disabled." },
"rowSelectionModel": { "description": "Sets the row selection model of the Data Grid." },
+ "rowSelectionPropagation": {
+ "description": "When rowSelectionPropagation.descendants
is set to true
. - Selecting a parent will auto-select all its filtered descendants. - Deselecting a parent will auto-deselect all its filtered descendants.
When rowSelectionPropagation.parents=true
- Selecting all descendants of a parent would auto-select it. - Deselecting a descendant of a selected parent would deselect the parent.
Works with tree data and row grouping on the client-side only."
+ },
"rowsLoadingMode": {
"description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client""
},
diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
index 46e1fcd78fbb..4df3406d73a3 100644
--- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
+++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
@@ -439,7 +439,7 @@ DataGridPremiumRaw.propTypes = {
*/
headerFilterHeight: PropTypes.number,
/**
- * If `true`, enables the data grid filtering on header feature.
+ * If `true`, the header filters feature is enabled.
* @default false
*/
headerFilters: PropTypes.bool,
@@ -985,6 +985,22 @@ DataGridPremiumRaw.propTypes = {
PropTypes.number,
PropTypes.string,
]),
+ /**
+ * When `rowSelectionPropagation.descendants` is set to `true`.
+ * - Selecting a parent will auto-select all its filtered descendants.
+ * - Deselecting a parent will auto-deselect all its filtered descendants.
+ *
+ * When `rowSelectionPropagation.parents=true`
+ * - Selecting all descendants of a parent would auto-select it.
+ * - Deselecting a descendant of a selected parent would deselect the parent.
+ *
+ * Works with tree data and row grouping on the client-side only.
+ * @default { parents: false, descendants: false }
+ */
+ rowSelectionPropagation: PropTypes.shape({
+ descendants: PropTypes.bool,
+ parents: PropTypes.bool,
+ }),
/**
* Loading rows can be processed on the server or client-side.
* Set it to 'client' if you would like enable infnite loading.
diff --git a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx
new file mode 100644
index 000000000000..d330b84ebd8c
--- /dev/null
+++ b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx
@@ -0,0 +1,164 @@
+import * as React from 'react';
+import { createRenderer, fireEvent } from '@mui/internal-test-utils';
+import { getCell } from 'test/utils/helperFn';
+import { expect } from 'chai';
+import {
+ DataGridPremium,
+ DataGridPremiumProps,
+ GridApi,
+ GridRowsProp,
+ useGridApiRef,
+} from '@mui/x-data-grid-premium';
+
+const isJSDOM = /jsdom/.test(window.navigator.userAgent);
+
+interface BaselineProps extends DataGridPremiumProps {
+ rows: GridRowsProp;
+}
+
+const rows: GridRowsProp = [
+ { id: 0, category1: 'Cat A', category2: 'Cat 1' },
+ { id: 1, category1: 'Cat A', category2: 'Cat 2' },
+ { id: 2, category1: 'Cat A', category2: 'Cat 2' },
+ { id: 3, category1: 'Cat B', category2: 'Cat 2' },
+ { id: 4, category1: 'Cat B', category2: 'Cat 1' },
+];
+
+const baselineProps: BaselineProps = {
+ autoHeight: isJSDOM,
+ disableVirtualization: true,
+ rows,
+ columns: [
+ {
+ field: 'id',
+ type: 'number',
+ },
+ {
+ field: 'category1',
+ },
+ {
+ field: 'category2',
+ },
+ ],
+};
+
+describe(' - Row selection', () => {
+ const { render } = createRenderer();
+
+ describe('props: rowSelectionPropagation = { descendants: true, parents: true }', () => {
+ let apiRef: React.MutableRefObject;
+
+ function Test(props: Partial) {
+ apiRef = useGridApiRef();
+
+ return (
+
+
+
+ );
+ }
+
+ it('should select all the children when selecting a parent', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([
+ 'auto-generated-row-category1/Cat B',
+ 3,
+ 4,
+ ]);
+ });
+
+ it('should deselect all the children when deselecting a parent', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([
+ 'auto-generated-row-category1/Cat B',
+ 3,
+ 4,
+ ]);
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows().size).to.equal(0);
+ });
+
+ it('should auto select the parent if all the children are selected', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ fireEvent.click(getCell(3, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([
+ 0,
+ 1,
+ 2,
+ 'auto-generated-row-category1/Cat A',
+ ]);
+ });
+
+ it('should deselect auto selected parent if one of the children is deselected', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ fireEvent.click(getCell(3, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([
+ 0,
+ 1,
+ 2,
+ 'auto-generated-row-category1/Cat A',
+ ]);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([0, 2]);
+ });
+
+ describe("prop: indeterminateCheckboxAction = 'select'", () => {
+ it('should select all the children when selecting an indeterminate parent', () => {
+ render(
+ ,
+ );
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ expect(getCell(0, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true');
+ fireEvent.click(getCell(0, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([
+ 0,
+ 1,
+ 2,
+ 'auto-generated-row-category1/Cat A',
+ ]);
+ });
+ });
+
+ describe("prop: indeterminateCheckboxAction = 'deselect'", () => {
+ it('should deselect all the children when selecting an indeterminate parent', () => {
+ render(
+ ,
+ );
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ expect(getCell(0, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true');
+ fireEvent.click(getCell(0, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows().size).to.equal(0);
+ });
+ });
+ });
+});
diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
index 022003ef5d6d..cc4801eda0c9 100644
--- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
+++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
@@ -394,7 +394,7 @@ DataGridProRaw.propTypes = {
*/
headerFilterHeight: PropTypes.number,
/**
- * If `true`, enables the data grid filtering on header feature.
+ * If `true`, the header filters feature is enabled.
* @default false
*/
headerFilters: PropTypes.bool,
@@ -891,6 +891,22 @@ DataGridProRaw.propTypes = {
PropTypes.number,
PropTypes.string,
]),
+ /**
+ * When `rowSelectionPropagation.descendants` is set to `true`.
+ * - Selecting a parent will auto-select all its filtered descendants.
+ * - Deselecting a parent will auto-deselect all its filtered descendants.
+ *
+ * When `rowSelectionPropagation.parents=true`
+ * - Selecting all descendants of a parent would auto-select it.
+ * - Deselecting a descendant of a selected parent would deselect the parent.
+ *
+ * Works with tree data and row grouping on the client-side only.
+ * @default { parents: false, descendants: false }
+ */
+ rowSelectionPropagation: PropTypes.shape({
+ descendants: PropTypes.bool,
+ parents: PropTypes.bool,
+ }),
/**
* Loading rows can be processed on the server or client-side.
* Set it to 'client' if you would like enable infnite loading.
diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts
index 0149f1a482f3..a54285727759 100644
--- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts
+++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts
@@ -5,7 +5,11 @@ import {
DATA_GRID_PROPS_DEFAULT_VALUES,
GridValidRowModel,
} from '@mui/x-data-grid';
-import { computeSlots, useProps } from '@mui/x-data-grid/internals';
+import {
+ computeSlots,
+ useProps,
+ ROW_SELECTION_PROPAGATION_DEFAULT,
+} from '@mui/x-data-grid/internals';
import {
DataGridProProps,
DataGridProProcessedProps,
@@ -46,6 +50,7 @@ export const DATA_GRID_PRO_PROPS_DEFAULT_VALUES: DataGridProPropsWithDefaultValu
getDetailPanelHeight: () => 500,
headerFilters: false,
keepColumnPositionIfDraggedOutside: false,
+ rowSelectionPropagation: ROW_SELECTION_PROPAGATION_DEFAULT,
rowReordering: false,
rowsLoadingMode: 'client',
scrollEndThreshold: 80,
diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx
index f878b572a3c5..23994a6684d0 100644
--- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx
+++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx
@@ -9,6 +9,8 @@ import {
DataGridPro,
DataGridProProps,
GridRowSelectionModel,
+ GridRowsProp,
+ GridColDef,
} from '@mui/x-data-grid-pro';
import { getBasicGridData } from '@mui/x-data-grid-generator';
@@ -44,6 +46,161 @@ describe(' - Row selection', () => {
);
}
+ const rows: GridRowsProp = [
+ {
+ hierarchy: ['Sarah'],
+ jobTitle: 'Head of Human Resources',
+ recruitmentDate: new Date(2020, 8, 12),
+ id: 0,
+ },
+ {
+ hierarchy: ['Thomas'],
+ jobTitle: 'Head of Sales',
+ recruitmentDate: new Date(2017, 3, 4),
+ id: 1,
+ },
+ {
+ hierarchy: ['Thomas', 'Robert'],
+ jobTitle: 'Sales Person',
+ recruitmentDate: new Date(2020, 11, 20),
+ id: 2,
+ },
+ {
+ hierarchy: ['Thomas', 'Karen'],
+ jobTitle: 'Sales Person',
+ recruitmentDate: new Date(2020, 10, 14),
+ id: 3,
+ },
+ {
+ hierarchy: ['Thomas', 'Nancy'],
+ jobTitle: 'Sales Person',
+ recruitmentDate: new Date(2017, 10, 29),
+ id: 4,
+ },
+ {
+ hierarchy: ['Thomas', 'Daniel'],
+ jobTitle: 'Sales Person',
+ recruitmentDate: new Date(2020, 7, 21),
+ id: 5,
+ },
+ {
+ hierarchy: ['Thomas', 'Christopher'],
+ jobTitle: 'Sales Person',
+ recruitmentDate: new Date(2020, 7, 20),
+ id: 6,
+ },
+ {
+ hierarchy: ['Thomas', 'Donald'],
+ jobTitle: 'Sales Person',
+ recruitmentDate: new Date(2019, 6, 28),
+ id: 7,
+ },
+ {
+ hierarchy: ['Mary'],
+ jobTitle: 'Head of Engineering',
+ recruitmentDate: new Date(2016, 3, 14),
+ id: 8,
+ },
+ {
+ hierarchy: ['Mary', 'Jennifer'],
+ jobTitle: 'Tech lead front',
+ recruitmentDate: new Date(2016, 5, 17),
+ id: 9,
+ },
+ {
+ hierarchy: ['Mary', 'Jennifer', 'Anna'],
+ jobTitle: 'Front-end developer',
+ recruitmentDate: new Date(2019, 11, 7),
+ id: 10,
+ },
+ {
+ hierarchy: ['Mary', 'Michael'],
+ jobTitle: 'Tech lead devops',
+ recruitmentDate: new Date(2021, 7, 1),
+ id: 11,
+ },
+ {
+ hierarchy: ['Mary', 'Linda'],
+ jobTitle: 'Tech lead back',
+ recruitmentDate: new Date(2017, 0, 12),
+ id: 12,
+ },
+ {
+ hierarchy: ['Mary', 'Linda', 'Elizabeth'],
+ jobTitle: 'Back-end developer',
+ recruitmentDate: new Date(2019, 2, 22),
+ id: 13,
+ },
+ {
+ hierarchy: ['Mary', 'Linda', 'William'],
+ jobTitle: 'Back-end developer',
+ recruitmentDate: new Date(2018, 4, 19),
+ id: 14,
+ },
+ ];
+
+ const columns: GridColDef[] = [
+ { field: 'jobTitle', headerName: 'Job Title', width: 200 },
+ {
+ field: 'recruitmentDate',
+ headerName: 'Recruitment Date',
+ type: 'date',
+ width: 150,
+ },
+ ];
+
+ const getTreeDataPath: DataGridProProps['getTreeDataPath'] = (row) => row.hierarchy;
+
+ function TreeDataGrid(props: Partial) {
+ apiRef = useGridApiRef();
+ return (
+
+
+
+ );
+ }
+
+ it('should keep the previously selected tree data parent selected if it becomes leaf after filtering', () => {
+ render();
+
+ fireEvent.click(
+ screen.getByRole('checkbox', {
+ name: /select all rows/i,
+ }),
+ );
+
+ expect(apiRef.current.getSelectedRows()).to.have.length(15);
+
+ act(() => {
+ apiRef.current.setFilterModel({
+ items: [
+ {
+ field: 'jobTitle',
+ value: 'Head of Sales',
+ operator: 'equals',
+ },
+ ],
+ });
+ });
+
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1]);
+ });
+
+ it('should put the parent into indeterminate if some but not all the children are selected', () => {
+ render();
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ expect(getCell(1, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true');
+ });
+
describe('prop: checkboxSelectionVisibleOnly = false', () => {
it('should select all rows of all pages if no row is selected', () => {
render(
@@ -221,16 +378,14 @@ describe(' - Row selection', () => {
const data = React.useMemo(() => getBasicGridData(rowLength, 2), [rowLength]);
- const rows = data.rows.slice(
- paginationModel.pageSize * paginationModel.page,
- paginationModel.pageSize * (paginationModel.page + 1),
- );
-
return (
- Row selection', () => {
});
});
+ describe('prop: rowSelectionPropagation = { descendants: false, parents: false }', () => {
+ function SelectionPropagationGrid(props: Partial
) {
+ return (
+
+ );
+ }
+
+ it('should select the parent only when selecting it', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1]);
+ });
+
+ it('should deselect the parent only when deselecting it', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2]);
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([2]);
+ });
+
+ it('should not auto select the parent if all the children are selected', () => {
+ render();
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ fireEvent.click(getCell(3, 0).querySelector('input')!);
+ fireEvent.click(getCell(4, 0).querySelector('input')!);
+ fireEvent.click(getCell(5, 0).querySelector('input')!);
+ fireEvent.click(getCell(6, 0).querySelector('input')!);
+ fireEvent.click(getCell(7, 0).querySelector('input')!);
+ // The parent row (Thomas, id: 1) should not be among the selected rows
+ expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7]);
+ });
+
+ it('should not deselect selected parent if one of the children is deselected', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ fireEvent.click(getCell(3, 0).querySelector('input')!);
+ fireEvent.click(getCell(4, 0).querySelector('input')!);
+ fireEvent.click(getCell(5, 0).querySelector('input')!);
+ fireEvent.click(getCell(6, 0).querySelector('input')!);
+ fireEvent.click(getCell(7, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ // The parent row (Thomas, id: 1) should still be among the selected rows
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 3, 4, 5, 6, 7]);
+ });
+
+ it('should select only the unwrapped rows when clicking "Select All" checkbox', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i }));
+ expect(apiRef.current.getSelectedRows()).to.have.keys([0, 1, 8]);
+ });
+
+ it('should deselect only the unwrapped rows when clicking "Select All" checkbox', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i }));
+ expect(apiRef.current.getSelectedRows()).to.have.keys([0, 1, 8]);
+ fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i }));
+ expect(apiRef.current.getSelectedRows().size).to.equal(0);
+ });
+ });
+
+ describe('prop: rowSelectionPropagation = { descendants: true, parents: false }', () => {
+ function SelectionPropagationGrid(props: Partial) {
+ return (
+
+ );
+ }
+
+ it('should select all the children when selecting a parent', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]);
+ });
+
+ it('should deselect all the children when deselecting a parent', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]);
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows().size).to.equal(0);
+ });
+
+ it('should not auto select the parent if all the children are selected', () => {
+ render();
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ fireEvent.click(getCell(3, 0).querySelector('input')!);
+ fireEvent.click(getCell(4, 0).querySelector('input')!);
+ fireEvent.click(getCell(5, 0).querySelector('input')!);
+ fireEvent.click(getCell(6, 0).querySelector('input')!);
+ fireEvent.click(getCell(7, 0).querySelector('input')!);
+ // The parent row (Thomas, id: 1) should not be among the selected rows
+ expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7]);
+ });
+
+ it('should not deselect selected parent if one of the children is deselected', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ // The parent row (Thomas, id: 1) should still be among the selected rows
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 3, 4, 5, 6, 7]);
+ });
+
+ it('should select all the nested rows when clicking "Select All" checkbox', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i }));
+ expect(apiRef.current.getSelectedRows().size).to.equal(15);
+ });
+
+ it('should deselect all the nested rows when clicking "Select All" checkbox', () => {
+ render();
+
+ fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i }));
+ expect(apiRef.current.getSelectedRows().size).to.equal(15);
+ fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i }));
+ expect(apiRef.current.getSelectedRows().size).to.equal(0);
+ });
+
+ describe('prop: isRowSelectable', () => {
+ it("should not select a parent or it's descendants if not allowed", () => {
+ render(
+ params.id !== 1}
+ />,
+ );
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows().size).to.equal(0);
+ });
+
+ it('should not auto-select a descendant if not allowed', () => {
+ render(
+ params.id !== 2}
+ />,
+ );
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 3, 4, 5, 6, 7]);
+ });
+ });
+ });
+
+ describe('prop: rowSelectionPropagation = { descendants: false, parents: true }', () => {
+ function SelectionPropagationGrid(props: Partial) {
+ return (
+
+ );
+ }
+
+ it('should select the parent only when selecting it', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1]);
+ });
+
+ it('should deselect the parent only when deselecting it', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2]);
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([2]);
+ });
+
+ it('should auto select the parent if all the children are selected', () => {
+ render();
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ fireEvent.click(getCell(3, 0).querySelector('input')!);
+ fireEvent.click(getCell(4, 0).querySelector('input')!);
+ fireEvent.click(getCell(5, 0).querySelector('input')!);
+ fireEvent.click(getCell(6, 0).querySelector('input')!);
+ fireEvent.click(getCell(7, 0).querySelector('input')!);
+ // The parent row (Thomas, id: 1) should be among the selected rows
+ expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7, 1]);
+ });
+
+ it('should deselect selected parent if one of the children is deselected', () => {
+ render();
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ fireEvent.click(getCell(3, 0).querySelector('input')!);
+ fireEvent.click(getCell(4, 0).querySelector('input')!);
+ fireEvent.click(getCell(5, 0).querySelector('input')!);
+ fireEvent.click(getCell(6, 0).querySelector('input')!);
+ fireEvent.click(getCell(7, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7, 1]);
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ // The parent row (Thomas, id: 1) should not be among the selected rows
+ expect(apiRef.current.getSelectedRows()).to.have.keys([3, 4, 5, 6, 7]);
+ });
+
+ describe('prop: isRowSelectable', () => {
+ it('should not auto select a parent if not allowed', () => {
+ render(
+ params.id !== 1}
+ />,
+ );
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ fireEvent.click(getCell(3, 0).querySelector('input')!);
+ fireEvent.click(getCell(4, 0).querySelector('input')!);
+ fireEvent.click(getCell(5, 0).querySelector('input')!);
+ fireEvent.click(getCell(6, 0).querySelector('input')!);
+ fireEvent.click(getCell(7, 0).querySelector('input')!);
+ // The parent row (Thomas, id: 1) should still not be among the selected rows
+ expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7]);
+ });
+ });
+ });
+
+ describe('prop: rowSelectionPropagation = { descendants: true, parents: true }', () => {
+ function SelectionPropagationGrid(props: Partial) {
+ return (
+
+ );
+ }
+
+ it('should select all the children when selecting a parent', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]);
+ });
+
+ it('should deselect all the children when deselecting a parent', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]);
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows().size).to.equal(0);
+ });
+
+ it('should auto select the parent if all the children are selected', () => {
+ render();
+
+ fireEvent.click(getCell(9, 0).querySelector('input')!);
+ fireEvent.click(getCell(11, 0).querySelector('input')!);
+ fireEvent.click(getCell(12, 0).querySelector('input')!);
+
+ // The parent row (Mary, id: 8) should be among the selected rows
+ expect(apiRef.current.getSelectedRows()).to.have.keys([9, 10, 11, 12, 8, 13, 14]);
+ });
+
+ it('should deselect auto selected parent if one of the children is deselected', () => {
+ render();
+
+ fireEvent.click(getCell(9, 0).querySelector('input')!);
+ fireEvent.click(getCell(11, 0).querySelector('input')!);
+ fireEvent.click(getCell(12, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([9, 10, 11, 12, 8, 13, 14]);
+ fireEvent.click(getCell(9, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([11, 12, 13, 14]);
+ });
+
+ describe("prop: indeterminateCheckboxAction = 'select'", () => {
+ it('should select all the children when selecting an indeterminate parent', () => {
+ render(
+ ,
+ );
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ expect(getCell(1, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true');
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]);
+ });
+ });
+
+ describe("prop: indeterminateCheckboxAction = 'deselect'", () => {
+ it('should deselect all the children when selecting an indeterminate parent', () => {
+ render(
+ ,
+ );
+
+ fireEvent.click(getCell(2, 0).querySelector('input')!);
+ expect(getCell(1, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true');
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows().size).to.equal(0);
+ });
+ });
+
+ describe('prop: keepNonExistentRowsSelected = true', () => {
+ it('should keep non-existent rows selected on filtering', () => {
+ render();
+
+ fireEvent.click(getCell(1, 0).querySelector('input')!);
+ expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]);
+
+ act(() => {
+ apiRef.current.setFilterModel({
+ items: [
+ {
+ field: 'jobTitle',
+ value: 'Head of Human Resources',
+ operator: 'equals',
+ },
+ ],
+ });
+ });
+
+ fireEvent.click(getCell(0, 0).querySelector('input')!);
+
+ expect(apiRef.current.getSelectedRows()).to.have.keys([0, 1, 2, 3, 4, 5, 6, 7]);
+ });
+ });
+ });
+
describe('apiRef: getSelectedRows', () => {
it('should handle the event internally before triggering onRowSelectionModelChange', () => {
render(
diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx
index 377713f3bb3b..5bfadd9b96f7 100644
--- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx
+++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx
@@ -4,12 +4,14 @@ import {
unstable_composeClasses as composeClasses,
unstable_useForkRef as useForkRef,
} from '@mui/utils';
-import type { GridRenderCellParams } from '../../models/params/gridCellParams';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { getDataGridUtilityClass } from '../../constants/gridClasses';
+import { useGridSelector } from '../../hooks/utils/useGridSelector';
+import { getCheckboxPropsSelector } from '../../hooks/features/rowSelection/utils';
import type { DataGridProcessedProps } from '../../models/props/DataGridProps';
import type { GridRowSelectionCheckboxParams } from '../../models/params/gridRowSelectionCheckboxParams';
+import type { GridRenderCellParams } from '../../models/params/gridCellParams';
type OwnerState = { classes: DataGridProcessedProps['classes'] };
@@ -32,7 +34,6 @@ const GridCellCheckboxForwardRef = React.forwardRef,
): void => {
const logger = useGridLogger(apiRef, 'useGridSelection');
- const runIfRowSelectionIsEnabled =
+ const runIfRowSelectionIsEnabled = React.useCallback(
(callback: (...args: Args) => void) =>
- (...args: Args) => {
- if (props.rowSelection) {
- callback(...args);
- }
- };
+ (...args: Args) => {
+ if (props.rowSelection) {
+ callback(...args);
+ }
+ },
+ [props.rowSelection],
+ );
+
+ const applyAutoSelection =
+ props.signature !== GridSignature.DataGrid &&
+ (props.rowSelectionPropagation?.parents || props.rowSelectionPropagation?.descendants);
const propRowSelectionModel = React.useMemo(() => {
return getSelectionModelPropValue(
@@ -120,6 +129,8 @@ export const useGridRowSelection = (
const canHaveMultipleSelection = isMultipleRowSelectionEnabled(props);
const visibleRows = useGridVisibleRows(apiRef, props);
+ const tree = useGridSelector(apiRef, gridRowTreeSelector);
+ const isNestedData = useGridSelector(apiRef, gridRowMaximumTreeDepthSelector) > 1;
const expandMouseRowRangeSelection = React.useCallback(
(id: GridRowId) => {
@@ -147,7 +158,7 @@ export const useGridRowSelection = (
[apiRef],
);
- /**
+ /*
* API METHODS
*/
const setRowSelectionModel = React.useCallback(
@@ -219,24 +230,77 @@ export const useGridRowSelection = (
if (resetSelection) {
logger.debug(`Setting selection for row ${id}`);
- apiRef.current.setRowSelectionModel(isSelected ? [id] : []);
+ const newSelection: GridRowId[] = [];
+ const addRow = (rowId: GridRowId) => {
+ newSelection.push(rowId);
+ };
+ if (isSelected) {
+ addRow(id);
+ if (applyAutoSelection) {
+ findRowsToSelect(
+ apiRef,
+ tree,
+ id,
+ props.rowSelectionPropagation?.descendants ?? false,
+ props.rowSelectionPropagation?.parents ?? false,
+ addRow,
+ );
+ }
+ }
+
+ apiRef.current.setRowSelectionModel(newSelection);
} else {
logger.debug(`Toggling selection for row ${id}`);
const selection = gridRowSelectionStateSelector(apiRef.current.state);
- const newSelection: GridRowId[] = selection.filter((el) => el !== id);
+ const newSelection: Set = new Set(selection);
+ newSelection.delete(id);
+
+ const addRow = (rowId: GridRowId) => {
+ newSelection.add(rowId);
+ };
+ const removeRow = (rowId: GridRowId) => {
+ newSelection.delete(rowId);
+ };
if (isSelected) {
- newSelection.push(id);
+ addRow(id);
+ if (applyAutoSelection) {
+ findRowsToSelect(
+ apiRef,
+ tree,
+ id,
+ props.rowSelectionPropagation?.descendants ?? false,
+ props.rowSelectionPropagation?.parents ?? false,
+ addRow,
+ );
+ }
+ } else if (applyAutoSelection) {
+ findRowsToDeselect(
+ apiRef,
+ tree,
+ id,
+ props.rowSelectionPropagation?.descendants ?? false,
+ props.rowSelectionPropagation?.parents ?? false,
+ removeRow,
+ );
}
- const isSelectionValid = newSelection.length < 2 || canHaveMultipleSelection;
+ const isSelectionValid = newSelection.size < 2 || canHaveMultipleSelection;
if (isSelectionValid) {
- apiRef.current.setRowSelectionModel(newSelection);
+ apiRef.current.setRowSelectionModel(Array.from(newSelection));
}
}
},
- [apiRef, logger, canHaveMultipleSelection],
+ [
+ apiRef,
+ logger,
+ applyAutoSelection,
+ tree,
+ props.rowSelectionPropagation?.descendants,
+ props.rowSelectionPropagation?.parents,
+ canHaveMultipleSelection,
+ ],
);
const selectRows = React.useCallback(
@@ -247,18 +311,63 @@ export const useGridRowSelection = (
let newSelection: GridRowId[];
if (resetSelection) {
- newSelection = isSelected ? selectableIds : [];
+ if (isSelected) {
+ newSelection = selectableIds;
+ if (applyAutoSelection) {
+ const addRow = (rowId: GridRowId) => {
+ newSelection.push(rowId);
+ };
+ selectableIds.forEach((id) => {
+ findRowsToSelect(
+ apiRef,
+ tree,
+ id,
+ props.rowSelectionPropagation?.descendants ?? false,
+ props.rowSelectionPropagation?.parents ?? false,
+ addRow,
+ );
+ });
+ }
+ } else {
+ newSelection = [];
+ }
} else {
// We clone the existing object to avoid mutating the same object returned by the selector to others part of the project
const selectionLookup = {
...selectedIdsLookupSelector(apiRef),
};
+ const addRow = (rowId: GridRowId) => {
+ selectionLookup[rowId] = rowId;
+ };
+ const removeRow = (rowId: GridRowId) => {
+ delete selectionLookup[rowId];
+ };
selectableIds.forEach((id) => {
if (isSelected) {
selectionLookup[id] = id;
+ if (applyAutoSelection) {
+ findRowsToSelect(
+ apiRef,
+ tree,
+ id,
+ props.rowSelectionPropagation?.descendants ?? false,
+ props.rowSelectionPropagation?.parents ?? false,
+ addRow,
+ );
+ }
} else {
- delete selectionLookup[id];
+ removeRow(id);
+ if (applyAutoSelection) {
+ findRowsToDeselect(
+ apiRef,
+ tree,
+ id,
+ props.rowSelectionPropagation?.descendants ?? false,
+ props.rowSelectionPropagation?.parents ?? false,
+ removeRow,
+ );
+ }
}
});
@@ -270,7 +379,15 @@ export const useGridRowSelection = (
apiRef.current.setRowSelectionModel(newSelection);
}
},
- [apiRef, logger, canHaveMultipleSelection],
+ [
+ logger,
+ applyAutoSelection,
+ canHaveMultipleSelection,
+ apiRef,
+ tree,
+ props.rowSelectionPropagation?.descendants,
+ props.rowSelectionPropagation?.parents,
+ ],
);
const selectRowRange = React.useCallback(
@@ -322,31 +439,63 @@ export const useGridRowSelection = (
props.signature === GridSignature.DataGrid ? 'private' : 'public',
);
- /**
+ /*
* EVENTS
*/
- const removeOutdatedSelection = React.useCallback(() => {
- if (props.keepNonExistentRowsSelected) {
- return;
- }
- const currentSelection = gridRowSelectionStateSelector(apiRef.current.state);
- const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef);
-
- // We clone the existing object to avoid mutating the same object returned by the selector to others part of the project
- const selectionLookup = { ...selectedIdsLookupSelector(apiRef) };
+ const removeOutdatedSelection = React.useCallback(
+ (sortModelUpdated = false) => {
+ const currentSelection = gridRowSelectionStateSelector(apiRef.current.state);
+ const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef);
+
+ // We clone the existing object to avoid mutating the same object returned by the selector to others part of the project
+ const selectionLookup = { ...selectedIdsLookupSelector(apiRef) };
+
+ let hasChanged = false;
+ currentSelection.forEach((id: GridRowId) => {
+ if (filteredRowsLookup[id] === false) {
+ if (props.keepNonExistentRowsSelected) {
+ return;
+ }
+ delete selectionLookup[id];
+ hasChanged = true;
+ return;
+ }
+ if (!props.rowSelectionPropagation?.parents) {
+ return;
+ }
+ const node = tree[id];
+ if (node.type === 'group') {
+ const isAutoGenerated = (node as GridGroupNode).isAutoGenerated;
+ if (isAutoGenerated) {
+ delete selectionLookup[id];
+ hasChanged = true;
+ return;
+ }
+ // Keep previously selected tree data parents selected if all their children are filtered out
+ if (!node.children.every((childId) => filteredRowsLookup[childId] === false)) {
+ delete selectionLookup[id];
+ hasChanged = true;
+ }
+ }
+ });
- let hasChanged = false;
- currentSelection.forEach((id: GridRowId) => {
- if (!filteredRowsLookup[id]) {
- delete selectionLookup[id];
- hasChanged = true;
+ if (hasChanged || (isNestedData && !sortModelUpdated)) {
+ const newSelection = Object.values(selectionLookup);
+ if (isNestedData) {
+ apiRef.current.selectRows(newSelection, true, true);
+ } else {
+ apiRef.current.setRowSelectionModel(newSelection);
+ }
}
- });
-
- if (hasChanged) {
- apiRef.current.setRowSelectionModel(Object.values(selectionLookup));
- }
- }, [apiRef, props.keepNonExistentRowsSelected]);
+ },
+ [
+ apiRef,
+ isNestedData,
+ props.rowSelectionPropagation?.parents,
+ props.keepNonExistentRowsSelected,
+ tree,
+ ],
+ );
const handleSingleRowSelection = React.useCallback(
(id: GridRowId, event: React.MouseEvent | React.KeyboardEvent) => {
@@ -535,7 +684,7 @@ export const useGridRowSelection = (
useGridApiEventHandler(
apiRef,
'sortedRowsSet',
- runIfRowSelectionIsEnabled(removeOutdatedSelection),
+ runIfRowSelectionIsEnabled(() => removeOutdatedSelection(true)),
);
useGridApiEventHandler(
apiRef,
@@ -560,7 +709,7 @@ export const useGridRowSelection = (
);
useGridApiEventHandler(apiRef, 'cellKeyDown', runIfRowSelectionIsEnabled(handleCellKeyDown));
- /**
+ /*
* EFFECTS
*/
React.useEffect(() => {
@@ -604,4 +753,8 @@ export const useGridRowSelection = (
apiRef.current.setRowSelectionModel([]);
}
}, [apiRef, canHaveMultipleSelection, checkboxSelection, isStateControlled, props.rowSelection]);
+
+ React.useEffect(() => {
+ runIfRowSelectionIsEnabled(removeOutdatedSelection);
+ }, [removeOutdatedSelection, runIfRowSelectionIsEnabled]);
};
diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts
index 908dc4260d1e..ba1ff85f77be 100644
--- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts
+++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts
@@ -1,5 +1,102 @@
-import type { DataGridProcessedProps } from '../../../models/props/DataGridProps';
import { GridSignature } from '../../utils/useGridApiEventHandler';
+import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils';
+import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector';
+import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector';
+import { selectedIdsLookupSelector } from './gridRowSelectionSelector';
+import { gridRowTreeSelector } from '../rows/gridRowsSelector';
+import { createSelector } from '../../../utils/createSelector';
+import type { GridGroupNode, GridRowId, GridRowTreeConfig } from '../../../models/gridRows';
+import type { DataGridProcessedProps } from '../../../models/props/DataGridProps';
+import type {
+ GridPrivateApiCommunity,
+ GridApiCommunity,
+} from '../../../models/api/gridApiCommunity';
+import type { GridRowSelectionPropagation } from '../../../models/gridRowSelectionModel';
+
+export const ROW_SELECTION_PROPAGATION_DEFAULT: GridRowSelectionPropagation = {
+ parents: false,
+ descendants: false,
+};
+
+function getGridRowGroupSelectableDescendants(
+ apiRef: React.MutableRefObject,
+ groupId: GridRowId,
+) {
+ const rowTree = gridRowTreeSelector(apiRef);
+ const sortedRowIds = gridSortedRowIdsSelector(apiRef);
+ const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef);
+ const groupNode = rowTree[groupId];
+ if (!groupNode || groupNode.type !== 'group') {
+ return [];
+ }
+
+ const descendants: GridRowId[] = [];
+
+ const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1;
+ for (
+ let index = startIndex;
+ index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth;
+ index += 1
+ ) {
+ const id = sortedRowIds[index];
+ if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) {
+ descendants.push(id);
+ }
+ }
+ return descendants;
+}
+
+// TODO v8: Use `createSelectorV8`
+export function getCheckboxPropsSelector(groupId: GridRowId, autoSelectParents: boolean) {
+ return createSelector(
+ gridRowTreeSelector,
+ gridSortedRowIdsSelector,
+ gridFilteredRowsLookupSelector,
+ selectedIdsLookupSelector,
+ (rowTree, sortedRowIds, filteredRowsLookup, rowSelectionLookup) => {
+ const groupNode = rowTree[groupId];
+ if (!groupNode || groupNode.type !== 'group') {
+ return {
+ isIndeterminate: false,
+ isChecked: rowSelectionLookup[groupId] === groupId,
+ };
+ }
+
+ if (rowSelectionLookup[groupId] === groupId) {
+ return {
+ isIndeterminate: false,
+ isChecked: true,
+ };
+ }
+
+ let selectableDescendentsCount = 0;
+ let selectedDescendentsCount = 0;
+ const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1;
+ for (
+ let index = startIndex;
+ index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth;
+ index += 1
+ ) {
+ const id = sortedRowIds[index];
+ if (filteredRowsLookup[id] !== false) {
+ selectableDescendentsCount += 1;
+ if (rowSelectionLookup[id] !== undefined) {
+ selectedDescendentsCount += 1;
+ }
+ }
+ }
+ return {
+ isIndeterminate:
+ (selectedDescendentsCount > 0 && selectedDescendentsCount < selectableDescendentsCount) ||
+ (selectedDescendentsCount === selectableDescendentsCount &&
+ rowSelectionLookup[groupId] === undefined),
+ isChecked: autoSelectParents
+ ? selectedDescendentsCount > 0
+ : rowSelectionLookup[groupId] === groupId,
+ };
+ },
+ );
+}
export function isMultipleRowSelectionEnabled(
props: Pick<
@@ -13,3 +110,134 @@ export function isMultipleRowSelectionEnabled(
}
return !props.disableMultipleRowSelection;
}
+
+const getRowNodeParents = (tree: GridRowTreeConfig, id: GridRowId) => {
+ const parents: GridRowId[] = [];
+
+ let parent: GridRowId | null = id;
+
+ while (parent != null && parent !== GRID_ROOT_GROUP_ID) {
+ const node = tree[parent] as GridGroupNode;
+ if (!node) {
+ return parents;
+ }
+ parents.push(parent);
+ parent = node.parent;
+ }
+ return parents;
+};
+
+const getFilteredRowNodeSiblings = (
+ tree: GridRowTreeConfig,
+ filteredRows: Record,
+ id: GridRowId,
+) => {
+ const node = tree[id];
+ if (!node) {
+ return [];
+ }
+
+ const parent = node.parent;
+ if (parent == null) {
+ return [];
+ }
+
+ const parentNode = tree[parent] as GridGroupNode;
+
+ return parentNode.children.filter((childId) => childId !== id && filteredRows[childId]);
+};
+
+export const findRowsToSelect = (
+ apiRef: React.MutableRefObject,
+ tree: GridRowTreeConfig,
+ selectedRow: GridRowId,
+ autoSelectDescendants: boolean,
+ autoSelectParents: boolean,
+ addRow: (rowId: GridRowId) => void,
+) => {
+ const filteredRows = gridFilteredRowsLookupSelector(apiRef);
+ const selectedIdsLookup = selectedIdsLookupSelector(apiRef);
+ const selectedDescendants: Set = new Set([]);
+
+ if (!autoSelectDescendants && !autoSelectParents) {
+ return;
+ }
+
+ if (autoSelectDescendants) {
+ const rowNode = tree[selectedRow];
+
+ if (rowNode?.type === 'group') {
+ const descendants = getGridRowGroupSelectableDescendants(apiRef, selectedRow);
+ descendants.forEach((rowId) => {
+ addRow(rowId);
+ selectedDescendants.add(rowId);
+ });
+ }
+ }
+
+ if (autoSelectParents) {
+ const checkAllDescendantsSelected = (rowId: GridRowId): boolean => {
+ if (selectedIdsLookup[rowId] !== rowId && !selectedDescendants.has(rowId)) {
+ return false;
+ }
+ const node = tree[rowId];
+ if (node?.type !== 'group') {
+ return true;
+ }
+ return node.children.every(checkAllDescendantsSelected);
+ };
+
+ const traverseParents = (rowId: GridRowId) => {
+ const siblings: GridRowId[] = getFilteredRowNodeSiblings(tree, filteredRows, rowId);
+ if (siblings.length === 0 || siblings.every(checkAllDescendantsSelected)) {
+ const rowNode = tree[rowId] as GridGroupNode;
+ const parent = rowNode.parent;
+ if (
+ parent != null &&
+ parent !== GRID_ROOT_GROUP_ID &&
+ apiRef.current.isRowSelectable(parent)
+ ) {
+ addRow(parent);
+ selectedDescendants.add(parent);
+ traverseParents(parent);
+ }
+ }
+ };
+ traverseParents(selectedRow);
+ }
+};
+
+export const findRowsToDeselect = (
+ apiRef: React.MutableRefObject,
+ tree: GridRowTreeConfig,
+ deselectedRow: GridRowId,
+ autoSelectDescendants: boolean,
+ autoSelectParents: boolean,
+ removeRow: (rowId: GridRowId) => void,
+) => {
+ const selectedIdsLookup = selectedIdsLookupSelector(apiRef);
+
+ if (!autoSelectParents && !autoSelectDescendants) {
+ return;
+ }
+
+ if (autoSelectParents) {
+ const allParents = getRowNodeParents(tree, deselectedRow);
+ allParents.forEach((parent) => {
+ const isSelected = selectedIdsLookup[parent] === parent;
+ if (isSelected) {
+ removeRow(parent);
+ }
+ });
+ }
+
+ if (autoSelectDescendants) {
+ const rowNode = tree[deselectedRow];
+ if (rowNode?.type === 'group') {
+ const descendants = getGridRowGroupSelectableDescendants(apiRef, deselectedRow);
+ descendants.forEach((descendant) => {
+ removeRow(descendant);
+ });
+ }
+ }
+};
diff --git a/packages/x-data-grid/src/internals/index.ts b/packages/x-data-grid/src/internals/index.ts
index 6d0b8e5b91b6..7463a879c2f6 100644
--- a/packages/x-data-grid/src/internals/index.ts
+++ b/packages/x-data-grid/src/internals/index.ts
@@ -133,6 +133,7 @@ export {
useGridColumnResize,
columnResizeStateInitializer,
} from '../hooks/features/columnResize/useGridColumnResize';
+export { ROW_SELECTION_PROPAGATION_DEFAULT } from '../hooks/features/rowSelection/utils';
export { useTimeout } from '../hooks/utils/useTimeout';
export { useGridVisibleRows, getVisibleRows } from '../hooks/utils/useGridVisibleRows';
diff --git a/packages/x-data-grid/src/models/gridRowSelectionModel.ts b/packages/x-data-grid/src/models/gridRowSelectionModel.ts
index 599d21df8b8f..92b98a2b8dca 100644
--- a/packages/x-data-grid/src/models/gridRowSelectionModel.ts
+++ b/packages/x-data-grid/src/models/gridRowSelectionModel.ts
@@ -1,5 +1,10 @@
import { GridRowId } from './gridRows';
+export type GridRowSelectionPropagation = {
+ descendants?: boolean;
+ parents?: boolean;
+};
+
export type GridInputRowSelectionModel = readonly GridRowId[] | GridRowId;
export type GridRowSelectionModel = readonly GridRowId[];
diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts
index 449f56046587..8d4495333596 100644
--- a/packages/x-data-grid/src/models/props/DataGridProps.ts
+++ b/packages/x-data-grid/src/models/props/DataGridProps.ts
@@ -33,6 +33,7 @@ import { GridColumnGroupingModel } from '../gridColumnGrouping';
import { GridPaginationMeta, GridPaginationModel } from '../gridPaginationProps';
import type { GridAutosizeOptions } from '../../hooks/features/columnResize';
import type { GridDataSource } from '../gridDataSource';
+import type { GridRowSelectionPropagation } from '../gridRowSelectionModel';
export interface GridExperimentalFeatures {
/**
@@ -827,10 +828,23 @@ export interface DataGridPropsWithoutDefaultValue