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

[DataGridPremium] Add support for cell selection #6567

Merged
merged 16 commits into from
Dec 14, 2022
Merged
21 changes: 21 additions & 0 deletions docs/data/data-grid/events/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@
"event": "MuiEvent<React.KeyboardEvent<HTMLElement>>",
"componentProp": "onCellKeyDown"
},
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "cellKeyUp",
"description": "Fired when a <code>keyup</code> event happens in a cell.",
"params": "GridCellParams",
"event": "MuiEvent<React.KeyboardEvent<HTMLElement>>"
},
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "cellModesModelChange",
Expand All @@ -75,13 +82,27 @@
"params": "GridCellParams",
"event": "MuiEvent<React.MouseEvent<HTMLElement>>"
},
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "cellMouseOver",
"description": "Fired when a <code>mouseover</code> event happens in a cell.",
"params": "GridCellParams",
"event": "MuiEvent<React.MouseEvent<HTMLElement>>"
},
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "cellMouseUp",
"description": "Fired when a <code>mouseup</code> event happens in a cell.",
"params": "GridCellParams",
"event": "MuiEvent<React.MouseEvent<HTMLElement>>"
},
{
"projects": ["x-data-grid-premium"],
"name": "cellSelectionChange",
"description": "Fired when the selection state of one or multiple cells change.",
"params": "GridCellSelectionModel",
"event": "MuiEvent<{}>"
},
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "columnGroupHeaderKeyDown",
Expand Down
2 changes: 1 addition & 1 deletion docs/data/data-grid/getting-started/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ The enterprise components come in two plans: Pro and Premium.
| [Single row selection](/x/react-data-grid/selection/#single-row-selection) | ✅ | ✅ | ✅ |
| [Checkbox selection](/x/react-data-grid/selection/#checkbox-selection) | ✅ | ✅ | ✅ |
| [Multiple row selection](/x/react-data-grid/selection/#multiple-row-selection) | ❌ | ✅ | ✅ |
| [Cell range selection](/x/react-data-grid/selection/#range-selection) | ❌ | ❌ | 🚧 |
| [Cell range selection](/x/react-data-grid/selection/#cell-selection) | ❌ | ❌ | |
| **Filtering** | | | |
| [Quick filter](/x/react-data-grid/filtering/#quick-filter) | ✅ | ✅ | ✅ |
| [Column filters](/x/react-data-grid/filtering/#single-and-multi-filtering) | ✅ | ✅ | ✅ |
Expand Down
1 change: 0 additions & 1 deletion docs/data/data-grid/overview/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ Please see [the Licensing page](/x/introduction/licensing/) for details.
While development of the data grid component is moving fast, there are still many additional features that we plan to implement. Some of them:

- Headless (hooks only)
- [Range selection](/x/react-data-grid/selection/#range-selection) <span class="plan-premium"></span>
- [Pivoting](/x/react-data-grid/pivoting/) <span class="plan-premium"></span>

You can find more details on, the [feature comparison](/x/react-data-grid/getting-started/#feature-comparison), our living quarterly [roadmap](https://github.com/mui/mui-x/projects/1) as well as on the open [GitHub issues](https://github.com/mui/mui-x/issues?q=is%3Aopen+label%3A%22component%3A+DataGrid%22+label%3Aenhancement).
Expand Down
87 changes: 87 additions & 0 deletions docs/data/data-grid/selection/CellSelectionFormulaField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import { DataGridPremium, useGridApiRef } from '@mui/x-data-grid-premium';
import { useDemoData } from '@mui/x-data-grid-generator';

export default function CellSelectionFormulaField() {
const apiRef = useGridApiRef();
const [value, setValue] = React.useState('');
const [cellSelectionModel, setCellSelectionModel] = React.useState({});
const [numberOfSelectedCells, setNumberOfSelectedCells] = React.useState(0);

const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 10,
maxColumns: 6,
});

const handleCellSelectionModelChange = React.useCallback((newModel) => {
setCellSelectionModel(newModel);
}, []);

const handleValueChange = React.useCallback((event) => {
setValue(event.target.value);
}, []);

const updateSelectedCells = React.useCallback(() => {
const updates = [];

Object.entries(cellSelectionModel).forEach(([id, fields]) => {
const updatedRow = { ...apiRef.current.getRow(id) };

Object.entries(fields).forEach(([field, isSelected]) => {
if (isSelected) {
updatedRow[field] = value;
}
});

updates.push(updatedRow);
});

apiRef.current.updateRows(updates);
}, [apiRef, cellSelectionModel, value]);

React.useEffect(() => {
const selectedCells = apiRef.current.unstable_getSelectedCellsAsArray();
setNumberOfSelectedCells(selectedCells.length);

if (selectedCells.length > 1) {
setValue('(multiple values)');
} else if (selectedCells.length === 1) {
setValue(
apiRef.current.getCellValue(selectedCells[0].id, selectedCells[0].field),
);
} else {
setValue('');
}
}, [apiRef, cellSelectionModel]);

return (
<div style={{ width: '100%' }}>
<Stack sx={{ mb: 1 }} direction="row" spacing={2}>
<TextField
label="Selected cell value"
disabled={numberOfSelectedCells === 0}
value={value}
onChange={handleValueChange}
fullWidth
/>
<Button disabled={numberOfSelectedCells === 0} onClick={updateSelectedCells}>
Update selected cells
</Button>
</Stack>
<div style={{ height: 400 }}>
<DataGridPremium
apiRef={apiRef}
rowSelection={false}
unstable_cellSelectionModel={cellSelectionModel}
unstable_onCellSelectionModelChange={handleCellSelectionModelChange}
unstable_cellSelection
{...data}
/>
</div>
</div>
);
}
99 changes: 99 additions & 0 deletions docs/data/data-grid/selection/CellSelectionFormulaField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import {
DataGridPremium,
GridCellSelectionModel,
GridRowModelUpdate,
useGridApiRef,
} from '@mui/x-data-grid-premium';
import { useDemoData } from '@mui/x-data-grid-generator';

export default function CellSelectionFormulaField() {
const apiRef = useGridApiRef();
const [value, setValue] = React.useState('');
const [cellSelectionModel, setCellSelectionModel] =
React.useState<GridCellSelectionModel>({});
const [numberOfSelectedCells, setNumberOfSelectedCells] = React.useState(0);

const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 10,
maxColumns: 6,
});

const handleCellSelectionModelChange = React.useCallback(
(newModel: GridCellSelectionModel) => {
setCellSelectionModel(newModel);
},
[],
);

const handleValueChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
},
[],
);

const updateSelectedCells = React.useCallback(() => {
const updates: GridRowModelUpdate[] = [];

Object.entries(cellSelectionModel).forEach(([id, fields]) => {
const updatedRow = { ...apiRef.current.getRow(id) };

Object.entries(fields).forEach(([field, isSelected]) => {
if (isSelected) {
updatedRow[field] = value;
}
});

updates.push(updatedRow);
});

apiRef.current.updateRows(updates);
}, [apiRef, cellSelectionModel, value]);

React.useEffect(() => {
const selectedCells = apiRef.current.unstable_getSelectedCellsAsArray();
setNumberOfSelectedCells(selectedCells.length);

if (selectedCells.length > 1) {
setValue('(multiple values)');
} else if (selectedCells.length === 1) {
setValue(
apiRef.current.getCellValue(selectedCells[0].id, selectedCells[0].field),
);
} else {
setValue('');
}
}, [apiRef, cellSelectionModel]);

return (
<div style={{ width: '100%' }}>
<Stack sx={{ mb: 1 }} direction="row" spacing={2}>
<TextField
label="Selected cell value"
disabled={numberOfSelectedCells === 0}
value={value}
onChange={handleValueChange}
fullWidth
/>
<Button disabled={numberOfSelectedCells === 0} onClick={updateSelectedCells}>
Update selected cells
</Button>
</Stack>
<div style={{ height: 400 }}>
<DataGridPremium
apiRef={apiRef}
rowSelection={false}
unstable_cellSelectionModel={cellSelectionModel}
unstable_onCellSelectionModelChange={handleCellSelectionModelChange}
unstable_cellSelection
{...data}
/>
</div>
</div>
);
}
30 changes: 30 additions & 0 deletions docs/data/data-grid/selection/CellSelectionGrid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import { DataGridPremium } from '@mui/x-data-grid-premium';
import { useDemoData } from '@mui/x-data-grid-generator';

export default function CellSelectionGrid() {
const [rowSelection, setRowSelection] = React.useState(false);

const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 10,
maxColumns: 6,
});

return (
<div style={{ width: '100%' }}>
<Button sx={{ mb: 2 }} onClick={() => setRowSelection(!rowSelection)}>
Toggle row selection
</Button>
<div style={{ height: 400 }}>
<DataGridPremium
rowSelection={rowSelection}
checkboxSelection={rowSelection}
unstable_cellSelection
{...data}
/>
</div>
</div>
);
}
30 changes: 30 additions & 0 deletions docs/data/data-grid/selection/CellSelectionGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import { DataGridPremium } from '@mui/x-data-grid-premium';
import { useDemoData } from '@mui/x-data-grid-generator';

export default function CellSelectionGrid() {
const [rowSelection, setRowSelection] = React.useState(false);

const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 10,
maxColumns: 6,
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to scroll automatically when selecting cells range?
Currently, using a mouse you can only select cell range within visible cells

Screen.Recording.2022-11-09.at.20.16.22.mov

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of UX, from the sources that I could benchmark:

  • Airtable: OK but it feels that it starts scrolling too far away from the bottom of the viewport. It's distracting.
  • Google Sheet: OK but it feels that it starts scrolling too late, only once you are beyond the bottom of the viewport. You don't see what you are going to select next.
  • Notion: The best 🥇. The threshold feels good, and they also have an incremental scrolling speed based on far you are from the bottom of the viewport 👌.

At least, my perspective as a user, I would be curious about @gerdadesign take on this UX.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can implement this later but it's possible. It's similar to what we have for the column headers (although it doesn't work).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's similar to what we have for the column headers (although it doesn't work).

Yes, there's an open issue for it #6236

return (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the Shift + Click range selection, is it possible to "remember" the first cell that started the range?
Like in spreadsheet:

Screen.Recording.2022-11-09.at.20.24.24.mov

Currently, the grid creates new range selection started from the last click cell:

Screen.Recording.2022-11-09.at.20.20.49.mov

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current focus logic, no. The cell with the border is the cell currently focused. When you click the first cell, then the last cell, the focused cell becomes the last clicked one. We would need to split the concept of focused cell between cell that has the focus and cell that has the border. If you create a selection by dragging, not clicking the last cell, then it behaves more like Google Spreadsheet, because the last cell was not clicked and the focus remains in the first cell.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We agreed on keeping the current behavior for now, as it requires some changes in how we manage the focused/outlined cell state.
@m4theushw will submit a follow-up PR to change the cell focus management so it's possible to always start a new range from a first selected cell of a previous range.
Did I capture it correctly?

<div style={{ width: '100%' }}>
<Button sx={{ mb: 2 }} onClick={() => setRowSelection(!rowSelection)}>
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
Toggle row selection
</Button>
<div style={{ height: 400 }}>
<DataGridPremium
rowSelection={rowSelection}
checkboxSelection={rowSelection}
unstable_cellSelection
{...data}
/>
</div>
</div>
);
}
11 changes: 11 additions & 0 deletions docs/data/data-grid/selection/CellSelectionGrid.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Button sx={{ mb: 2 }} onClick={() => setRowSelection(!rowSelection)}>
Toggle row selection
</Button>
<div style={{ height: 400 }}>
<DataGridPremium
rowSelection={rowSelection}
checkboxSelection={rowSelection}
unstable_cellSelection
{...data}
/>
</div>
50 changes: 50 additions & 0 deletions docs/data/data-grid/selection/CellSelectionRangeStyling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import { styled, lighten, darken, alpha } from '@mui/material/styles';
import { DataGridPremium, gridClasses } from '@mui/x-data-grid-premium';
import { useDemoData } from '@mui/x-data-grid-generator';

const StyledDataGridPremium = styled(DataGridPremium)(({ theme }) => {
const borderColor =
theme.palette.mode === 'light'
? lighten(alpha(theme.palette.divider, 1), 0.88)
: darken(alpha(theme.palette.divider, 1), 0.68);

const selectedCellBorder = alpha(theme.palette.primary.main, 0.5);

return {
[`& .${gridClasses.cell}`]: {
border: `1px solid transparent`,
borderRight: `1px solid ${borderColor}`,
borderBottom: `1px solid ${borderColor}`,
},
[`& .${gridClasses.cell}.Mui-selected`]: {
borderColor: alpha(theme.palette.primary.main, 0.1),
},
[`& .${gridClasses.cell}.Mui-selected.${gridClasses['cell--rangeTop']}`]: {
borderTopColor: selectedCellBorder,
},
[`& .${gridClasses.cell}.Mui-selected.${gridClasses['cell--rangeBottom']}`]: {
borderBottomColor: selectedCellBorder,
},
[`& .${gridClasses.cell}.Mui-selected.${gridClasses['cell--rangeLeft']}`]: {
borderLeftColor: selectedCellBorder,
},
[`& .${gridClasses.cell}.Mui-selected.${gridClasses['cell--rangeRight']}`]: {
borderRightColor: selectedCellBorder,
},
};
});

export default function CellSelectionRangeStyling() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 10,
maxColumns: 6,
});

return (
<div style={{ height: 400, width: '100%' }}>
<StyledDataGridPremium rowSelection={false} unstable_cellSelection {...data} />
</div>
);
}