Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataGrid] The header selection checkbox should work with prop.checkboxSelectionVisibleOnly #2781

Merged
merged 9 commits into from
Oct 14, 2021
Merged
4 changes: 4 additions & 0 deletions docs/src/pages/components/data-grid/events/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@
"name": "filterModelChange",
"description": "Fired when the filter model changes.\nCalled with a GridFilterModel object."
},
{
"name": "headerSelectionCheckboxChange",
"description": "Fired when the value of the selection checkbox of the header is changed\nCalled with a GridHeaderSelectionCheckboxParams object."
},
{ "name": "pageChange", "description": "Fired when the page changes." },
{ "name": "pageSizeChange", "description": "Fired when the page size changes." },
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import { GridEvents } from '../../constants/eventsConstants';
import { useGridSelector } from '../../hooks/utils/useGridSelector';
import { gridPaginatedVisibleSortedGridRowIdsSelector } from '../../hooks/features/pagination/gridPaginationSelector';
import { visibleSortedGridRowIdsSelector } from '../../hooks/features/filter/gridFilterSelector';
import { gridTabIndexColumnHeaderSelector } from '../../hooks/features/focus/gridFocusStateSelector';
import { gridRowCountSelector } from '../../hooks/features/rows/gridRowsSelector';
import { gridSelectionStateSelector } from '../../hooks/features/selection/gridSelectionSelector';
import { GridColumnHeaderParams } from '../../models/params/gridColumnHeaderParams';
import { isNavigationKey, isSpaceKey } from '../../utils/keyboardUtils';
Expand All @@ -14,6 +11,9 @@ import { getDataGridUtilityClass } from '../../gridClasses';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { composeClasses } from '../../utils/material-ui-utils';
import { GridComponentProps } from '../../GridComponentProps';
import { GridHeaderSelectionCheckboxParams } from '../../models/params/gridHeaderSelectionCheckboxParams';
import { visibleSortedGridRowIdsSelector } from '../../hooks/features/filter/gridFilterSelector';
import { gridPaginatedVisibleSortedGridRowIdsSelector } from '../../hooks/features/pagination/gridPaginationSelector';

type OwnerState = { classes: GridComponentProps['classes'] };

Expand All @@ -36,7 +36,11 @@ const GridHeaderCheckbox = React.forwardRef<HTMLInputElement, GridColumnHeaderPa
const classes = useUtilityClasses(ownerState);
const tabIndexState = useGridSelector(apiRef, gridTabIndexColumnHeaderSelector);
const selection = useGridSelector(apiRef, gridSelectionStateSelector);
const totalRows = useGridSelector(apiRef, gridRowCountSelector);
const visibleRows = useGridSelector(apiRef, visibleSortedGridRowIdsSelector);
const paginatedVisibleRows = useGridSelector(
apiRef,
gridPaginatedVisibleSortedGridRowIdsSelector,
);

const filteredSelection = React.useMemo(
() =>
Expand All @@ -46,21 +50,36 @@ const GridHeaderCheckbox = React.forwardRef<HTMLInputElement, GridColumnHeaderPa
[apiRef, rootProps.isRowSelectable, selection],
);

const totalSelectedRows = filteredSelection.length;
const isIndeterminate = totalSelectedRows > 0 && totalSelectedRows !== totalRows;
// TODO core v5 remove || isIndeterminate, no longer has any effect
const isChecked = (totalSelectedRows > 0 && totalSelectedRows === totalRows) || isIndeterminate;
// All the rows that could be selected / unselected by toggling this checkbox
const selectionCandidates = React.useMemo(() => {
if (!rootProps.pagination || !rootProps.checkboxSelectionVisibleOnly) {
return visibleRows;
}
return paginatedVisibleRows;
}, [
rootProps.pagination,
rootProps.checkboxSelectionVisibleOnly,
paginatedVisibleRows,
visibleRows,
]);

// Amount of rows selected and that could be selected / unselected by toggling this checkbox
const currentSelectionSize = React.useMemo(
() => filteredSelection.filter((id) => selectionCandidates.includes(id)).length,
[filteredSelection, selectionCandidates],
);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const checked = event.target.checked;
const isIndeterminate =
currentSelectionSize > 0 && currentSelectionSize < selectionCandidates.length;

const shouldLimitSelectionToCurrentPage =
rootProps.checkboxSelectionVisibleOnly && rootProps.pagination;
const isChecked = currentSelectionSize > 0;

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const params: GridHeaderSelectionCheckboxParams = {
value: event.target.checked,
};

const rowsToBeSelected = shouldLimitSelectionToCurrentPage
? gridPaginatedVisibleSortedGridRowIdsSelector(apiRef.current.state)
: visibleSortedGridRowIdsSelector(apiRef.current.state);
apiRef.current.selectRows(rowsToBeSelected, checked, !event.target.indeterminate);
apiRef.current.publishEvent(GridEvents.headerSelectionCheckboxChange, params);
};

const tabIndex = tabIndexState !== null && tabIndexState.field === props.field ? 0 : -1;
Expand Down
5 changes: 5 additions & 0 deletions packages/grid/_modules_/grid/constants/eventsConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ export enum GridEvents {
* Called with a [[GridSelectionModelChangeParams]] object.
*/
selectionChange = 'selectionChange',
/**
* Fired when the value of the selection checkbox of the header is changed
* Called with a [[GridHeaderSelectionCheckboxParams]] object.
*/
headerSelectionCheckboxChange = 'headerSelectionCheckboxChange',
/**
* Fired when the value of the selection checkbox of a row is changed
* Called with a [[GridRowSelectionCheckboxParams]] object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
selectedGridRowsSelector,
selectedIdsLookupSelector,
} from './gridSelectionSelector';
import { gridPaginatedVisibleSortedGridRowIdsSelector } from '../pagination';
import { visibleSortedGridRowIdsSelector } from '../filter';
import { GridHeaderSelectionCheckboxParams } from '../../../models/params/gridHeaderSelectionCheckboxParams';
import { GridCellParams } from '../../../models/params/gridCellParams';
import { GridRowSelectionCheckboxParams } from '../../../models/params/gridRowSelectionCheckboxParams';
import { useGridStateInit } from '../../utils/useGridStateInit';
Expand Down Expand Up @@ -267,12 +269,31 @@ export const useGridSelection = (
[apiRef, expandRowRangeSelection],
);

const handleHeaderSelectionCheckboxChange = React.useCallback(
(params: GridHeaderSelectionCheckboxParams) => {
const shouldLimitSelectionToCurrentPage =
props.checkboxSelectionVisibleOnly && props.pagination;

const rowsToBeSelected = shouldLimitSelectionToCurrentPage
? gridPaginatedVisibleSortedGridRowIdsSelector(apiRef.current.state)
: visibleSortedGridRowIdsSelector(apiRef.current.state);

apiRef.current.selectRows(rowsToBeSelected, params.value);
},
[apiRef, props.checkboxSelectionVisibleOnly, props.pagination],
);

useGridApiEventHandler(apiRef, GridEvents.rowClick, handleRowClick);
useGridApiEventHandler(
apiRef,
GridEvents.rowSelectionCheckboxChange,
handleRowSelectionCheckboxChange,
);
useGridApiEventHandler(
apiRef,
GridEvents.headerSelectionCheckboxChange,
handleHeaderSelectionCheckboxChange,
);
useGridApiEventHandler(apiRef, GridEvents.cellMouseDown, preventSelectionOnShift);

const selectionApi: GridSelectionApi = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface GridHeaderSelectionCheckboxParams {
value: boolean;
}
1 change: 1 addition & 0 deletions packages/grid/_modules_/grid/models/params/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './gridSortModelParams';
export * from './gridStateChangeParams';
export * from './gridViewportRowsChangeParams';
export * from './gridRowSelectionCheckboxParams';
export * from './gridHeaderSelectionCheckboxParams';
128 changes: 112 additions & 16 deletions packages/grid/x-grid/src/tests/selection.DataGridPro.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { getColumnValues, getRow, getRows } from 'test/utils/helperFn';
import { getCell, getColumnValues, getRow, getRows } from 'test/utils/helperFn';
import {
// @ts-expect-error need to migrate helpers to TypeScript
screen,
Expand Down Expand Up @@ -53,37 +53,45 @@ describe('<DataGridPro /> - Selection', () => {
);
};

describe('prop: checkboxSelectionVisibleOnly', () => {
it('should select all visible rows of all pages if checkboxSelectionVisibleOnly = false', () => {
describe('prop: checkboxSelectionVisibleOnly = false', () => {
it('should select all rows of all pages if no row is selected', () => {
render(
<TestDataGridSelection
checkboxSelection
pageSize={1}
pageSize={2}
pagination
rowsPerPageOptions={[1]}
rowsPerPageOptions={[2]}
/>,
);
const selectAllCheckbox = document.querySelector('input[type="checkbox"]');
const selectAllCheckbox = screen.getByRole('checkbox', {
name: /select all rows checkbox/i,
});
fireEvent.click(selectAllCheckbox);
expect(apiRef.current.getSelectedRows()).to.have.length(4);
expect(selectAllCheckbox.checked).to.equal(true);
});

it('should select all visible rows of the current page if checkboxSelectionVisibleOnly = true and pagination is enabled', () => {
it('should unselect all rows of all the pages if 1 row of another page is selected', () => {
render(
<TestDataGridSelection
checkboxSelection
checkboxSelectionVisibleOnly
pageSize={1}
pageSize={2}
pagination
rowsPerPageOptions={[1]}
rowsPerPageOptions={[2]}
/>,
);
const selectAllCheckbox = document.querySelector('input[type="checkbox"]');
fireEvent.click(selectAllCheckbox);
fireEvent.click(getCell(0, 0));
expect(apiRef.current.getSelectedRows()).to.have.keys([0]);
fireEvent.click(screen.getByRole('button', { name: /next page/i }));
const selectAllCheckbox = screen.getByRole('checkbox', {
name: /select all rows checkbox/i,
});
fireEvent.click(selectAllCheckbox);
expect(apiRef.current.getSelectedRows()).to.have.length(0);
expect(selectAllCheckbox.checked).to.equal(false);
});

it('should select all rows when if checkboxSelectionVisibleOnly = false and pagination is not enabled', () => {
it('should select all visible rows if pagination is not enabled', () => {
const rowLength = 10;

render(
Expand All @@ -94,14 +102,38 @@ describe('<DataGridPro /> - Selection', () => {
/>,
);

const selectAll = screen.getByRole('checkbox', {
const selectAllCheckbox = screen.getByRole('checkbox', {
name: /select all rows checkbox/i,
});
fireEvent.click(selectAll);
fireEvent.click(selectAllCheckbox);
expect(apiRef.current.getSelectedRows()).to.have.length(rowLength);
expect(selectAllCheckbox.checked).to.equal(true);
});

it('should throw a console error if checkboxSelectionVisibleOnly is used without pagination', () => {
it('should set the header checkbox in a indeterminate state when some rows of other pages are not selected', () => {
render(
<TestDataGridSelection
checkboxSelection
checkboxSelectionVisibleOnly={false}
pageSize={2}
pagination
rowsPerPageOptions={[2]}
/>,
);

const selectAllCheckbox = screen.getByRole('checkbox', {
name: /select all rows checkbox/i,
});

fireEvent.click(getCell(0, 0));
fireEvent.click(getCell(1, 0));
fireEvent.click(screen.getByRole('button', { name: /next page/i }));
expect(selectAllCheckbox).to.have.attr('data-indeterminate', 'true');
});
});

describe('prop: checkboxSelectionVisibleOnly = true', () => {
it('should throw a console error if used without pagination', () => {
expect(() => {
render(
<TestDataGridSelection checkboxSelection checkboxSelectionVisibleOnly rowLength={100} />,
Expand All @@ -112,6 +144,70 @@ describe('<DataGridPro /> - Selection', () => {
'MUI: The `checkboxSelectionVisibleOnly` prop has no effect when the pagination is not enabled.',
);
});

it('should select all the rows of the current page if no row of the current page is selected', () => {
render(
<TestDataGridSelection
checkboxSelection
checkboxSelectionVisibleOnly
pageSize={2}
pagination
rowsPerPageOptions={[2]}
/>,
);
fireEvent.click(getCell(0, 0));
expect(apiRef.current.getSelectedRows()).to.have.keys([0]);
fireEvent.click(screen.getByRole('button', { name: /next page/i }));
const selectAllCheckbox = screen.getByRole('checkbox', {
name: /select all rows checkbox/i,
});
fireEvent.click(selectAllCheckbox);
expect(apiRef.current.getSelectedRows()).to.have.keys([0, 2, 3]);
expect(selectAllCheckbox.checked).to.equal(true);
});

it('should unselect all the rows of the current page if 1 row of the current page is selected', () => {
render(
<TestDataGridSelection
checkboxSelection
checkboxSelectionVisibleOnly
pageSize={2}
pagination
rowsPerPageOptions={[2]}
/>,
);
fireEvent.click(getCell(0, 0));
expect(apiRef.current.getSelectedRows()).to.have.keys([0]);
fireEvent.click(screen.getByRole('button', { name: /next page/i }));
fireEvent.click(getCell(2, 0));
expect(apiRef.current.getSelectedRows()).to.have.keys([0, 2]);
const selectAllCheckbox = screen.getByRole('checkbox', {
name: /select all rows checkbox/i,
});
fireEvent.click(selectAllCheckbox);
expect(apiRef.current.getSelectedRows()).to.have.keys([0]);
expect(selectAllCheckbox.checked).to.equal(false);
});

it('should not set the header checkbox in a indeterminate state when some rows of other pages are not selected', () => {
render(
<TestDataGridSelection
checkboxSelection
checkboxSelectionVisibleOnly
pageSize={2}
pagination
rowsPerPageOptions={[2]}
/>,
);

fireEvent.click(getCell(0, 0));
fireEvent.click(getCell(1, 0));
fireEvent.click(screen.getByRole('button', { name: /next page/i }));
const selectAllCheckbox = screen.getByRole('checkbox', {
name: /select all rows checkbox/i,
});
expect(selectAllCheckbox).to.have.attr('data-indeterminate', 'false');
});
});

describe('apiRef: getSelectedRows', () => {
Expand Down